diff --git a/packages/loopover-miner/lib/claim-ledger-cli.d.ts b/packages/loopover-miner/lib/claim-ledger-cli.d.ts index 05b9254760..78dd4620ff 100644 --- a/packages/loopover-miner/lib/claim-ledger-cli.d.ts +++ b/packages/loopover-miner/lib/claim-ledger-cli.d.ts @@ -1,59 +1,43 @@ import type { ClaimEntry, ClaimLedger, ClaimStatus } from "./claim-ledger.js"; - -export type ParsedClaimClaimArgs = - | { - repoFullName: string; - issueNumber: number; - note: string | undefined; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; - } - | { error: string }; - -export type ParsedClaimReleaseArgs = - | { - repoFullName: string; - issueNumber: number; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; - } - | { error: string }; - -export type ParsedClaimListArgs = - | { - json: boolean; - repoFullName: string | null; - status: ClaimStatus | null; - } - | { error: string }; - -export function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs; - -export function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs; - -export function parseClaimListArgs(args: string[]): ParsedClaimListArgs; - -export function renderClaimsTable(entries: ClaimEntry[]): string; - -export function runClaimClaim( - args: string[], - options?: { openClaimLedger?: () => ClaimLedger }, -): number; - -export function runClaimRelease( - args: string[], - options?: { openClaimLedger?: () => ClaimLedger }, -): number; - -export function runClaimList( - args: string[], - options?: { openClaimLedger?: () => ClaimLedger }, -): number; - -export function runClaimCli( - subcommand: string | undefined, - args: string[], - options?: { openClaimLedger?: () => ClaimLedger }, -): number; +export type ParsedClaimClaimArgs = { + repoFullName: string; + issueNumber: number; + note: string | undefined; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; +} | { + error: string; +}; +export type ParsedClaimReleaseArgs = { + repoFullName: string; + issueNumber: number; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; +} | { + error: string; +}; +export type ParsedClaimListArgs = { + json: boolean; + repoFullName: string | null; + status: ClaimStatus | null; +} | { + error: string; +}; +export declare function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs; +export declare function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs; +export declare function parseClaimListArgs(args: string[]): ParsedClaimListArgs; +export declare function renderClaimsTable(entries: ClaimEntry[]): string; +export declare function runClaimClaim(args: string[], options?: { + openClaimLedger?: () => ClaimLedger; +}): number; +export declare function runClaimRelease(args: string[], options?: { + openClaimLedger?: () => ClaimLedger; +}): number; +export declare function runClaimList(args: string[], options?: { + openClaimLedger?: () => ClaimLedger; +}): number; +export declare function runClaimCli(subcommand: string | undefined, args: string[], options?: { + openClaimLedger?: () => ClaimLedger; +}): number; diff --git a/packages/loopover-miner/lib/claim-ledger-cli.js b/packages/loopover-miner/lib/claim-ledger-cli.js index 5d186b4301..76513eab0c 100644 --- a/packages/loopover-miner/lib/claim-ledger-cli.js +++ b/packages/loopover-miner/lib/claim-ledger-cli.js @@ -1,321 +1,318 @@ import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js"; import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; import { isValidRepoSegment } from "./repo-clone.js"; - -const CLAIM_CLAIM_USAGE = - "Usage: loopover-miner claim claim [--note ] [--api-base-url ] [--dry-run] [--json]"; -const CLAIM_RELEASE_USAGE = - "Usage: loopover-miner claim release [--api-base-url ] [--dry-run] [--json]"; -const CLAIM_LIST_USAGE = - "Usage: loopover-miner claim list [--repo ] [--status active|released|expired] [--json]"; - +const CLAIM_CLAIM_USAGE = "Usage: loopover-miner claim claim [--note ] [--api-base-url ] [--dry-run] [--json]"; +const CLAIM_RELEASE_USAGE = "Usage: loopover-miner claim release [--api-base-url ] [--dry-run] [--json]"; +const CLAIM_LIST_USAGE = "Usage: loopover-miner claim list [--repo ] [--status active|released|expired] [--json]"; function parseRepoArg(value, usage) { - if (!value) return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; + if (!value) + return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; } - function parseIssueNumberArg(value, usage) { - if (!value) return { error: usage }; - const parsed = Number(value); - if (!Number.isInteger(parsed) || parsed < 1) { - return { error: "issue number must be a positive integer." }; - } - return { issueNumber: parsed }; + if (!value) + return { error: usage }; + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) { + return { error: "issue number must be a positive integer." }; + } + return { issueNumber: parsed }; } - export function parseClaimClaimArgs(args) { - const options = { json: false, note: undefined, dryRun: false, apiBaseUrl: undefined }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - // #4847: reports what a real claim would do and returns before opening the claim ledger at all. - if (token === "--dry-run") { - options.dryRun = true; - continue; + const options = { + json: false, + note: undefined, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + // #4847: reports what a real claim would do and returns before opening the claim ledger at all. + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--note") { + const note = args[index + 1]; + if (!note || note.startsWith("-")) { + return { error: CLAIM_CLAIM_USAGE }; + } + options.note = note; + index += 1; + continue; + } + // #5563: scope the claim to a non-default forge host, so it doesn't collide with (or get confused for) a + // same-named repo on the default github.com host. + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: CLAIM_CLAIM_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - if (token === "--note") { - const note = args[index + 1]; - if (!note || note.startsWith("-")) { + if (positional.length !== 2) { return { error: CLAIM_CLAIM_USAGE }; - } - options.note = note; - index += 1; - continue; - } - // #5563: scope the claim to a non-default forge host, so it doesn't collide with (or get confused for) a - // same-named repo on the default github.com host. - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { - return { error: CLAIM_CLAIM_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; } - positional.push(token); - } - - if (positional.length !== 2) { - return { error: CLAIM_CLAIM_USAGE }; - } - - const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE); - if ("error" in repo) return repo; - const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE); - if ("error" in issue) return issue; - - return { - repoFullName: repo.repoFullName, - issueNumber: issue.issueNumber, - note: options.note, - dryRun: options.dryRun, - json: options.json, - apiBaseUrl: options.apiBaseUrl, - }; + const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE); + if ("error" in repo) + return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE); + if ("error" in issue) + return issue; + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + note: options.note, + dryRun: options.dryRun, + json: options.json, + apiBaseUrl: options.apiBaseUrl, + }; } - export function parseClaimReleaseArgs(args) { - const options = { json: false, dryRun: false, apiBaseUrl: undefined }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; + const options = { + json: false, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: CLAIM_RELEASE_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { + if (positional.length !== 2) { return { error: CLAIM_RELEASE_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; } - positional.push(token); - } - - if (positional.length !== 2) { - return { error: CLAIM_RELEASE_USAGE }; - } - - const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE); - if ("error" in repo) return repo; - const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE); - if ("error" in issue) return issue; - - return { - repoFullName: repo.repoFullName, - issueNumber: issue.issueNumber, - dryRun: options.dryRun, - json: options.json, - apiBaseUrl: options.apiBaseUrl, - }; + const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE); + if ("error" in repo) + return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE); + if ("error" in issue) + return issue; + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + dryRun: options.dryRun, + json: options.json, + apiBaseUrl: options.apiBaseUrl, + }; } - export function parseClaimListArgs(args) { - const options = { json: false, repoFullName: null, status: null }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; + const options = { + json: false, + repoFullName: null, + status: null, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE); + if ("error" in repo) + return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--status") { + const statusArg = args[index + 1]; + if (!statusArg || statusArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + if (!CLAIM_STATUSES.includes(statusArg)) { + return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` }; + } + options.status = statusArg; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) { + if (positional.length > 0) { return { error: CLAIM_LIST_USAGE }; - } - const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE); - if ("error" in repo) return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token === "--status") { - const statusArg = args[index + 1]; - if (!statusArg || statusArg.startsWith("-")) { - return { error: CLAIM_LIST_USAGE }; - } - if (!CLAIM_STATUSES.includes(statusArg)) { - return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` }; - } - options.status = statusArg; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; } - positional.push(token); - } - - if (positional.length > 0) { - return { error: CLAIM_LIST_USAGE }; - } - - return options; + return options; } - function display(value) { - if (value === null || value === undefined) return "-"; - return String(value); + if (value === null || value === undefined) + return "-"; + return String(value); } - export function renderClaimsTable(entries) { - if (!Array.isArray(entries) || entries.length === 0) return "no claim ledger entries"; - const header = [ - "repo".padEnd(24), - "issue".padStart(6), - "status".padEnd(10), - "claimed-at".padEnd(24), - "note".padEnd(16), - ].join(" "); - const lines = entries.map((entry) => - [ - entry.repoFullName.padEnd(24), - display(entry.issueNumber).padStart(6), - entry.status.padEnd(10), - display(entry.claimedAt).padEnd(24), - display(entry.note).padEnd(16), - ].join(" "), - ); - return [header, ...lines].join("\n"); + if (!Array.isArray(entries) || entries.length === 0) + return "no claim ledger entries"; + const header = [ + "repo".padEnd(24), + "issue".padStart(6), + "status".padEnd(10), + "claimed-at".padEnd(24), + "note".padEnd(16), + ].join(" "); + const lines = entries.map((entry) => [ + entry.repoFullName.padEnd(24), + display(entry.issueNumber).padStart(6), + entry.status.padEnd(10), + display(entry.claimedAt).padEnd(24), + display(entry.note).padEnd(16), + ].join(" ")); + return [header, ...lines].join("\n"); } - function withClaimLedger(options, run) { - const ownsLedger = options.openClaimLedger === undefined; - const claimLedger = (options.openClaimLedger ?? openClaimLedger)(); - try { - return run(claimLedger); - } finally { - if (ownsLedger) claimLedger.close(); - } + const ownsLedger = options.openClaimLedger === undefined; + const claimLedger = (options.openClaimLedger ?? openClaimLedger)(); + try { + return run(claimLedger); + } + finally { + if (ownsLedger) + claimLedger.close(); + } } - export function runClaimClaim(args, options = {}) { - const parsed = parseClaimClaimArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, note: parsed.note ?? null }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } else { - console.log( - `DRY RUN: would claim ${parsed.repoFullName}#${parsed.issueNumber}${parsed.note ? ` (note: ${parsed.note})` : ""}. No claim-ledger write was made.`, - ); + const parsed = parseClaimClaimArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, note: parsed.note ?? null }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult, null, 2)); + } + else { + console.log(`DRY RUN: would claim ${parsed.repoFullName}#${parsed.issueNumber}${parsed.note ? ` (note: ${parsed.note})` : ""}. No claim-ledger write was made.`); + } + return 0; + } + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.claimIssue(parsed.repoFullName, parsed.issueNumber, parsed.note, parsed.apiBaseUrl); + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } + else { + console.log(claim.status); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); } - return 0; - } - - try { - return withClaimLedger(options, (claimLedger) => { - const claim = claimLedger.claimIssue( - parsed.repoFullName, - parsed.issueNumber, - parsed.note, - parsed.apiBaseUrl, - ); - if (parsed.json) { - console.log(JSON.stringify({ claim }, null, 2)); - } else { - console.log(claim.status); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } } - export function runClaimRelease(args, options = {}) { - const parsed = parseClaimReleaseArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult, null, 2)); - } else { - console.log(`DRY RUN: would release the claim on ${parsed.repoFullName}#${parsed.issueNumber}. No claim-ledger write was made.`); + const parsed = parseClaimReleaseArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult, null, 2)); + } + else { + console.log(`DRY RUN: would release the claim on ${parsed.repoFullName}#${parsed.issueNumber}. No claim-ledger write was made.`); + } + return 0; + } + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber, parsed.apiBaseUrl); + if (!claim) { + return reportCliFailure(parsed.json, "claim_not_found"); + } + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } + else { + console.log(claim.status); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); } - return 0; - } - - try { - return withClaimLedger(options, (claimLedger) => { - const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber, parsed.apiBaseUrl); - if (!claim) { - return reportCliFailure(parsed.json, "claim_not_found"); - } - if (parsed.json) { - console.log(JSON.stringify({ claim }, null, 2)); - } else { - console.log(claim.status); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } } - export function runClaimList(args, options = {}) { - const parsed = parseClaimListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - return withClaimLedger(options, (claimLedger) => { - const filter = {}; - if (parsed.repoFullName !== null) filter.repoFullName = parsed.repoFullName; - if (parsed.status !== null) filter.status = parsed.status; - const claims = claimLedger.listClaims(filter); - if (parsed.json) { - console.log(JSON.stringify({ claims }, null, 2)); - } else { - console.log(renderClaimsTable(claims)); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } + const parsed = parseClaimListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + return withClaimLedger(options, (claimLedger) => { + const filter = {}; + if (parsed.repoFullName !== null) + filter.repoFullName = parsed.repoFullName; + if (parsed.status !== null) + filter.status = parsed.status; + const claims = claimLedger.listClaims(filter); + if (parsed.json) { + console.log(JSON.stringify({ claims }, null, 2)); + } + else { + console.log(renderClaimsTable(claims)); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } } - export function runClaimCli(subcommand, args, options = {}) { - if (subcommand === "claim") return runClaimClaim(args, options); - if (subcommand === "release") return runClaimRelease(args, options); - if (subcommand === "list") return runClaimList(args, options); - return reportCliFailure(argsWantJson(args), `Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`); + if (subcommand === "claim") + return runClaimClaim(args, options); + if (subcommand === "release") + return runClaimRelease(args, options); + if (subcommand === "list") + return runClaimList(args, options); + return reportCliFailure(argsWantJson(args), `Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`); } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhaW0tbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsYWltLWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGNBQWMsRUFBRSxlQUFlLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQztBQUVwRSxPQUFPLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDbEYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFFckQsTUFBTSxpQkFBaUIsR0FDckIscUhBQXFILENBQUM7QUFDeEgsTUFBTSxtQkFBbUIsR0FDdkIsdUdBQXVHLENBQUM7QUFDMUcsTUFBTSxnQkFBZ0IsR0FDcEIsb0dBQW9HLENBQUM7QUFpQ3ZHLFNBQVMsWUFBWSxDQUFDLEtBQXlCLEVBQUUsS0FBYTtJQUM1RCxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDcEMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzdCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUyxJQUFJLENBQUMsa0JBQWtCLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ3RHLE9BQU8sRUFBRSxLQUFLLEVBQUUsd0NBQXdDLEVBQUUsQ0FBQztJQUM3RCxDQUFDO0lBQ0QsT0FBTyxFQUFFLFlBQVksRUFBRSxHQUFHLEtBQUssSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDO0FBQzlDLENBQUM7QUFFRCxTQUFTLG1CQUFtQixDQUFDLEtBQXlC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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/claim-ledger-cli.ts b/packages/loopover-miner/lib/claim-ledger-cli.ts new file mode 100644 index 0000000000..ba9da50db9 --- /dev/null +++ b/packages/loopover-miner/lib/claim-ledger-cli.ts @@ -0,0 +1,366 @@ +import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js"; +import type { ClaimEntry, ClaimLedger, ClaimStatus } from "./claim-ledger.js"; +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; +import { isValidRepoSegment } from "./repo-clone.js"; + +const CLAIM_CLAIM_USAGE = + "Usage: loopover-miner claim claim [--note ] [--api-base-url ] [--dry-run] [--json]"; +const CLAIM_RELEASE_USAGE = + "Usage: loopover-miner claim release [--api-base-url ] [--dry-run] [--json]"; +const CLAIM_LIST_USAGE = + "Usage: loopover-miner claim list [--repo ] [--status active|released|expired] [--json]"; + +export type ParsedClaimClaimArgs = + | { + repoFullName: string; + issueNumber: number; + note: string | undefined; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; + } + | { error: string }; + +export type ParsedClaimReleaseArgs = + | { + repoFullName: string; + issueNumber: number; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; + } + | { error: string }; + +export type ParsedClaimListArgs = + | { + json: boolean; + repoFullName: string | null; + status: ClaimStatus | null; + } + | { error: string }; + +type ParsedRepoArg = { repoFullName: string } | { error: string }; + +function parseRepoArg(value: string | undefined, usage: string): ParsedRepoArg { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +function parseIssueNumberArg(value: string | undefined, usage: string): { issueNumber: number } | { error: string } { + if (!value) return { error: usage }; + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) { + return { error: "issue number must be a positive integer." }; + } + return { issueNumber: parsed }; +} + +export function parseClaimClaimArgs(args: string[]): ParsedClaimClaimArgs { + const options: { json: boolean; note: string | undefined; dryRun: boolean; apiBaseUrl: string | undefined } = { + json: false, + note: undefined, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + // #4847: reports what a real claim would do and returns before opening the claim ledger at all. + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--note") { + const note = args[index + 1]; + if (!note || note.startsWith("-")) { + return { error: CLAIM_CLAIM_USAGE }; + } + options.note = note; + index += 1; + continue; + } + // #5563: scope the claim to a non-default forge host, so it doesn't collide with (or get confused for) a + // same-named repo on the default github.com host. + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: CLAIM_CLAIM_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 2) { + return { error: CLAIM_CLAIM_USAGE }; + } + + const repo = parseRepoArg(positional[0], CLAIM_CLAIM_USAGE); + if ("error" in repo) return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_CLAIM_USAGE); + if ("error" in issue) return issue; + + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + note: options.note, + dryRun: options.dryRun, + json: options.json, + apiBaseUrl: options.apiBaseUrl, + }; +} + +export function parseClaimReleaseArgs(args: string[]): ParsedClaimReleaseArgs { + const options: { json: boolean; dryRun: boolean; apiBaseUrl: string | undefined } = { + json: false, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: CLAIM_RELEASE_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 2) { + return { error: CLAIM_RELEASE_USAGE }; + } + + const repo = parseRepoArg(positional[0], CLAIM_RELEASE_USAGE); + if ("error" in repo) return repo; + const issue = parseIssueNumberArg(positional[1], CLAIM_RELEASE_USAGE); + if ("error" in issue) return issue; + + return { + repoFullName: repo.repoFullName, + issueNumber: issue.issueNumber, + dryRun: options.dryRun, + json: options.json, + apiBaseUrl: options.apiBaseUrl, + }; +} + +export function parseClaimListArgs(args: string[]): ParsedClaimListArgs { + const options: { json: boolean; repoFullName: string | null; status: ClaimStatus | null } = { + json: false, + repoFullName: null, + status: null, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + const repo = parseRepoArg(repoArg, CLAIM_LIST_USAGE); + if ("error" in repo) return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--status") { + const statusArg = args[index + 1]; + if (!statusArg || statusArg.startsWith("-")) { + return { error: CLAIM_LIST_USAGE }; + } + if (!(CLAIM_STATUSES as readonly string[]).includes(statusArg)) { + return { error: `status must be one of: ${CLAIM_STATUSES.join(", ")}.` }; + } + options.status = statusArg as ClaimStatus; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length > 0) { + return { error: CLAIM_LIST_USAGE }; + } + + return options; +} + +function display(value: unknown): string { + if (value === null || value === undefined) return "-"; + return String(value); +} + +export function renderClaimsTable(entries: ClaimEntry[]): string { + if (!Array.isArray(entries) || entries.length === 0) return "no claim ledger entries"; + const header = [ + "repo".padEnd(24), + "issue".padStart(6), + "status".padEnd(10), + "claimed-at".padEnd(24), + "note".padEnd(16), + ].join(" "); + const lines = entries.map((entry) => + [ + entry.repoFullName.padEnd(24), + display(entry.issueNumber).padStart(6), + entry.status.padEnd(10), + display(entry.claimedAt).padEnd(24), + display(entry.note).padEnd(16), + ].join(" "), + ); + return [header, ...lines].join("\n"); +} + +function withClaimLedger(options: { openClaimLedger?: () => ClaimLedger }, run: (claimLedger: ClaimLedger) => T): T { + const ownsLedger = options.openClaimLedger === undefined; + const claimLedger = (options.openClaimLedger ?? openClaimLedger)(); + try { + return run(claimLedger); + } finally { + if (ownsLedger) claimLedger.close(); + } +} + +export function runClaimClaim(args: string[], options: { openClaimLedger?: () => ClaimLedger } = {}): number { + const parsed = parseClaimClaimArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber, note: parsed.note ?? null }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult, null, 2)); + } else { + console.log( + `DRY RUN: would claim ${parsed.repoFullName}#${parsed.issueNumber}${parsed.note ? ` (note: ${parsed.note})` : ""}. No claim-ledger write was made.`, + ); + } + return 0; + } + + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.claimIssue( + parsed.repoFullName, + parsed.issueNumber, + parsed.note, + parsed.apiBaseUrl, + ); + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } else { + console.log(claim.status); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runClaimRelease(args: string[], options: { openClaimLedger?: () => ClaimLedger } = {}): number { + const parsed = parseClaimReleaseArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, issueNumber: parsed.issueNumber }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult, null, 2)); + } else { + console.log(`DRY RUN: would release the claim on ${parsed.repoFullName}#${parsed.issueNumber}. No claim-ledger write was made.`); + } + return 0; + } + + try { + return withClaimLedger(options, (claimLedger) => { + const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber, parsed.apiBaseUrl); + if (!claim) { + return reportCliFailure(parsed.json, "claim_not_found"); + } + if (parsed.json) { + console.log(JSON.stringify({ claim }, null, 2)); + } else { + console.log(claim.status); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runClaimList(args: string[], options: { openClaimLedger?: () => ClaimLedger } = {}): number { + const parsed = parseClaimListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + return withClaimLedger(options, (claimLedger) => { + const filter: { repoFullName?: string; status?: ClaimStatus } = {}; + if (parsed.repoFullName !== null) filter.repoFullName = parsed.repoFullName; + if (parsed.status !== null) filter.status = parsed.status; + const claims = claimLedger.listClaims(filter); + if (parsed.json) { + console.log(JSON.stringify({ claims }, null, 2)); + } else { + console.log(renderClaimsTable(claims)); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runClaimCli(subcommand: string | undefined, args: string[], options: { openClaimLedger?: () => ClaimLedger } = {}): number { + if (subcommand === "claim") return runClaimClaim(args, options); + if (subcommand === "release") return runClaimRelease(args, options); + if (subcommand === "list") return runClaimList(args, options); + return reportCliFailure(argsWantJson(args), `Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`); +} diff --git a/packages/loopover-miner/lib/event-ledger-cli.d.ts b/packages/loopover-miner/lib/event-ledger-cli.d.ts index 6741347b6c..5962b1f915 100644 --- a/packages/loopover-miner/lib/event-ledger-cli.d.ts +++ b/packages/loopover-miner/lib/event-ledger-cli.d.ts @@ -1,82 +1,65 @@ import type { EventLedger, LedgerEntry } from "./event-ledger.js"; - -export type ParsedLedgerListArgs = - | { - json: boolean; - repoFullName: string | null; - since: number | null; - type: string | null; - } - | { error: string }; - -export function parseLedgerListArgs(args: string[]): ParsedLedgerListArgs; - -export function filterLedgerEvents( - events: LedgerEntry[], - options?: { type?: string | null }, -): LedgerEntry[]; - -export const AUDIT_FEED_ENTRY_FIELDS: readonly [ - "eventType", - "repoFullName", - "outcome", - "actor", - "detail", - "createdAt", -]; - -export function projectLedgerEventToAuditFeedEntry(entry: LedgerEntry): { - eventType: string; - repoFullName: string | null; - outcome: string | null; - actor: string | null; - detail: string | null; - createdAt: string; -}; - -export type AuditFeedMcpFilterInput = { - repoFullName?: string | null; - since?: number | null; - type?: string | null; -}; - -export function normalizeAuditFeedMcpFilter(input?: AuditFeedMcpFilterInput): { - repoFullName: string | null; - since: number | null; - type: string | null; +export type ParsedLedgerListArgs = { + json: boolean; + repoFullName: string | null; + since: number | null; + type: string | null; +} | { + error: string; }; - -export function collectEventLedgerAuditFeed( - eventLedger: EventLedger, - filter?: { repoFullName?: string | null; since?: number | null; type?: string | null }, -): { - repoFullName?: string; - events: Array<{ +export declare function parseLedgerListArgs(args: string[]): ParsedLedgerListArgs; +export declare function filterLedgerEvents(events: LedgerEntry[], options?: { + type?: string | null; +}): LedgerEntry[]; +/** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */ +export declare const AUDIT_FEED_ENTRY_FIELDS: readonly ["eventType", "repoFullName", "outcome", "actor", "detail", "createdAt"]; +type AuditFeedEntry = { eventType: string; repoFullName: string | null; outcome: string | null; actor: string | null; detail: string | null; createdAt: string; - }>; }; - -export function renderLedgerTable(events: LedgerEntry[]): string; - -export function renderEventLedgerMetrics(events: readonly LedgerEntry[]): string; - -export function runLedgerList( - args: string[], - options?: { initEventLedger?: () => EventLedger }, -): number; - -export function runLedgerMetrics( - args: string[], - options?: { initEventLedger?: () => EventLedger }, -): number; - -export function runLedgerCli( - subcommand: string | undefined, - args: string[], - options?: { initEventLedger?: () => EventLedger }, -): number; +/** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */ +export declare function projectLedgerEventToAuditFeedEntry(entry: LedgerEntry): AuditFeedEntry; +export type AuditFeedMcpFilterInput = { + repoFullName?: string | null; + since?: number | null; + type?: string | null; +}; +type AuditFeedFilter = { + repoFullName: string | null; + since: number | null; + type: string | null; +}; +/** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */ +export declare function normalizeAuditFeedMcpFilter(input?: AuditFeedMcpFilterInput): AuditFeedFilter; +/** Read-only audit feed shared by the MCP audit-feed tool (#5158). */ +export declare function collectEventLedgerAuditFeed(eventLedger: EventLedger, filter?: { + repoFullName?: string | null; + since?: number | null; + type?: string | null; +}): { + repoFullName?: string; + events: AuditFeedEntry[]; +}; +export declare function renderLedgerTable(events: LedgerEntry[]): string; +/** + * Render event-ledger activity as Prometheus text-exposition counters: one `loopover_miner_events_total{type}` + * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling + * `ledger list --json` (#4841). Pure + side-effect-free — the caller supplies the rows and prints the result; + * deterministic (series emitted in sorted type order); always emits HELP/TYPE so an empty ledger is still a + * well-formed exposition document. + */ +export declare function renderEventLedgerMetrics(events: readonly LedgerEntry[]): string; +export declare function runLedgerList(args: string[], options?: { + initEventLedger?: () => EventLedger; +}): number; +export declare function runLedgerMetrics(args: string[], options?: { + initEventLedger?: () => EventLedger; +}): number; +export declare function runLedgerCli(subcommand: string | undefined, args: string[], options?: { + initEventLedger?: () => EventLedger; +}): number; +export {}; diff --git a/packages/loopover-miner/lib/event-ledger-cli.js b/packages/loopover-miner/lib/event-ledger-cli.js index 32ce6a5d71..0683ed7e2f 100644 --- a/packages/loopover-miner/lib/event-ledger-cli.js +++ b/packages/loopover-miner/lib/event-ledger-cli.js @@ -1,189 +1,186 @@ import { initEventLedger } from "./event-ledger.js"; import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; import { isValidRepoSegment } from "./repo-clone.js"; - -const LEDGER_LIST_USAGE = - "Usage: loopover-miner ledger list [--repo ] [--since ] [--type ] [--json]"; - +const LEDGER_LIST_USAGE = "Usage: loopover-miner ledger list [--repo ] [--since ] [--type ] [--json]"; function parseRepoArg(value, usage) { - if (!value) return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; + if (!value) + return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; } - function parseSinceArg(value) { - const since = Number(value); - if (!Number.isInteger(since) || since < 0) { - return { error: "since must be a non-negative integer seq cursor." }; - } - return { since }; + const since = Number(value); + if (!Number.isInteger(since) || since < 0) { + return { error: "since must be a non-negative integer seq cursor." }; + } + return { since }; } - export function parseLedgerListArgs(args) { - const options = { json: false, repoFullName: null, since: null, type: null }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) return { error: LEDGER_LIST_USAGE }; - const repo = parseRepoArg(repoArg, LEDGER_LIST_USAGE); - if ("error" in repo) return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - if (token === "--since") { - const sinceArg = args[index + 1]; - if (!sinceArg || sinceArg.startsWith("--")) return { error: LEDGER_LIST_USAGE }; - const parsedSince = parseSinceArg(sinceArg); - if ("error" in parsedSince) return parsedSince; - options.since = parsedSince.since; - index += 1; - continue; - } - if (token === "--type") { - const type = args[index + 1]; - if (!type || type.startsWith("-")) return { error: LEDGER_LIST_USAGE }; - options.type = type.trim(); - index += 1; - continue; + const options = { + json: false, + repoFullName: null, + since: null, + type: null, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) + return { error: LEDGER_LIST_USAGE }; + const repo = parseRepoArg(repoArg, LEDGER_LIST_USAGE); + if ("error" in repo) + return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--since") { + const sinceArg = args[index + 1]; + if (!sinceArg || sinceArg.startsWith("--")) + return { error: LEDGER_LIST_USAGE }; + const parsedSince = parseSinceArg(sinceArg); + if ("error" in parsedSince) + return parsedSince; + options.since = parsedSince.since; + index += 1; + continue; + } + if (token === "--type") { + const type = args[index + 1]; + if (!type || type.startsWith("-")) + return { error: LEDGER_LIST_USAGE }; + options.type = type.trim(); + index += 1; + continue; + } + if (token.startsWith("-")) + return { error: `Unknown option: ${token}` }; + positional.push(token); } - if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; - positional.push(token); - } - - if (positional.length > 0) return { error: LEDGER_LIST_USAGE }; - return options; + if (positional.length > 0) + return { error: LEDGER_LIST_USAGE }; + return options; } - export function filterLedgerEvents(events, options = {}) { - if (!Array.isArray(events)) return []; - const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; - if (!type) return events; - return events.filter((entry) => entry.type === type); + if (!Array.isArray(events)) + return []; + const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; + if (!type) + return events; + return events.filter((entry) => entry.type === type); } - /** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */ export const AUDIT_FEED_ENTRY_FIELDS = Object.freeze([ - "eventType", - "repoFullName", - "outcome", - "actor", - "detail", - "createdAt", + "eventType", + "repoFullName", + "outcome", + "actor", + "detail", + "createdAt", ]); - function optionalMetadataString(value) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed || null; + if (typeof value !== "string") + return null; + const trimmed = value.trim(); + return trimmed || null; } - /** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */ export function projectLedgerEventToAuditFeedEntry(entry) { - const payload = - entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) ? entry.payload : {}; - return { - eventType: entry.type, - repoFullName: entry.repoFullName, - outcome: optionalMetadataString(payload.outcome), - actor: optionalMetadataString(payload.actor), - detail: optionalMetadataString(payload.detail), - createdAt: entry.createdAt, - }; + const payload = entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) + ? entry.payload + : {}; + return { + eventType: entry.type, + repoFullName: entry.repoFullName, + outcome: optionalMetadataString(payload.outcome), + actor: optionalMetadataString(payload.actor), + detail: optionalMetadataString(payload.detail), + createdAt: entry.createdAt, + }; } - /** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */ export function normalizeAuditFeedMcpFilter(input = {}) { - if (input === null || typeof input !== "object" || Array.isArray(input)) { - throw new Error("filter must be an object"); - } - const filter = { repoFullName: null, since: null, type: null }; - if (input.repoFullName !== undefined && input.repoFullName !== null) { - const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form."); - if ("error" in repo) throw new Error(repo.error); - filter.repoFullName = repo.repoFullName; - } - if (input.since !== undefined && input.since !== null) { - const parsedSince = parseSinceArg(String(input.since)); - if ("error" in parsedSince) throw new Error(parsedSince.error); - filter.since = parsedSince.since; - } - if (input.type !== undefined && input.type !== null) { - const trimmed = String(input.type).trim(); - if (!trimmed) throw new Error("type must be a non-empty string."); - filter.type = trimmed; - } - return filter; + if (input === null || typeof input !== "object" || Array.isArray(input)) { + throw new Error("filter must be an object"); + } + const filter = { repoFullName: null, since: null, type: null }; + if (input.repoFullName !== undefined && input.repoFullName !== null) { + const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form."); + if ("error" in repo) + throw new Error(repo.error); + filter.repoFullName = repo.repoFullName; + } + if (input.since !== undefined && input.since !== null) { + const parsedSince = parseSinceArg(String(input.since)); + if ("error" in parsedSince) + throw new Error(parsedSince.error); + filter.since = parsedSince.since; + } + if (input.type !== undefined && input.type !== null) { + const trimmed = String(input.type).trim(); + if (!trimmed) + throw new Error("type must be a non-empty string."); + filter.type = trimmed; + } + return filter; } - /** Read-only audit feed shared by the MCP audit-feed tool (#5158). */ export function collectEventLedgerAuditFeed(eventLedger, filter = {}) { - const events = filterLedgerEvents( - eventLedger.readEvents({ - repoFullName: filter.repoFullName, - since: filter.since, - }), - { type: filter.type }, - ); - return { - ...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}), - events: events.map(projectLedgerEventToAuditFeedEntry), - }; + const events = filterLedgerEvents(eventLedger.readEvents({ + repoFullName: filter.repoFullName ?? null, + since: filter.since ?? null, + }), { type: filter.type ?? null }); + return { + ...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}), + events: events.map(projectLedgerEventToAuditFeedEntry), + }; } - function display(value) { - if (value === null || value === undefined) return "-"; - return String(value); + if (value === null || value === undefined) + return "-"; + return String(value); } - export function renderLedgerTable(events) { - if (!Array.isArray(events) || events.length === 0) return "no event ledger entries"; - const header = [ - "seq".padStart(4), - "type".padEnd(20), - "repo".padEnd(24), - "created-at".padEnd(24), - ].join(" "); - const lines = events.map((entry) => - [ - String(entry.seq).padStart(4), - entry.type.padEnd(20), - display(entry.repoFullName).padEnd(24), - display(entry.createdAt).padEnd(24), - ].join(" "), - ); - return [header, ...lines].join("\n"); + if (!Array.isArray(events) || events.length === 0) + return "no event ledger entries"; + const header = [ + "seq".padStart(4), + "type".padEnd(20), + "repo".padEnd(24), + "created-at".padEnd(24), + ].join(" "); + const lines = events.map((entry) => [ + String(entry.seq).padStart(4), + entry.type.padEnd(20), + display(entry.repoFullName).padEnd(24), + display(entry.createdAt).padEnd(24), + ].join(" ")); + return [header, ...lines].join("\n"); } - const EVENT_LEDGER_METRICS_USAGE = "Usage: loopover-miner ledger metrics"; - // Prometheus metric name for the per-type event-ledger counter. Mirrors the `loopover_miner_*_total` naming and // the HELP/TYPE/label conventions of the engine's renderMinerPredictionMetrics // (packages/loopover-engine/src/miner-prediction-metrics.ts) rather than importing across the package boundary. const MINER_EVENTS_TOTAL = "loopover_miner_events_total"; - /** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */ function escapeHelpText(help) { - return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); + return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); } - /** Prometheus label-value escaping — backslash, double-quote, newline — so an arbitrary event `type` string can * never break the metric line (mirrors miner-prediction-metrics.ts's escapeLabelValue). */ function escapeLabelValue(value) { - return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); } - /** * Render event-ledger activity as Prometheus text-exposition counters: one `loopover_miner_events_total{type}` * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling @@ -192,76 +189,75 @@ function escapeLabelValue(value) { * well-formed exposition document. */ export function renderEventLedgerMetrics(events) { - const totalByType = new Map(); - for (const entry of events) { - totalByType.set(entry.type, (totalByType.get(entry.type) ?? 0) + 1); - } - const lines = [ - `# HELP ${MINER_EVENTS_TOTAL} ${escapeHelpText("Event-ledger entries the miner has recorded, by event type.")}`, - `# TYPE ${MINER_EVENTS_TOTAL} counter`, - ]; - for (const [type, count] of [...totalByType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { - lines.push(`${MINER_EVENTS_TOTAL}{type="${escapeLabelValue(type)}"} ${count}`); - } - return `${lines.join("\n")}\n`; + const totalByType = new Map(); + for (const entry of events) { + totalByType.set(entry.type, (totalByType.get(entry.type) ?? 0) + 1); + } + const lines = [ + `# HELP ${MINER_EVENTS_TOTAL} ${escapeHelpText("Event-ledger entries the miner has recorded, by event type.")}`, + `# TYPE ${MINER_EVENTS_TOTAL} counter`, + ]; + for (const [type, count] of [...totalByType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { + lines.push(`${MINER_EVENTS_TOTAL}{type="${escapeLabelValue(type)}"} ${count}`); + } + return `${lines.join("\n")}\n`; } - function withEventLedger(options, run) { - const ownsLedger = options.initEventLedger === undefined; - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - try { - return run(eventLedger); - } finally { - if (ownsLedger) eventLedger.close(); - } + const ownsLedger = options.initEventLedger === undefined; + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + try { + return run(eventLedger); + } + finally { + if (ownsLedger) + eventLedger.close(); + } } - export function runLedgerList(args, options = {}) { - const parsed = parseLedgerListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - return withEventLedger(options, (eventLedger) => { - const events = filterLedgerEvents( - eventLedger.readEvents({ - repoFullName: parsed.repoFullName, - since: parsed.since, - }), - { type: parsed.type }, - ); - if (parsed.json) { - console.log(JSON.stringify({ events }, null, 2)); - } else { - console.log(renderLedgerTable(events)); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } + const parsed = parseLedgerListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + return withEventLedger(options, (eventLedger) => { + const events = filterLedgerEvents(eventLedger.readEvents({ + repoFullName: parsed.repoFullName, + since: parsed.since, + }), { type: parsed.type }); + if (parsed.json) { + console.log(JSON.stringify({ events }, null, 2)); + } + else { + console.log(renderLedgerTable(events)); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } } - export function runLedgerMetrics(args, options = {}) { - if (args.length > 0) { - return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE); - } - - try { - return withEventLedger(options, (eventLedger) => { - // renderEventLedgerMetrics returns a newline-terminated document; console.log re-adds the terminator, so - // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics). - console.log(renderEventLedgerMetrics(eventLedger.readEvents()).trimEnd()); - return 0; - }); - } catch (error) { - return reportCliFailure(argsWantJson(args), describeCliError(error)); - } + if (args.length > 0) { + return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE); + } + try { + return withEventLedger(options, (eventLedger) => { + // renderEventLedgerMetrics returns a newline-terminated document; console.log re-adds the terminator, so + // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics). + console.log(renderEventLedgerMetrics(eventLedger.readEvents()).trimEnd()); + return 0; + }); + } + catch (error) { + return reportCliFailure(argsWantJson(args), describeCliError(error)); + } } - export function runLedgerCli(subcommand, args, options = {}) { - if (subcommand === "list") return runLedgerList(args, options); - if (subcommand === "metrics") return runLedgerMetrics(args, options); - return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`); + if (subcommand === "list") + return runLedgerList(args, options); + if (subcommand === "metrics") + return runLedgerMetrics(args, options); + return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`); } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXZlbnQtbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImV2ZW50LWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXBELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNsRixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUVyRCxNQUFNLGlCQUFpQixHQUNyQix1R0FBdUcsQ0FBQztBQWExRyxTQUFTLFlBQVksQ0FBQyxLQUF5QixFQUFFLEtBQWE7SUFDNUQsSUFBSSxDQUFDLEtBQUs7UUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxDQUFDO0lBQ3BDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM3QixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVMsSUFBSSxDQUFDLGtCQUFrQixDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsa0JBQWtCLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUN0RyxPQUFPLEVBQUUsS0FBSyxFQUFFLHdDQUF3QyxFQUFFLENBQUM7SUFDN0QsQ0FBQztJQUNELE9BQU8sRUFBRSxZQUFZLEVBQUUsR0FBRyxLQUFLLElBQUksSUFBSSxFQUFFLEVBQUUsQ0FBQztBQUM5QyxDQUFDO0FBRUQsU0FBUyxhQUFhLENBQUMsS0FBYTtJQUNsQyxNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7SUFDNUIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxTQUFTLENBQUMsS0FB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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/event-ledger-cli.ts b/packages/loopover-miner/lib/event-ledger-cli.ts new file mode 100644 index 0000000000..a019e2b806 --- /dev/null +++ b/packages/loopover-miner/lib/event-ledger-cli.ts @@ -0,0 +1,306 @@ +import { initEventLedger } from "./event-ledger.js"; +import type { EventLedger, LedgerEntry } from "./event-ledger.js"; +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; +import { isValidRepoSegment } from "./repo-clone.js"; + +const LEDGER_LIST_USAGE = + "Usage: loopover-miner ledger list [--repo ] [--since ] [--type ] [--json]"; + +export type ParsedLedgerListArgs = + | { + json: boolean; + repoFullName: string | null; + since: number | null; + type: string | null; + } + | { error: string }; + +type ParsedRepoArg = { repoFullName: string } | { error: string }; + +function parseRepoArg(value: string | undefined, usage: string): ParsedRepoArg { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined || !isValidRepoSegment(owner) || !isValidRepoSegment(repo)) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +function parseSinceArg(value: string): { since: number } | { error: string } { + const since = Number(value); + if (!Number.isInteger(since) || since < 0) { + return { error: "since must be a non-negative integer seq cursor." }; + } + return { since }; +} + +export function parseLedgerListArgs(args: string[]): ParsedLedgerListArgs { + const options: { json: boolean; repoFullName: string | null; since: number | null; type: string | null } = { + json: false, + repoFullName: null, + since: null, + type: null, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) return { error: LEDGER_LIST_USAGE }; + const repo = parseRepoArg(repoArg, LEDGER_LIST_USAGE); + if ("error" in repo) return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--since") { + const sinceArg = args[index + 1]; + if (!sinceArg || sinceArg.startsWith("--")) return { error: LEDGER_LIST_USAGE }; + const parsedSince = parseSinceArg(sinceArg); + if ("error" in parsedSince) return parsedSince; + options.since = parsedSince.since; + index += 1; + continue; + } + if (token === "--type") { + const type = args[index + 1]; + if (!type || type.startsWith("-")) return { error: LEDGER_LIST_USAGE }; + options.type = type.trim(); + index += 1; + continue; + } + if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; + positional.push(token); + } + + if (positional.length > 0) return { error: LEDGER_LIST_USAGE }; + return options; +} + +export function filterLedgerEvents(events: LedgerEntry[], options: { type?: string | null } = {}): LedgerEntry[] { + if (!Array.isArray(events)) return []; + const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; + if (!type) return events; + return events.filter((entry) => entry.type === type); +} + +/** Metadata-only audit-feed columns exposed by the MCP tool (#5158). */ +export const AUDIT_FEED_ENTRY_FIELDS = Object.freeze([ + "eventType", + "repoFullName", + "outcome", + "actor", + "detail", + "createdAt", +] as const); + +function optionalMetadataString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed || null; +} + +type AuditFeedEntry = { + eventType: string; + repoFullName: string | null; + outcome: string | null; + actor: string | null; + detail: string | null; + createdAt: string; +}; + +/** Project one ledger row to the public, metadata-only audit-feed shape — never returns payload_json. */ +export function projectLedgerEventToAuditFeedEntry(entry: LedgerEntry): AuditFeedEntry { + const payload: Record = + entry?.payload && typeof entry.payload === "object" && !Array.isArray(entry.payload) + ? (entry.payload as Record) + : {}; + return { + eventType: entry.type, + repoFullName: entry.repoFullName, + outcome: optionalMetadataString(payload.outcome), + actor: optionalMetadataString(payload.actor), + detail: optionalMetadataString(payload.detail), + createdAt: entry.createdAt, + }; +} + +export type AuditFeedMcpFilterInput = { + repoFullName?: string | null; + since?: number | null; + type?: string | null; +}; + +type AuditFeedFilter = { repoFullName: string | null; since: number | null; type: string | null }; + +/** Normalize optional MCP/JSON filter args into the shape `ledger list` already uses (#5158). */ +export function normalizeAuditFeedMcpFilter(input: AuditFeedMcpFilterInput = {}): AuditFeedFilter { + if (input === null || typeof input !== "object" || Array.isArray(input)) { + throw new Error("filter must be an object"); + } + const filter: AuditFeedFilter = { repoFullName: null, since: null, type: null }; + if (input.repoFullName !== undefined && input.repoFullName !== null) { + const repo = parseRepoArg(String(input.repoFullName), "repoFullName must be in owner/repo form."); + if ("error" in repo) throw new Error(repo.error); + filter.repoFullName = repo.repoFullName; + } + if (input.since !== undefined && input.since !== null) { + const parsedSince = parseSinceArg(String(input.since)); + if ("error" in parsedSince) throw new Error(parsedSince.error); + filter.since = parsedSince.since; + } + if (input.type !== undefined && input.type !== null) { + const trimmed = String(input.type).trim(); + if (!trimmed) throw new Error("type must be a non-empty string."); + filter.type = trimmed; + } + return filter; +} + +/** Read-only audit feed shared by the MCP audit-feed tool (#5158). */ +export function collectEventLedgerAuditFeed( + eventLedger: EventLedger, + filter: { repoFullName?: string | null; since?: number | null; type?: string | null } = {}, +): { repoFullName?: string; events: AuditFeedEntry[] } { + const events = filterLedgerEvents( + eventLedger.readEvents({ + repoFullName: filter.repoFullName ?? null, + since: filter.since ?? null, + }), + { type: filter.type ?? null }, + ); + return { + ...(filter.repoFullName ? { repoFullName: filter.repoFullName } : {}), + events: events.map(projectLedgerEventToAuditFeedEntry), + }; +} + +function display(value: unknown): string { + if (value === null || value === undefined) return "-"; + return String(value); +} + +export function renderLedgerTable(events: LedgerEntry[]): string { + if (!Array.isArray(events) || events.length === 0) return "no event ledger entries"; + const header = [ + "seq".padStart(4), + "type".padEnd(20), + "repo".padEnd(24), + "created-at".padEnd(24), + ].join(" "); + const lines = events.map((entry) => + [ + String(entry.seq).padStart(4), + entry.type.padEnd(20), + display(entry.repoFullName).padEnd(24), + display(entry.createdAt).padEnd(24), + ].join(" "), + ); + return [header, ...lines].join("\n"); +} + +const EVENT_LEDGER_METRICS_USAGE = "Usage: loopover-miner ledger metrics"; + +// Prometheus metric name for the per-type event-ledger counter. Mirrors the `loopover_miner_*_total` naming and +// the HELP/TYPE/label conventions of the engine's renderMinerPredictionMetrics +// (packages/loopover-engine/src/miner-prediction-metrics.ts) rather than importing across the package boundary. +const MINER_EVENTS_TOTAL = "loopover_miner_events_total"; + +/** HELP-text escaping — backslash + newline (mirrors miner-prediction-metrics.ts's escapeHelpText). */ +function escapeHelpText(help: string): string { + return help.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"); +} + +/** Prometheus label-value escaping — backslash, double-quote, newline — so an arbitrary event `type` string can + * never break the metric line (mirrors miner-prediction-metrics.ts's escapeLabelValue). */ +function escapeLabelValue(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); +} + +/** + * Render event-ledger activity as Prometheus text-exposition counters: one `loopover_miner_events_total{type}` + * series per event type, so a self-hoster's own Grafana/alerting can scrape ledger activity instead of polling + * `ledger list --json` (#4841). Pure + side-effect-free — the caller supplies the rows and prints the result; + * deterministic (series emitted in sorted type order); always emits HELP/TYPE so an empty ledger is still a + * well-formed exposition document. + */ +export function renderEventLedgerMetrics(events: readonly LedgerEntry[]): string { + const totalByType = new Map(); + for (const entry of events) { + totalByType.set(entry.type, (totalByType.get(entry.type) ?? 0) + 1); + } + const lines = [ + `# HELP ${MINER_EVENTS_TOTAL} ${escapeHelpText("Event-ledger entries the miner has recorded, by event type.")}`, + `# TYPE ${MINER_EVENTS_TOTAL} counter`, + ]; + for (const [type, count] of [...totalByType.entries()].sort((a, b) => a[0].localeCompare(b[0]))) { + lines.push(`${MINER_EVENTS_TOTAL}{type="${escapeLabelValue(type)}"} ${count}`); + } + return `${lines.join("\n")}\n`; +} + +function withEventLedger(options: { initEventLedger?: () => EventLedger }, run: (eventLedger: EventLedger) => T): T { + const ownsLedger = options.initEventLedger === undefined; + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + try { + return run(eventLedger); + } finally { + if (ownsLedger) eventLedger.close(); + } +} + +export function runLedgerList(args: string[], options: { initEventLedger?: () => EventLedger } = {}): number { + const parsed = parseLedgerListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + return withEventLedger(options, (eventLedger) => { + const events = filterLedgerEvents( + eventLedger.readEvents({ + repoFullName: parsed.repoFullName, + since: parsed.since, + }), + { type: parsed.type }, + ); + if (parsed.json) { + console.log(JSON.stringify({ events }, null, 2)); + } else { + console.log(renderLedgerTable(events)); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runLedgerMetrics(args: string[], options: { initEventLedger?: () => EventLedger } = {}): number { + if (args.length > 0) { + return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE); + } + + try { + return withEventLedger(options, (eventLedger) => { + // renderEventLedgerMetrics returns a newline-terminated document; console.log re-adds the terminator, so + // trim it to emit exactly one trailing newline (mirrors metrics-cli.js's runMetrics). + console.log(renderEventLedgerMetrics(eventLedger.readEvents()).trimEnd()); + return 0; + }); + } catch (error) { + return reportCliFailure(argsWantJson(args), describeCliError(error)); + } +} + +export function runLedgerCli(subcommand: string | undefined, args: string[], options: { initEventLedger?: () => EventLedger } = {}): number { + if (subcommand === "list") return runLedgerList(args, options); + if (subcommand === "metrics") return runLedgerMetrics(args, options); + return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`); +} diff --git a/packages/loopover-miner/lib/governor-ledger-cli.d.ts b/packages/loopover-miner/lib/governor-ledger-cli.d.ts index ed1e2e1b8d..7b230173d3 100644 --- a/packages/loopover-miner/lib/governor-ledger-cli.d.ts +++ b/packages/loopover-miner/lib/governor-ledger-cli.d.ts @@ -1,32 +1,22 @@ -import type { GovernorLedger, GovernorLedgerEntry } from "./governor-ledger.js"; import type { GovernorPauseCliOptions } from "./governor-pause-cli.js"; - +import type { GovernorLedger, GovernorLedgerEntry } from "./governor-ledger.js"; export type GovernorLedgerEventType = "allowed" | "denied" | "throttled" | "kill_switch"; - -export type ParsedGovernorListArgs = - | { - json: boolean; - repoFullName: string | null; - type: GovernorLedgerEventType | null; - } - | { error: string }; - -export function parseGovernorListArgs(args: string[]): ParsedGovernorListArgs; - -export function filterGovernorEvents( - events: GovernorLedgerEntry[], - options?: { type?: string | null }, -): GovernorLedgerEntry[]; - -export function renderGovernorTable(events: GovernorLedgerEntry[]): string; - -export function runGovernorList( - args: string[], - options?: { initGovernorLedger?: () => GovernorLedger }, -): Promise; - -export function runGovernorCli( - subcommand: string | undefined, - args: string[], - options?: { initGovernorLedger?: () => GovernorLedger; nowMs?: number } & GovernorPauseCliOptions, -): Promise; +export type ParsedGovernorListArgs = { + json: boolean; + repoFullName: string | null; + type: GovernorLedgerEventType | null; +} | { + error: string; +}; +export declare function parseGovernorListArgs(args: string[]): ParsedGovernorListArgs; +export declare function filterGovernorEvents(events: GovernorLedgerEntry[], options?: { + type?: string | null; +}): GovernorLedgerEntry[]; +export declare function renderGovernorTable(events: GovernorLedgerEntry[]): string; +export declare function runGovernorList(args: string[], options?: { + initGovernorLedger?: () => GovernorLedger; +}): Promise; +export declare function runGovernorCli(subcommand: string | undefined, args: string[], options?: { + initGovernorLedger?: () => GovernorLedger; + nowMs?: number; +} & GovernorPauseCliOptions): Promise; diff --git a/packages/loopover-miner/lib/governor-ledger-cli.js b/packages/loopover-miner/lib/governor-ledger-cli.js index 0f3d3acae3..d18df5b54e 100644 --- a/packages/loopover-miner/lib/governor-ledger-cli.js +++ b/packages/loopover-miner/lib/governor-ledger-cli.js @@ -1,158 +1,157 @@ import { runGovernorPause, runGovernorResume, runGovernorStatus } from "./governor-pause-cli.js"; import { runGovernorMetrics } from "./governor-metrics-cli.js"; - /** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@loopover/engine`. */ import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; - const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([ - "allowed", - "denied", - "throttled", - "kill_switch", + "allowed", + "denied", + "throttled", + "kill_switch", ]); - -const GOVERNOR_LIST_USAGE = - "Usage: loopover-miner governor list [--repo ] [--type allowed|denied|throttled|kill_switch] [--json]"; - +const GOVERNOR_LIST_USAGE = "Usage: loopover-miner governor list [--repo ] [--type allowed|denied|throttled|kill_switch] [--json]"; const GOVERNOR_SUBCOMMAND_USAGE = [ - GOVERNOR_LIST_USAGE, - " loopover-miner governor pause [--reason ] [--dry-run] [--json]", - " loopover-miner governor resume [--dry-run] [--json]", - " loopover-miner governor status [--json]", - " loopover-miner governor metrics", + GOVERNOR_LIST_USAGE, + " loopover-miner governor pause [--reason ] [--dry-run] [--json]", + " loopover-miner governor resume [--dry-run] [--json]", + " loopover-miner governor status [--json]", + " loopover-miner governor metrics", ].join("\n"); - function parseRepoArg(value, usage) { - if (!value) return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; + if (!value) + return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; } - export function parseGovernorListArgs(args) { - const options = { json: false, repoFullName: null, type: null }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--repo") { - const repoArg = args[index + 1]; - if (!repoArg || repoArg.startsWith("-")) return { error: GOVERNOR_LIST_USAGE }; - const repo = parseRepoArg(repoArg, GOVERNOR_LIST_USAGE); - if ("error" in repo) return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; + const options = { + json: false, + repoFullName: null, + type: null, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) + return { error: GOVERNOR_LIST_USAGE }; + const repo = parseRepoArg(repoArg, GOVERNOR_LIST_USAGE); + if ("error" in repo) + return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--type") { + const type = args[index + 1]; + if (!type || type.startsWith("-")) + return { error: GOVERNOR_LIST_USAGE }; + const trimmed = type.trim(); + if (!GOVERNOR_LEDGER_EVENT_TYPES.includes(trimmed)) { + return { + error: `Invalid type: ${trimmed}. Expected one of ${GOVERNOR_LEDGER_EVENT_TYPES.join(", ")}.`, + }; + } + options.type = trimmed; + index += 1; + continue; + } + if (token.startsWith("-")) + return { error: `Unknown option: ${token}` }; + positional.push(token); } - if (token === "--type") { - const type = args[index + 1]; - if (!type || type.startsWith("-")) return { error: GOVERNOR_LIST_USAGE }; - const trimmed = type.trim(); - if (!GOVERNOR_LEDGER_EVENT_TYPES.includes(trimmed)) { - return { - error: `Invalid type: ${trimmed}. Expected one of ${GOVERNOR_LEDGER_EVENT_TYPES.join(", ")}.`, - }; - } - options.type = trimmed; - index += 1; - continue; - } - if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; - positional.push(token); - } - - if (positional.length > 0) return { error: GOVERNOR_LIST_USAGE }; - return options; + if (positional.length > 0) + return { error: GOVERNOR_LIST_USAGE }; + return options; } - export function filterGovernorEvents(events, options = {}) { - if (!Array.isArray(events)) return []; - const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; - if (!type) return events; - return events.filter((entry) => entry.eventType === type); + if (!Array.isArray(events)) + return []; + const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; + if (!type) + return events; + return events.filter((entry) => entry.eventType === type); } - function display(value) { - if (value === null || value === undefined) return "-"; - return String(value); + if (value === null || value === undefined) + return "-"; + return String(value); } - export function renderGovernorTable(events) { - if (!Array.isArray(events) || events.length === 0) return "no governor ledger entries"; - const header = [ - "id".padStart(4), - "type".padEnd(12), - "repo".padEnd(24), - "action".padEnd(10), - "decision".padEnd(10), - "ts".padEnd(24), - ].join(" "); - const lines = events.map((entry) => - [ - String(entry.id).padStart(4), - entry.eventType.padEnd(12), - display(entry.repoFullName).padEnd(24), - entry.actionClass.padEnd(10), - entry.decision.padEnd(10), - display(entry.ts).padEnd(24), - ].join(" "), - ); - return [header, ...lines].join("\n"); + if (!Array.isArray(events) || events.length === 0) + return "no governor ledger entries"; + const header = [ + "id".padStart(4), + "type".padEnd(12), + "repo".padEnd(24), + "action".padEnd(10), + "decision".padEnd(10), + "ts".padEnd(24), + ].join(" "); + const lines = events.map((entry) => [ + String(entry.id).padStart(4), + entry.eventType.padEnd(12), + display(entry.repoFullName).padEnd(24), + entry.actionClass.padEnd(10), + entry.decision.padEnd(10), + display(entry.ts).padEnd(24), + ].join(" ")); + return [header, ...lines].join("\n"); } - async function withGovernorLedger(options, run) { - const ownsLedger = options.initGovernorLedger === undefined; - const initGovernorLedger = - options.initGovernorLedger ?? (await import("./governor-ledger.js")).initGovernorLedger; - const governorLedger = initGovernorLedger(); - try { - return run(governorLedger); - } finally { - if (ownsLedger) governorLedger.close(); - } + const ownsLedger = options.initGovernorLedger === undefined; + const initGovernorLedger = options.initGovernorLedger ?? (await import("./governor-ledger.js")).initGovernorLedger; + const governorLedger = initGovernorLedger(); + try { + return run(governorLedger); + } + finally { + if (ownsLedger) + governorLedger.close(); + } } - export async function runGovernorList(args, options = {}) { - const parsed = parseGovernorListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - return await withGovernorLedger(options, (governorLedger) => { - const events = filterGovernorEvents( - governorLedger.readGovernorEvents({ - repoFullName: parsed.repoFullName, - }), - { type: parsed.type }, - ); - if (parsed.json) { - console.log(JSON.stringify({ events }, null, 2)); - } else { - console.log(renderGovernorTable(events)); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } + const parsed = parseGovernorListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + return await withGovernorLedger(options, (governorLedger) => { + const events = filterGovernorEvents(governorLedger.readGovernorEvents({ + repoFullName: parsed.repoFullName, + }), { type: parsed.type }); + if (parsed.json) { + console.log(JSON.stringify({ events }, null, 2)); + } + else { + console.log(renderGovernorTable(events)); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } } - export async function runGovernorCli(subcommand, args, options = {}) { - if (subcommand === "list") return runGovernorList(args, options); - if (subcommand === "pause") return runGovernorPause(args, options); - if (subcommand === "resume") return runGovernorResume(args, options); - if (subcommand === "status") return runGovernorStatus(args, options); - if (subcommand === "metrics") return runGovernorMetrics(args, options); - return reportCliFailure( - argsWantJson(args), - `Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`, - ); + if (subcommand === "list") + return runGovernorList(args, options); + if (subcommand === "pause") + return runGovernorPause(args, options); + if (subcommand === "resume") + return runGovernorResume(args, options); + if (subcommand === "status") + return runGovernorStatus(args, options); + if (subcommand === "metrics") + return runGovernorMetrics(args, options); + return reportCliFailure(argsWantJson(args), `Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`); } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ292ZXJub3ItbGVkZ2VyLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImdvdmVybm9yLWxlZGdlci1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFFakcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFHL0Qsc0VBQXNFO0FBQ3RFLE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUlsRixNQUFNLDJCQUEyQixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDaEQsU0FBUztJQUNULFFBQVE7SUFDUixXQUFXO0lBQ1gsYUFBYTtDQUNMLENBQUMsQ0FBQztBQUVaLE1BQU0sbUJBQW1CLEdBQ3ZCLGtIQUFrSCxDQUFDO0FBRXJILE1BQU0seUJBQXlCLEdBQUc7SUFDaEMsbUJBQW1CO0lBQ25CLDZFQUE2RTtJQUM3RSw0REFBNEQ7SUFDNUQsZ0RBQWdEO0lBQ2hELHdDQUF3QztDQUN6QyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztBQVliLFNBQVMsWUFBWSxDQUFDLEtBQXlCLEVBQUUsS0FBYTtJQUM1RCxJQUFJLENBQUMsS0FBSztRQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDcEMsTUFBTSxPQUFPLEdBQUcsS0FBSyxDQUFDLElBQUksRUFBRSxDQUFDO0lBQzdCLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDaEQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFBSSxLQUFLLEtBQUssU0FBUyxFQUFFLENBQUM7UUFDM0MsT0FBTyxFQUFFLEtBQUssRUFBRSx3Q0FBd0Ms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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/governor-ledger-cli.ts b/packages/loopover-miner/lib/governor-ledger-cli.ts new file mode 100644 index 0000000000..4b7c53d63d --- /dev/null +++ b/packages/loopover-miner/lib/governor-ledger-cli.ts @@ -0,0 +1,189 @@ +import { runGovernorPause, runGovernorResume, runGovernorStatus } from "./governor-pause-cli.js"; +import type { GovernorPauseCliOptions } from "./governor-pause-cli.js"; +import { runGovernorMetrics } from "./governor-metrics-cli.js"; +import type { GovernorLedger, GovernorLedgerEntry } from "./governor-ledger.js"; + +/** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@loopover/engine`. */ +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; + +export type GovernorLedgerEventType = "allowed" | "denied" | "throttled" | "kill_switch"; + +const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([ + "allowed", + "denied", + "throttled", + "kill_switch", +] as const); + +const GOVERNOR_LIST_USAGE = + "Usage: loopover-miner governor list [--repo ] [--type allowed|denied|throttled|kill_switch] [--json]"; + +const GOVERNOR_SUBCOMMAND_USAGE = [ + GOVERNOR_LIST_USAGE, + " loopover-miner governor pause [--reason ] [--dry-run] [--json]", + " loopover-miner governor resume [--dry-run] [--json]", + " loopover-miner governor status [--json]", + " loopover-miner governor metrics", +].join("\n"); + +export type ParsedGovernorListArgs = + | { + json: boolean; + repoFullName: string | null; + type: GovernorLedgerEventType | null; + } + | { error: string }; + +type ParsedRepoArg = { repoFullName: string } | { error: string }; + +function parseRepoArg(value: string | undefined, usage: string): ParsedRepoArg { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +export function parseGovernorListArgs(args: string[]): ParsedGovernorListArgs { + const options: { json: boolean; repoFullName: string | null; type: GovernorLedgerEventType | null } = { + json: false, + repoFullName: null, + type: null, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + if (!repoArg || repoArg.startsWith("-")) return { error: GOVERNOR_LIST_USAGE }; + const repo = parseRepoArg(repoArg, GOVERNOR_LIST_USAGE); + if ("error" in repo) return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + if (token === "--type") { + const type = args[index + 1]; + if (!type || type.startsWith("-")) return { error: GOVERNOR_LIST_USAGE }; + const trimmed = type.trim(); + if (!(GOVERNOR_LEDGER_EVENT_TYPES as readonly string[]).includes(trimmed)) { + return { + error: `Invalid type: ${trimmed}. Expected one of ${GOVERNOR_LEDGER_EVENT_TYPES.join(", ")}.`, + }; + } + options.type = trimmed as GovernorLedgerEventType; + index += 1; + continue; + } + if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; + positional.push(token); + } + + if (positional.length > 0) return { error: GOVERNOR_LIST_USAGE }; + return options; +} + +export function filterGovernorEvents( + events: GovernorLedgerEntry[], + options: { type?: string | null } = {}, +): GovernorLedgerEntry[] { + if (!Array.isArray(events)) return []; + const type = typeof options.type === "string" && options.type.trim() ? options.type.trim() : null; + if (!type) return events; + return events.filter((entry) => entry.eventType === type); +} + +function display(value: unknown): string { + if (value === null || value === undefined) return "-"; + return String(value); +} + +export function renderGovernorTable(events: GovernorLedgerEntry[]): string { + if (!Array.isArray(events) || events.length === 0) return "no governor ledger entries"; + const header = [ + "id".padStart(4), + "type".padEnd(12), + "repo".padEnd(24), + "action".padEnd(10), + "decision".padEnd(10), + "ts".padEnd(24), + ].join(" "); + const lines = events.map((entry) => + [ + String(entry.id).padStart(4), + entry.eventType.padEnd(12), + display(entry.repoFullName).padEnd(24), + entry.actionClass.padEnd(10), + entry.decision.padEnd(10), + display(entry.ts).padEnd(24), + ].join(" "), + ); + return [header, ...lines].join("\n"); +} + +async function withGovernorLedger( + options: { initGovernorLedger?: () => GovernorLedger }, + run: (governorLedger: GovernorLedger) => T, +): Promise { + const ownsLedger = options.initGovernorLedger === undefined; + const initGovernorLedger = + options.initGovernorLedger ?? (await import("./governor-ledger.js")).initGovernorLedger; + const governorLedger = initGovernorLedger(); + try { + return run(governorLedger); + } finally { + if (ownsLedger) governorLedger.close(); + } +} + +export async function runGovernorList( + args: string[], + options: { initGovernorLedger?: () => GovernorLedger } = {}, +): Promise { + const parsed = parseGovernorListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + return await withGovernorLedger(options, (governorLedger) => { + const events = filterGovernorEvents( + governorLedger.readGovernorEvents({ + repoFullName: parsed.repoFullName, + }), + { type: parsed.type }, + ); + if (parsed.json) { + console.log(JSON.stringify({ events }, null, 2)); + } else { + console.log(renderGovernorTable(events)); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export async function runGovernorCli( + subcommand: string | undefined, + args: string[], + options: { initGovernorLedger?: () => GovernorLedger; nowMs?: number } & GovernorPauseCliOptions = {}, +): Promise { + if (subcommand === "list") return runGovernorList(args, options); + if (subcommand === "pause") return runGovernorPause(args, options); + if (subcommand === "resume") return runGovernorResume(args, options); + if (subcommand === "status") return runGovernorStatus(args, options); + if (subcommand === "metrics") return runGovernorMetrics(args, options); + return reportCliFailure( + argsWantJson(args), + `Unknown governor subcommand: ${subcommand ?? ""}.\n${GOVERNOR_SUBCOMMAND_USAGE}`, + ); +} diff --git a/packages/loopover-miner/lib/plan-store-cli.d.ts b/packages/loopover-miner/lib/plan-store-cli.d.ts index 794818190e..c0d5051157 100644 --- a/packages/loopover-miner/lib/plan-store-cli.d.ts +++ b/packages/loopover-miner/lib/plan-store-cli.d.ts @@ -1,37 +1,23 @@ import type { PlanRecord, PlanStatus, PlanStore } from "./plan-store.js"; - -export type ParsedPlanListArgs = - | { - json: boolean; - status: PlanStatus | null; - } - | { error: string }; - -export type ParsedPlanShowArgs = - | { - planId: string; - json: boolean; - } - | { error: string }; - -export function parsePlanListArgs(args: string[]): ParsedPlanListArgs; - -export function parsePlanShowArgs(args: string[]): ParsedPlanShowArgs; - -export function renderPlanTable(plans: PlanRecord[]): string; - -export function runPlanList( - args: string[], - options?: { openPlanStore?: () => PlanStore }, -): number; - -export function runPlanShow( - args: string[], - options?: { openPlanStore?: () => PlanStore }, -): number; - -export function runPlanCli( - subcommand: string | undefined, - args: string[], - options?: { openPlanStore?: () => PlanStore }, -): number; +export type ParsedPlanListArgs = { + json: boolean; + status: PlanStatus | null; +} | { + error: string; +}; +export type ParsedPlanShowArgs = { + planId: string; + json: boolean; +} | { + error: string; +}; +export declare function parsePlanListArgs(args: string[]): ParsedPlanListArgs; +export declare function parsePlanShowArgs(args: string[]): ParsedPlanShowArgs; +export declare function renderPlanTable(plans: PlanRecord[]): string; +type PlanCliOptions = { + openPlanStore?: () => PlanStore; +}; +export declare function runPlanList(args: string[], options?: PlanCliOptions): number; +export declare function runPlanShow(args: string[], options?: PlanCliOptions): number; +export declare function runPlanCli(subcommand: string | undefined, args: string[], options?: PlanCliOptions): number; +export {}; diff --git a/packages/loopover-miner/lib/plan-store-cli.js b/packages/loopover-miner/lib/plan-store-cli.js index 11134574be..2819ed0f39 100644 --- a/packages/loopover-miner/lib/plan-store-cli.js +++ b/packages/loopover-miner/lib/plan-store-cli.js @@ -1,151 +1,147 @@ import { PLAN_STATUSES, openPlanStore } from "./plan-store.js"; import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; - -const PLAN_LIST_USAGE = - "Usage: loopover-miner plan list [--status pending|running|completed|failed] [--json]"; +const PLAN_LIST_USAGE = "Usage: loopover-miner plan list [--status pending|running|completed|failed] [--json]"; const PLAN_SHOW_USAGE = "Usage: loopover-miner plan show [--json]"; - function parseJsonFlag(args) { - const options = { json: false }; - const positional = []; - - for (const token of args) { - if (token === "--json") { - options.json = true; - continue; + const options = { json: false }; + const positional = []; + for (const token of args) { + if (token === "--json") { + options.json = true; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; - } - positional.push(token); - } - - return { positional, ...options }; + return { positional, ...options }; } - export function parsePlanListArgs(args) { - const options = { json: false, status: null }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--status") { - const status = args[index + 1]; - if (!status || status.startsWith("-")) return { error: PLAN_LIST_USAGE }; - if (!PLAN_STATUSES.includes(status)) { - return { error: `Invalid status: ${status}. Expected one of ${PLAN_STATUSES.join(", ")}.` }; - } - options.status = status; - index += 1; - continue; + const options = { json: false, status: null }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--status") { + const status = args[index + 1]; + if (!status || status.startsWith("-")) + return { error: PLAN_LIST_USAGE }; + if (!PLAN_STATUSES.includes(status)) { + return { error: `Invalid status: ${status}. Expected one of ${PLAN_STATUSES.join(", ")}.` }; + } + options.status = status; + index += 1; + continue; + } + if (token.startsWith("-")) + return { error: `Unknown option: ${token}` }; + positional.push(token); } - if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; - positional.push(token); - } - - if (positional.length > 0) return { error: PLAN_LIST_USAGE }; - return options; + if (positional.length > 0) + return { error: PLAN_LIST_USAGE }; + return options; } - export function parsePlanShowArgs(args) { - const parsed = parseJsonFlag(args); - if ("error" in parsed) return parsed; - if (parsed.positional.length !== 1) return { error: PLAN_SHOW_USAGE }; - - const planId = parsed.positional[0]?.trim(); - if (!planId) return { error: PLAN_SHOW_USAGE }; - - return { - planId, - json: parsed.json, - }; + const parsed = parseJsonFlag(args); + if ("error" in parsed) + return parsed; + if (parsed.positional.length !== 1) + return { error: PLAN_SHOW_USAGE }; + const planId = parsed.positional[0]?.trim(); + if (!planId) + return { error: PLAN_SHOW_USAGE }; + return { + planId, + json: parsed.json, + }; } - function display(value) { - if (value === null || value === undefined) return "-"; - return String(value); + if (value === null || value === undefined) + return "-"; + return String(value); } - export function renderPlanTable(plans) { - if (!Array.isArray(plans) || plans.length === 0) return "no saved plans"; - const header = [ - "plan-id".padEnd(20), - "status".padEnd(10), - "steps".padStart(5), - "updated-at".padEnd(24), - ].join(" "); - const lines = plans.map((record) => - [ - record.planId.padEnd(20), - record.status.padEnd(10), - String(record.plan.steps.length).padStart(5), - display(record.updatedAt).padEnd(24), - ].join(" "), - ); - return [header, ...lines].join("\n"); + if (!Array.isArray(plans) || plans.length === 0) + return "no saved plans"; + const header = [ + "plan-id".padEnd(20), + "status".padEnd(10), + "steps".padStart(5), + "updated-at".padEnd(24), + ].join(" "); + const lines = plans.map((record) => [ + record.planId.padEnd(20), + record.status.padEnd(10), + String(record.plan.steps.length).padStart(5), + display(record.updatedAt).padEnd(24), + ].join(" ")); + return [header, ...lines].join("\n"); } - function withPlanStore(options, run) { - const ownsStore = options.openPlanStore === undefined; - const planStore = (options.openPlanStore ?? openPlanStore)(); - try { - return run(planStore); - } finally { - if (ownsStore) planStore.close(); - } + const ownsStore = options.openPlanStore === undefined; + const planStore = (options.openPlanStore ?? openPlanStore)(); + try { + return run(planStore); + } + finally { + if (ownsStore) + planStore.close(); + } } - export function runPlanList(args, options = {}) { - const parsed = parsePlanListArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - return withPlanStore(options, (planStore) => { - const plans = planStore.listPlans({ status: parsed.status }); - if (parsed.json) { - console.log(JSON.stringify({ plans }, null, 2)); - } else { - console.log(renderPlanTable(plans)); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } + const parsed = parsePlanListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + return withPlanStore(options, (planStore) => { + const plans = planStore.listPlans({ status: parsed.status }); + if (parsed.json) { + console.log(JSON.stringify({ plans }, null, 2)); + } + else { + console.log(renderPlanTable(plans)); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } } - export function runPlanShow(args, options = {}) { - const parsed = parsePlanShowArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - return withPlanStore(options, (planStore) => { - const plan = planStore.loadPlan(parsed.planId); - if (!plan) { - return reportCliFailure(parsed.json, "plan_not_found"); - } - if (parsed.json) { - console.log(JSON.stringify({ plan }, null, 2)); - } else { - console.log(`${plan.status} (${plan.plan.steps.length} steps)`); - } - return 0; - }); - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } + const parsed = parsePlanShowArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + return withPlanStore(options, (planStore) => { + const plan = planStore.loadPlan(parsed.planId); + if (!plan) { + return reportCliFailure(parsed.json, "plan_not_found"); + } + if (parsed.json) { + console.log(JSON.stringify({ plan }, null, 2)); + } + else { + console.log(`${plan.status} (${plan.plan.steps.length} steps)`); + } + return 0; + }); + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } } - export function runPlanCli(subcommand, args, options = {}) { - if (subcommand === "list") return runPlanList(args, options); - if (subcommand === "show") return runPlanShow(args, options); - return reportCliFailure(argsWantJson(args), `Unknown plan subcommand: ${subcommand ?? ""}. ${PLAN_LIST_USAGE}`); + if (subcommand === "list") + return runPlanList(args, options); + if (subcommand === "show") + return runPlanShow(args, options); + return reportCliFailure(argsWantJson(args), `Unknown plan subcommand: ${subcommand ?? ""}. ${PLAN_LIST_USAGE}`); } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGxhbi1zdG9yZS1jbGkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJwbGFuLXN0b3JlLWNsaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsYUFBYSxFQUFFLGFBQWEsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRS9ELE9BQU8sRUFBRSxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUVsRixNQUFNLGVBQWUsR0FDbkIsc0ZBQXNGLENBQUM7QUFDekYsTUFBTSxlQUFlLEdBQUcsbURBQW1ELENBQUM7QUFrQjVFLFNBQVMsYUFBYSxDQUFDLElBQWM7SUFDbkMsTUFBTSxPQUFPLEdBQUcsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLENBQUM7SUFDaEMsTUFBTSxVQUFVLEdBQWEsRUFBRSxDQUFDO0lBRWhDLEtBQUssTUFBTSxLQUFLLElBQUksSUFBSSxFQUFFLENBQUM7UUFDekIsSUFBSSxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7WUFDdkIsT0FBTyxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7WUFDcEIsU0FBUztRQUNYLENBQUM7UUFDRCxJQUFJLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQztZQUMxQixPQUFPLEVBQUUsS0FBSyxFQUFFLG1CQUFtQixLQUFLLEVBQUUsRUFBRSxDQUFDO1FBQy9DLENBQUM7UUFDRCxVQUFVLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3pCLENBQUM7SUFFRCxPQUFPLEVBQUUsVUFBVSxFQUFFLEdBQUcsT0FBTyxFQUFFLENBQUM7QUFDcEMsQ0FBQztBQUVELE1BQU0sVUFBVSxpQkFBaUIsQ0FBQyxJQUFjO0lBQzlDLE1BQU0sT0FBTyxHQUFpRCxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxDQUFDO0lBQzVGLE1BQU0sVUFBVSxHQUFhLEVBQUUsQ0FBQztJQUVo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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/plan-store-cli.ts b/packages/loopover-miner/lib/plan-store-cli.ts new file mode 100644 index 0000000000..2d25593fb0 --- /dev/null +++ b/packages/loopover-miner/lib/plan-store-cli.ts @@ -0,0 +1,174 @@ +import { PLAN_STATUSES, openPlanStore } from "./plan-store.js"; +import type { PlanRecord, PlanStatus, PlanStore } from "./plan-store.js"; +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; + +const PLAN_LIST_USAGE = + "Usage: loopover-miner plan list [--status pending|running|completed|failed] [--json]"; +const PLAN_SHOW_USAGE = "Usage: loopover-miner plan show [--json]"; + +export type ParsedPlanListArgs = + | { + json: boolean; + status: PlanStatus | null; + } + | { error: string }; + +export type ParsedPlanShowArgs = + | { + planId: string; + json: boolean; + } + | { error: string }; + +type ParsedJsonFlag = { positional: string[]; json: boolean } | { error: string }; + +function parseJsonFlag(args: string[]): ParsedJsonFlag { + const options = { json: false }; + const positional: string[] = []; + + for (const token of args) { + if (token === "--json") { + options.json = true; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + return { positional, ...options }; +} + +export function parsePlanListArgs(args: string[]): ParsedPlanListArgs { + const options: { json: boolean; status: PlanStatus | null } = { json: false, status: null }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--status") { + const status = args[index + 1]; + if (!status || status.startsWith("-")) return { error: PLAN_LIST_USAGE }; + if (!(PLAN_STATUSES as readonly string[]).includes(status)) { + return { error: `Invalid status: ${status}. Expected one of ${PLAN_STATUSES.join(", ")}.` }; + } + options.status = status as PlanStatus; + index += 1; + continue; + } + if (token.startsWith("-")) return { error: `Unknown option: ${token}` }; + positional.push(token); + } + + if (positional.length > 0) return { error: PLAN_LIST_USAGE }; + return options; +} + +export function parsePlanShowArgs(args: string[]): ParsedPlanShowArgs { + const parsed = parseJsonFlag(args); + if ("error" in parsed) return parsed; + if (parsed.positional.length !== 1) return { error: PLAN_SHOW_USAGE }; + + const planId = parsed.positional[0]?.trim(); + if (!planId) return { error: PLAN_SHOW_USAGE }; + + return { + planId, + json: parsed.json, + }; +} + +function display(value: unknown): string { + if (value === null || value === undefined) return "-"; + return String(value); +} + +export function renderPlanTable(plans: PlanRecord[]): string { + if (!Array.isArray(plans) || plans.length === 0) return "no saved plans"; + const header = [ + "plan-id".padEnd(20), + "status".padEnd(10), + "steps".padStart(5), + "updated-at".padEnd(24), + ].join(" "); + const lines = plans.map((record) => + [ + record.planId.padEnd(20), + record.status.padEnd(10), + String(record.plan.steps.length).padStart(5), + display(record.updatedAt).padEnd(24), + ].join(" "), + ); + return [header, ...lines].join("\n"); +} + +type PlanCliOptions = { openPlanStore?: () => PlanStore }; + +function withPlanStore(options: PlanCliOptions, run: (planStore: PlanStore) => T): T { + const ownsStore = options.openPlanStore === undefined; + const planStore = (options.openPlanStore ?? openPlanStore)(); + try { + return run(planStore); + } finally { + if (ownsStore) planStore.close(); + } +} + +export function runPlanList(args: string[], options: PlanCliOptions = {}): number { + const parsed = parsePlanListArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + return withPlanStore(options, (planStore) => { + const plans = planStore.listPlans({ status: parsed.status }); + if (parsed.json) { + console.log(JSON.stringify({ plans }, null, 2)); + } else { + console.log(renderPlanTable(plans)); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runPlanShow(args: string[], options: PlanCliOptions = {}): number { + const parsed = parsePlanShowArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + return withPlanStore(options, (planStore) => { + const plan = planStore.loadPlan(parsed.planId); + if (!plan) { + return reportCliFailure(parsed.json, "plan_not_found"); + } + if (parsed.json) { + console.log(JSON.stringify({ plan }, null, 2)); + } else { + console.log(`${plan.status} (${plan.plan.steps.length} steps)`); + } + return 0; + }); + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runPlanCli( + subcommand: string | undefined, + args: string[], + options: PlanCliOptions = {}, +): number { + if (subcommand === "list") return runPlanList(args, options); + if (subcommand === "show") return runPlanShow(args, options); + return reportCliFailure(argsWantJson(args), `Unknown plan subcommand: ${subcommand ?? ""}. ${PLAN_LIST_USAGE}`); +} diff --git a/packages/loopover-miner/lib/purge-cli.d.ts b/packages/loopover-miner/lib/purge-cli.d.ts index 0f84a01e6d..989ec1c07d 100644 --- a/packages/loopover-miner/lib/purge-cli.d.ts +++ b/packages/loopover-miner/lib/purge-cli.d.ts @@ -1,48 +1,42 @@ -import type { ClaimLedger } from "./claim-ledger.js"; -import type { EventLedger } from "./event-ledger.js"; -import type { GovernorLedger } from "./governor-ledger.js"; -import type { PredictionLedger } from "./prediction-ledger.js"; -import type { PortfolioQueueStore } from "./portfolio-queue.js"; -import type { RunStateStore } from "./run-state.js"; - -export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE: string; - -export type ParsedPurgeArgs = { json: boolean; dryRun: boolean; repoFullName: string } | { error: string }; - -export function parsePurgeArgs(args: string[]): ParsedPurgeArgs; - -export type PurgeStoreResult = { store: string; purged: number | null; error?: string; note?: string }; -export type PurgeDryRunStoreResult = { store: string; wouldPurge: number | null; error?: string }; - +export declare const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; +export type ParsedPurgeArgs = { + json: boolean; + dryRun: boolean; + repoFullName: string; +} | { + error: string; +}; +export declare function parsePurgeArgs(args: string[]): ParsedPurgeArgs; +export type PurgeStoreResult = { + store: string; + purged: number | null; + error?: string; + note?: string; +}; +export type PurgeDryRunStoreResult = { + store: string; + wouldPurge: number | null; + error?: string; +}; export type PurgeDryRunResult = { - outcome: "dry_run"; - repoFullName: string; - stores: PurgeDryRunStoreResult[]; - attemptLogNote: string; - attemptLogTotalRows: number; + outcome: "dry_run"; + repoFullName: string; + stores: PurgeDryRunStoreResult[]; + attemptLogNote: string; + attemptLogTotalRows: number; }; - export type PurgeSummary = { - outcome: "purged" | "partial"; - repoFullName: string; - totalPurged: number; - stores: PurgeStoreResult[]; - purgedAt: string; + outcome: "purged" | "partial"; + repoFullName: string; + totalPurged: number; + stores: PurgeStoreResult[]; + purgedAt: string; }; - export type PurgeCliOptions = { - openClaimLedger?: () => ClaimLedger; - initEventLedger?: () => EventLedger; - initGovernorLedger?: () => GovernorLedger; - initPredictionLedger?: () => PredictionLedger; - initPortfolioQueueStore?: () => PortfolioQueueStore; - initRunStateStore?: () => RunStateStore; - resolveDbPaths?: Record string>; -}; - -export function runPurgeDryRun( - parsed: { repoFullName: string; json: boolean }, - options?: PurgeCliOptions, -): number; - -export function runPurge(args: string[], options?: PurgeCliOptions): number; + resolveDbPaths?: Record string>; +} & Record; +export declare function runPurgeDryRun(parsed: { + repoFullName: string; + json: boolean; +}, options?: PurgeCliOptions): number; +export declare function runPurge(args: string[], options?: PurgeCliOptions): number; diff --git a/packages/loopover-miner/lib/purge-cli.js b/packages/loopover-miner/lib/purge-cli.js index 0160723172..c5dee32c2f 100644 --- a/packages/loopover-miner/lib/purge-cli.js +++ b/packages/loopover-miner/lib/purge-cli.js @@ -22,201 +22,179 @@ import { initContributionProfileCache, resolveContributionProfileCacheDbPath } f import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; import { resolveAttemptLogDbPath } from "./attempt-log.js"; -import { - CLAIM_LEDGER_PURGE_SPEC, - EVENT_LEDGER_PURGE_SPEC, - GOVERNOR_LEDGER_PURGE_SPEC, - PREDICTION_LEDGER_PURGE_SPEC, - PORTFOLIO_QUEUE_PURGE_SPEC, - RUN_STATE_PURGE_SPEC, - CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, - GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, - GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, - POLICY_VERDICT_CACHE_PURGE_SPEC, - countStoreByRepo, - describeError, -} from "./store-maintenance.js"; +import { CLAIM_LEDGER_PURGE_SPEC, EVENT_LEDGER_PURGE_SPEC, GOVERNOR_LEDGER_PURGE_SPEC, PREDICTION_LEDGER_PURGE_SPEC, PORTFOLIO_QUEUE_PURGE_SPEC, RUN_STATE_PURGE_SPEC, CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, POLICY_VERDICT_CACHE_PURGE_SPEC, countStoreByRepo, describeError, } from "./store-maintenance.js"; import { argsWantJson, reportCliFailure } from "./cli-error.js"; - const PURGE_USAGE = "Usage: loopover-miner purge --repo [--dry-run] [--json]"; - -export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = - "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; - +export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; const REAL_PURGE_TARGETS = [ - { name: "claim-ledger", optionKey: "openClaimLedger", opener: openClaimLedger, resolveDbPath: resolveClaimLedgerDbPath, spec: CLAIM_LEDGER_PURGE_SPEC }, - { name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC }, - { name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC }, - { name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC }, - { name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC }, - { name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC }, - { name: "contribution-profile-cache", optionKey: "initContributionProfileCache", opener: initContributionProfileCache, resolveDbPath: resolveContributionProfileCacheDbPath, spec: CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC }, - // governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a - // single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091). - { name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] }, - { name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC }, + { name: "claim-ledger", optionKey: "openClaimLedger", opener: openClaimLedger, resolveDbPath: resolveClaimLedgerDbPath, spec: CLAIM_LEDGER_PURGE_SPEC }, + { name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC }, + { name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC }, + { name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC }, + { name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC }, + { name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC }, + { name: "contribution-profile-cache", optionKey: "initContributionProfileCache", opener: initContributionProfileCache, resolveDbPath: resolveContributionProfileCacheDbPath, spec: CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC }, + // governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a + // single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091). + { name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] }, + { name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC }, ]; - function parseRepoArg(value, usage) { - if (!value) return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; + if (!value) + return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; } - export function parsePurgeArgs(args) { - const options = { json: false, dryRun: false, repoFullName: null }; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; - } - if (token === "--dry-run") { - options.dryRun = true; - continue; + const options = { json: false, dryRun: false, repoFullName: null }; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + // Only the flag-look-alike case is checked here ("--repo --json") -- a genuinely missing value (repoArg + // undefined) falls through to parseRepoArg's own `!value` guard below, the single source of truth for that. + if (repoArg !== undefined && repoArg.startsWith("-")) + return { error: PURGE_USAGE }; + const repo = parseRepoArg(repoArg, PURGE_USAGE); + if ("error" in repo) + return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + return { error: `Unknown option: ${token}` }; } - if (token === "--repo") { - const repoArg = args[index + 1]; - // Only the flag-look-alike case is checked here ("--repo --json") -- a genuinely missing value (repoArg - // undefined) falls through to parseRepoArg's own `!value` guard below, the single source of truth for that. - if (repoArg !== undefined && repoArg.startsWith("-")) return { error: PURGE_USAGE }; - const repo = parseRepoArg(repoArg, PURGE_USAGE); - if ("error" in repo) return repo; - options.repoFullName = repo.repoFullName; - index += 1; - continue; - } - return { error: `Unknown option: ${token}` }; - } - - if (!options.repoFullName) return { error: PURGE_USAGE }; - return options; + if (!options.repoFullName) + return { error: PURGE_USAGE }; + return { json: options.json, dryRun: options.dryRun, repoFullName: options.repoFullName }; } - /** Read-only row count against an on-disk store file, for --dry-run. `{ readOnly: true }` (camelCase) is the * only option node:sqlite recognizes for a driver-enforced read-only connection -- the lowercase `readonly` * key is silently ignored. Never touches a store that doesn't exist yet (opening one -- even read-only -- * requires the file to already be there; a dry run must make zero writes). */ function countExistingRows(dbPath, countFn) { - if (!existsSync(dbPath)) return 0; - const db = new DatabaseSync(dbPath, { readOnly: true }); - try { - return countFn(db); - } finally { - db.close(); - } + if (!existsSync(dbPath)) + return 0; + const db = new DatabaseSync(dbPath, { readOnly: true }); + try { + return countFn(db); + } + finally { + db.close(); + } } - function renderDryRunSummary(result) { - const purgeableLine = result.stores - .map((entry) => `${entry.store}=${entry.wouldPurge}`) - .join(", "); - return [ - `DRY RUN: would purge ${result.repoFullName} from: ${purgeableLine}. No writes were made.`, - `${ATTEMPT_LOG_NOT_PURGEABLE_NOTE} (${result.attemptLogTotalRows} total row(s) currently in attempt-log, all repos).`, - ].join("\n"); + const purgeableLine = result.stores + .map((entry) => `${entry.store}=${entry.wouldPurge}`) + .join(", "); + return [ + `DRY RUN: would purge ${result.repoFullName} from: ${purgeableLine}. No writes were made.`, + `${ATTEMPT_LOG_NOT_PURGEABLE_NOTE} (${result.attemptLogTotalRows} total row(s) currently in attempt-log, all repos).`, + ].join("\n"); } - export function runPurgeDryRun(parsed, options = {}) { - const resolveDbPaths = options.resolveDbPaths ?? {}; - const stores = REAL_PURGE_TARGETS.map((target) => { - const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)(); - // A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the - // per-table counts against the single read-only handle so the preview matches what a real purge removes. - const specs = target.specs ?? [target.spec]; - try { - const wouldPurge = countExistingRows(dbPath, (db) => - specs.reduce((sum, spec) => sum + countStoreByRepo(db, spec, parsed.repoFullName), 0), - ); - return { store: target.name, wouldPurge }; - } catch (error) { - return { store: target.name, wouldPurge: null, error: describeError(error) }; + const resolveDbPaths = options.resolveDbPaths ?? {}; + const stores = REAL_PURGE_TARGETS.map((target) => { + const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)(); + // A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the + // per-table counts against the single read-only handle so the preview matches what a real purge removes. + const specs = target.specs ?? [target.spec]; + try { + const wouldPurge = countExistingRows(dbPath, (db) => specs.reduce((sum, spec) => sum + countStoreByRepo(db, spec, parsed.repoFullName), 0)); + return { store: target.name, wouldPurge }; + } + catch (error) { + return { store: target.name, wouldPurge: null, error: describeError(error) }; + } + }); + const attemptLogDbPath = (resolveDbPaths["attempt-log"] ?? resolveAttemptLogDbPath)(); + const attemptLogTotalRows = countExistingRows(attemptLogDbPath, (db) => Number(db.prepare("SELECT COUNT(*) AS count FROM attempt_log_events").get().count)); + const result = { + outcome: "dry_run", + repoFullName: parsed.repoFullName, + stores, + attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE, + attemptLogTotalRows, + }; + if (parsed.json) { + console.log(JSON.stringify(result, null, 2)); + } + else { + console.log(renderDryRunSummary(result)); } - }); - - const attemptLogDbPath = (resolveDbPaths["attempt-log"] ?? resolveAttemptLogDbPath)(); - const attemptLogTotalRows = countExistingRows(attemptLogDbPath, (db) => - Number(db.prepare("SELECT COUNT(*) AS count FROM attempt_log_events").get().count), - ); - - const result = { - outcome: "dry_run", - repoFullName: parsed.repoFullName, - stores, - attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE, - attemptLogTotalRows, - }; - - if (parsed.json) { - console.log(JSON.stringify(result, null, 2)); - } else { - console.log(renderDryRunSummary(result)); - } - return 0; + return 0; } - function purgeOneStore(target, options, repoFullName) { - const ownsStore = options[target.optionKey] === undefined; - let store; - try { - store = (options[target.optionKey] ?? target.opener)(); - const purged = store.purgeByRepo(repoFullName); - return { store: target.name, purged }; - } catch (error) { - return { store: target.name, purged: null, error: describeError(error) }; - } finally { - if (ownsStore) store?.close(); - } + const injected = options[target.optionKey]; + const ownsStore = injected === undefined; + let store; + try { + store = (injected ?? target.opener)(); + const purged = store.purgeByRepo(repoFullName); + return { store: target.name, purged }; + } + catch (error) { + return { store: target.name, purged: null, error: describeError(error) }; + } + finally { + if (ownsStore) + store?.close(); + } } - function renderPurgeSummary(summary) { - const perStore = summary.stores - .map((entry) => { - if ("error" in entry) return `${entry.store}=ERROR(${entry.error})`; - if (entry.purged === null) return `${entry.store}=skipped`; - return `${entry.store}=${entry.purged}`; + const perStore = summary.stores + .map((entry) => { + if ("error" in entry && entry.error !== undefined) + return `${entry.store}=ERROR(${entry.error})`; + if (entry.purged === null) + return `${entry.store}=skipped`; + return `${entry.store}=${entry.purged}`; }) - .join(", "); - return [ - `Purged ${summary.totalPurged} row(s) for ${summary.repoFullName} at ${summary.purgedAt}: ${perStore}.`, - ATTEMPT_LOG_NOT_PURGEABLE_NOTE, - ].join(" "); + .join(", "); + return [ + `Purged ${summary.totalPurged} row(s) for ${summary.repoFullName} at ${summary.purgedAt}: ${perStore}.`, + ATTEMPT_LOG_NOT_PURGEABLE_NOTE, + ].join(" "); } - export function runPurge(args, options = {}) { - const parsed = parsePurgeArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - if (parsed.dryRun) { - return runPurgeDryRun(parsed, options); - } - - const perStoreResults = REAL_PURGE_TARGETS.map((target) => purgeOneStore(target, options, parsed.repoFullName)); - perStoreResults.push({ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE }); - - const totalPurged = perStoreResults.reduce((sum, entry) => sum + (entry.purged ?? 0), 0); - const hadError = perStoreResults.some((entry) => "error" in entry); - const summary = { - outcome: hadError ? "partial" : "purged", - repoFullName: parsed.repoFullName, - totalPurged, - stores: perStoreResults, - purgedAt: new Date().toISOString(), - }; - - // Audit-observable by design (#5564): print the summary in BOTH the success and partial-failure case, so a - // purge -- or a purge that only partly succeeded -- is never silent. - if (parsed.json) { - console.log(JSON.stringify(summary, null, 2)); - } else { - console.log(renderPurgeSummary(summary)); - } - return hadError ? 2 : 0; + const parsed = parsePurgeArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + if (parsed.dryRun) { + return runPurgeDryRun(parsed, options); + } + const perStoreResults = REAL_PURGE_TARGETS.map((target) => purgeOneStore(target, options, parsed.repoFullName)); + perStoreResults.push({ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE }); + const totalPurged = perStoreResults.reduce((sum, entry) => sum + (entry.purged ?? 0), 0); + const hadError = perStoreResults.some((entry) => "error" in entry && entry.error !== undefined); + const summary = { + outcome: hadError ? "partial" : "purged", + repoFullName: parsed.repoFullName, + totalPurged, + stores: perStoreResults, + purgedAt: new Date().toISOString(), + }; + // Audit-observable by design (#5564): print the summary in BOTH the success and partial-failure case, so a + // purge -- or a purge that only partly succeeded -- is never silent. + if (parsed.json) { + console.log(JSON.stringify(summary, null, 2)); + } + else { + console.log(renderPurgeSummary(summary)); + } + return hadError ? 2 : 0; } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVyZ2UtY2xpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicHVyZ2UtY2xpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG1IQUFtSDtBQUNuSCx1R0FBdUc7QUFDdkcsZ0hBQWdIO0FBQ2hILDRHQUE0RztBQUM1RywyRUFBMkU7QUFDM0UsK0dBQStHO0FBQy9HLDhHQUE4RztBQUM5RywrR0FBK0c7QUFDL0csRUFBRTtBQUNGLDJHQUEyRztBQUMzRywrR0FBK0c7QUFDL0csMEdBQTBHO0FBQzFHLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDckMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLGFBQWEsQ0FBQztBQUMzQyxPQUFPLEVBQUUsZUFBZSxFQUFFLHdCQUF3QixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDOUUsT0FBTyxFQUFFLGVBQWUsRUFBRSx3QkFBd0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQzlFLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3ZGLE9BQU8sRUFBRSxvQkFBb0IsRUFBRSw2QkFBNkIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzdGLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSwyQkFBMkIsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzVGLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxxQkFBcUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQzFFLE9BQU8sRUFBRSw0QkFBNEIsRUFBRSxxQ0FBcUMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ3RILE9BQU8sRUFBRSxpQkFBaUIsRUFBRSwwQkFBMEIsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBQ3BGLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSwrQkFBK0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQ3pHLE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzNELE9BQU8s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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/purge-cli.ts b/packages/loopover-miner/lib/purge-cli.ts new file mode 100644 index 0000000000..28dc84859b --- /dev/null +++ b/packages/loopover-miner/lib/purge-cli.ts @@ -0,0 +1,262 @@ +// `loopover-miner purge` (#5564, #6599): an explicit, operator-invoked right-to-be-forgotten path across the local +// ledgers. Deletes every row for one repo from the stores that have a real `repoColumn` (claim-ledger, +// event-ledger, governor-ledger, prediction-ledger, portfolio-queue, run-state, contribution-profile-cache, and +// governor-state's two repo-scoped tables — #7091), via each store's own `purgeByRepo` method (which reuses +// `store-maintenance.js`'s shared, identifier-guarded `purgeStoreByRepo`). +// `attempt-log.js` is deliberately reported as not-purgeable rather than silently skipped or approximated: its +// payload is a free-form `Record` with no dedicated repo column, so a precise per-repo match +// isn't possible there without risking false matches -- see store-maintenance.js's own purge-spec doc comment. +// +// Every purge is audit-observable by design (#5564's own acceptance criteria): the real (non-dry-run) path +// always prints a per-store summary, even under --json, so a purge can never be silent. A failure in one store +// does not prevent reporting what succeeded in the others -- see purgeOneStore's own per-store try/catch. +import { existsSync } from "node:fs"; +import { DatabaseSync } from "node:sqlite"; +import { openClaimLedger, resolveClaimLedgerDbPath } from "./claim-ledger.js"; +import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js"; +import { initGovernorLedger, resolveGovernorLedgerDbPath } from "./governor-ledger.js"; +import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js"; +import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "./portfolio-queue.js"; +import { initRunStateStore, resolveRunStateDbPath } from "./run-state.js"; +import { initContributionProfileCache, resolveContributionProfileCacheDbPath } from "./contribution-profile-cache.js"; +import { openGovernorState, resolveGovernorStateDbPath } from "./governor-state.js"; +import { initPolicyVerdictCacheStore, resolvePolicyVerdictCacheDbPath } from "./policy-verdict-cache.js"; +import { resolveAttemptLogDbPath } from "./attempt-log.js"; +import { + CLAIM_LEDGER_PURGE_SPEC, + EVENT_LEDGER_PURGE_SPEC, + GOVERNOR_LEDGER_PURGE_SPEC, + PREDICTION_LEDGER_PURGE_SPEC, + PORTFOLIO_QUEUE_PURGE_SPEC, + RUN_STATE_PURGE_SPEC, + CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC, + GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, + GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC, + POLICY_VERDICT_CACHE_PURGE_SPEC, + countStoreByRepo, + describeError, +} from "./store-maintenance.js"; +import { argsWantJson, reportCliFailure } from "./cli-error.js"; + +const PURGE_USAGE = "Usage: loopover-miner purge --repo [--dry-run] [--json]"; + +export const ATTEMPT_LOG_NOT_PURGEABLE_NOTE = + "attempt-log has no repoFullName column and cannot be purged by repo (#5564); its rows are unaffected"; + +type PurgeSpec = Parameters[1]; +type PurgeableStore = { purgeByRepo(repoFullName: string): number; close(): void }; + +type PurgeTarget = { + name: string; + optionKey: string; + opener: () => PurgeableStore; + resolveDbPath: () => string; + spec?: PurgeSpec; + specs?: PurgeSpec[]; +}; + +const REAL_PURGE_TARGETS: PurgeTarget[] = [ + { name: "claim-ledger", optionKey: "openClaimLedger", opener: openClaimLedger, resolveDbPath: resolveClaimLedgerDbPath, spec: CLAIM_LEDGER_PURGE_SPEC }, + { name: "event-ledger", optionKey: "initEventLedger", opener: initEventLedger, resolveDbPath: resolveEventLedgerDbPath, spec: EVENT_LEDGER_PURGE_SPEC }, + { name: "governor-ledger", optionKey: "initGovernorLedger", opener: initGovernorLedger, resolveDbPath: resolveGovernorLedgerDbPath, spec: GOVERNOR_LEDGER_PURGE_SPEC }, + { name: "prediction-ledger", optionKey: "initPredictionLedger", opener: initPredictionLedger, resolveDbPath: resolvePredictionLedgerDbPath, spec: PREDICTION_LEDGER_PURGE_SPEC }, + { name: "portfolio-queue", optionKey: "initPortfolioQueueStore", opener: initPortfolioQueueStore, resolveDbPath: resolvePortfolioQueueDbPath, spec: PORTFOLIO_QUEUE_PURGE_SPEC }, + { name: "run-state", optionKey: "initRunStateStore", opener: initRunStateStore, resolveDbPath: resolveRunStateDbPath, spec: RUN_STATE_PURGE_SPEC }, + { name: "contribution-profile-cache", optionKey: "initContributionProfileCache", opener: initContributionProfileCache, resolveDbPath: resolveContributionProfileCacheDbPath, spec: CONTRIBUTION_PROFILE_CACHE_PURGE_SPEC }, + // governor-state holds TWO repo-scoped tables in one DB file; its store.purgeByRepo deletes both against a + // single handle (never reopening the file), and its dry-run count sums both via `specs` (#7091). + { name: "governor-state", optionKey: "openGovernorState", opener: openGovernorState, resolveDbPath: resolveGovernorStateDbPath, specs: [GOVERNOR_REPUTATION_HISTORY_PURGE_SPEC, GOVERNOR_OWN_SUBMISSIONS_PURGE_SPEC] }, + { name: "policy-verdict-cache", optionKey: "initPolicyVerdictCacheStore", opener: initPolicyVerdictCacheStore, resolveDbPath: resolvePolicyVerdictCacheDbPath, spec: POLICY_VERDICT_CACHE_PURGE_SPEC }, +]; + +export type ParsedPurgeArgs = { json: boolean; dryRun: boolean; repoFullName: string } | { error: string }; + +type ParsedRepoArg = { repoFullName: string } | { error: string }; + +function parseRepoArg(value: string | undefined, usage: string): ParsedRepoArg { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +export function parsePurgeArgs(args: string[]): ParsedPurgeArgs { + const options: { json: boolean; dryRun: boolean; repoFullName: string | null } = { json: false, dryRun: false, repoFullName: null }; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--repo") { + const repoArg = args[index + 1]; + // Only the flag-look-alike case is checked here ("--repo --json") -- a genuinely missing value (repoArg + // undefined) falls through to parseRepoArg's own `!value` guard below, the single source of truth for that. + if (repoArg !== undefined && repoArg.startsWith("-")) return { error: PURGE_USAGE }; + const repo = parseRepoArg(repoArg, PURGE_USAGE); + if ("error" in repo) return repo; + options.repoFullName = repo.repoFullName; + index += 1; + continue; + } + return { error: `Unknown option: ${token}` }; + } + + if (!options.repoFullName) return { error: PURGE_USAGE }; + return { json: options.json, dryRun: options.dryRun, repoFullName: options.repoFullName }; +} + +export type PurgeStoreResult = { store: string; purged: number | null; error?: string; note?: string }; +export type PurgeDryRunStoreResult = { store: string; wouldPurge: number | null; error?: string }; + +export type PurgeDryRunResult = { + outcome: "dry_run"; + repoFullName: string; + stores: PurgeDryRunStoreResult[]; + attemptLogNote: string; + attemptLogTotalRows: number; +}; + +export type PurgeSummary = { + outcome: "purged" | "partial"; + repoFullName: string; + totalPurged: number; + stores: PurgeStoreResult[]; + purgedAt: string; +}; + +export type PurgeCliOptions = { + resolveDbPaths?: Record string>; +} & Record; + +/** Read-only row count against an on-disk store file, for --dry-run. `{ readOnly: true }` (camelCase) is the + * only option node:sqlite recognizes for a driver-enforced read-only connection -- the lowercase `readonly` + * key is silently ignored. Never touches a store that doesn't exist yet (opening one -- even read-only -- + * requires the file to already be there; a dry run must make zero writes). */ +function countExistingRows(dbPath: string, countFn: (db: DatabaseSync) => number): number { + if (!existsSync(dbPath)) return 0; + const db = new DatabaseSync(dbPath, { readOnly: true }); + try { + return countFn(db); + } finally { + db.close(); + } +} + +function renderDryRunSummary(result: PurgeDryRunResult): string { + const purgeableLine = result.stores + .map((entry) => `${entry.store}=${entry.wouldPurge}`) + .join(", "); + return [ + `DRY RUN: would purge ${result.repoFullName} from: ${purgeableLine}. No writes were made.`, + `${ATTEMPT_LOG_NOT_PURGEABLE_NOTE} (${result.attemptLogTotalRows} total row(s) currently in attempt-log, all repos).`, + ].join("\n"); +} + +export function runPurgeDryRun(parsed: { repoFullName: string; json: boolean }, options: PurgeCliOptions = {}): number { + const resolveDbPaths = options.resolveDbPaths ?? {}; + const stores: PurgeDryRunStoreResult[] = REAL_PURGE_TARGETS.map((target) => { + const dbPath = (resolveDbPaths[target.name] ?? target.resolveDbPath)(); + // A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the + // per-table counts against the single read-only handle so the preview matches what a real purge removes. + const specs = target.specs ?? [target.spec!]; + try { + const wouldPurge = countExistingRows(dbPath, (db) => + specs.reduce((sum, spec) => sum + countStoreByRepo(db, spec, parsed.repoFullName), 0), + ); + return { store: target.name, wouldPurge }; + } catch (error) { + return { store: target.name, wouldPurge: null, error: describeError(error) }; + } + }); + + const attemptLogDbPath = (resolveDbPaths["attempt-log"] ?? resolveAttemptLogDbPath)(); + const attemptLogTotalRows = countExistingRows(attemptLogDbPath, (db) => + Number((db.prepare("SELECT COUNT(*) AS count FROM attempt_log_events").get() as { count: number | bigint }).count), + ); + + const result: PurgeDryRunResult = { + outcome: "dry_run", + repoFullName: parsed.repoFullName, + stores, + attemptLogNote: ATTEMPT_LOG_NOT_PURGEABLE_NOTE, + attemptLogTotalRows, + }; + + if (parsed.json) { + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(renderDryRunSummary(result)); + } + return 0; +} + +function purgeOneStore(target: PurgeTarget, options: PurgeCliOptions, repoFullName: string): PurgeStoreResult { + const injected = (options as Record PurgeableStore) | undefined>)[target.optionKey]; + const ownsStore = injected === undefined; + let store: PurgeableStore | undefined; + try { + store = (injected ?? target.opener)(); + const purged = store.purgeByRepo(repoFullName); + return { store: target.name, purged }; + } catch (error) { + return { store: target.name, purged: null, error: describeError(error) }; + } finally { + if (ownsStore) store?.close(); + } +} + +function renderPurgeSummary(summary: PurgeSummary): string { + const perStore = summary.stores + .map((entry) => { + if ("error" in entry && entry.error !== undefined) return `${entry.store}=ERROR(${entry.error})`; + if (entry.purged === null) return `${entry.store}=skipped`; + return `${entry.store}=${entry.purged}`; + }) + .join(", "); + return [ + `Purged ${summary.totalPurged} row(s) for ${summary.repoFullName} at ${summary.purgedAt}: ${perStore}.`, + ATTEMPT_LOG_NOT_PURGEABLE_NOTE, + ].join(" "); +} + +export function runPurge(args: string[], options: PurgeCliOptions = {}): number { + const parsed = parsePurgeArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + if (parsed.dryRun) { + return runPurgeDryRun(parsed, options); + } + + const perStoreResults: PurgeStoreResult[] = REAL_PURGE_TARGETS.map((target) => purgeOneStore(target, options, parsed.repoFullName)); + perStoreResults.push({ store: "attempt-log", purged: null, note: ATTEMPT_LOG_NOT_PURGEABLE_NOTE }); + + const totalPurged = perStoreResults.reduce((sum, entry) => sum + (entry.purged ?? 0), 0); + const hadError = perStoreResults.some((entry) => "error" in entry && entry.error !== undefined); + const summary: PurgeSummary = { + outcome: hadError ? "partial" : "purged", + repoFullName: parsed.repoFullName, + totalPurged, + stores: perStoreResults, + purgedAt: new Date().toISOString(), + }; + + // Audit-observable by design (#5564): print the summary in BOTH the success and partial-failure case, so a + // purge -- or a purge that only partly succeeded -- is never silent. + if (parsed.json) { + console.log(JSON.stringify(summary, null, 2)); + } else { + console.log(renderPurgeSummary(summary)); + } + return hadError ? 2 : 0; +} diff --git a/packages/loopover-miner/lib/run-state-cli.d.ts b/packages/loopover-miner/lib/run-state-cli.d.ts index eafbe6fc43..1856ff824c 100644 --- a/packages/loopover-miner/lib/run-state-cli.d.ts +++ b/packages/loopover-miner/lib/run-state-cli.d.ts @@ -1,27 +1,23 @@ -export type ParsedStateGetArgs = - | { - repoFullName: string; - json: boolean; - apiBaseUrl: string | undefined; - } - | { error: string }; - -export type ParsedStateSetArgs = - | { - repoFullName: string; - state: "idle" | "discovering" | "planning" | "preparing"; - dryRun: boolean; - json: boolean; - apiBaseUrl: string | undefined; - } - | { error: string }; - -export function parseStateGetArgs(args: string[]): ParsedStateGetArgs; - -export function parseStateSetArgs(args: string[]): ParsedStateSetArgs; - -export function runStateGet(args: string[]): number; - -export function runStateSet(args: string[]): number; - -export function runStateCli(subcommand: string | undefined, args: string[]): number; +type RunStateValue = "idle" | "discovering" | "planning" | "preparing"; +export type ParsedStateGetArgs = { + repoFullName: string; + json: boolean; + apiBaseUrl: string | undefined; +} | { + error: string; +}; +export type ParsedStateSetArgs = { + repoFullName: string; + state: RunStateValue; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; +} | { + error: string; +}; +export declare function parseStateGetArgs(args: string[]): ParsedStateGetArgs; +export declare function parseStateSetArgs(args: string[]): ParsedStateSetArgs; +export declare function runStateGet(args: string[]): number; +export declare function runStateSet(args: string[]): number; +export declare function runStateCli(subcommand: string | undefined, args: string[]): number; +export {}; diff --git a/packages/loopover-miner/lib/run-state-cli.js b/packages/loopover-miner/lib/run-state-cli.js index 67a989bc3e..13897972d1 100644 --- a/packages/loopover-miner/lib/run-state-cli.js +++ b/packages/loopover-miner/lib/run-state-cli.js @@ -1,154 +1,148 @@ import { RUN_STATES, getRunState, setRunState } from "./run-state.js"; import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; - const STATE_GET_USAGE = "Usage: loopover-miner state get [--api-base-url ] [--json]"; -const STATE_SET_USAGE = - "Usage: loopover-miner state set [--api-base-url ] [--dry-run] [--json]"; - +const STATE_SET_USAGE = "Usage: loopover-miner state set [--api-base-url ] [--dry-run] [--json]"; const allowedRunStates = new Set(RUN_STATES); - function parseRepoArg(value, usage) { - if (!value) return { error: usage }; - const trimmed = value.trim(); - const [owner, repo, extra] = trimmed.split("/"); - if (!owner || !repo || extra !== undefined) { - return { error: "Repository must be in owner/repo form." }; - } - return { repoFullName: `${owner}/${repo}` }; + if (!value) + return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; } - export function parseStateGetArgs(args) { - const options = { json: false, apiBaseUrl: undefined }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; + const options = { json: false, apiBaseUrl: undefined }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + // #5563: scope the lookup to a non-default forge host, so it doesn't collide with (or get confused for) a + // same-named repo on the default github.com host. + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: STATE_GET_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - // #5563: scope the lookup to a non-default forge host, so it doesn't collide with (or get confused for) a - // same-named repo on the default github.com host. - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { + if (positional.length !== 1) { return { error: STATE_GET_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; - } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; } - positional.push(token); - } - - if (positional.length !== 1) { - return { error: STATE_GET_USAGE }; - } - - const repo = parseRepoArg(positional[0], STATE_GET_USAGE); - if ("error" in repo) return repo; - - return { repoFullName: repo.repoFullName, ...options }; + const repo = parseRepoArg(positional[0], STATE_GET_USAGE); + if ("error" in repo) + return repo; + return { repoFullName: repo.repoFullName, ...options }; } - export function parseStateSetArgs(args) { - const options = { json: false, dryRun: false, apiBaseUrl: undefined }; - const positional = []; - - for (let index = 0; index < args.length; index += 1) { - const token = args[index]; - if (token === "--json") { - options.json = true; - continue; + const options = { + json: false, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional = []; + for (let index = 0; index < args.length; index += 1) { + const token = args[index]; + if (token === "--json") { + options.json = true; + continue; + } + // #4847: reports what a real state set would do and returns before writing to the run-state store. + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: STATE_SET_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); } - // #4847: reports what a real state set would do and returns before writing to the run-state store. - if (token === "--dry-run") { - options.dryRun = true; - continue; - } - if (token === "--api-base-url") { - const value = args[index + 1]; - if (!value || value.startsWith("-")) { + if (positional.length !== 2) { return { error: STATE_SET_USAGE }; - } - options.apiBaseUrl = value; - index += 1; - continue; } - if (token.startsWith("-")) { - return { error: `Unknown option: ${token}` }; + const repo = parseRepoArg(positional[0], STATE_SET_USAGE); + if ("error" in repo) + return repo; + const state = positional[1]; + if (!allowedRunStates.has(state)) { + return { error: `Invalid state: ${state}. Expected one of ${RUN_STATES.join(", ")}.` }; } - positional.push(token); - } - - if (positional.length !== 2) { - return { error: STATE_SET_USAGE }; - } - - const repo = parseRepoArg(positional[0], STATE_SET_USAGE); - if ("error" in repo) return repo; - - const state = positional[1]; - if (!allowedRunStates.has(state)) { - return { error: `Invalid state: ${state}. Expected one of ${RUN_STATES.join(", ")}.` }; - } - - return { repoFullName: repo.repoFullName, state, ...options }; + return { repoFullName: repo.repoFullName, state: state, ...options }; } - export function runStateGet(args) { - const parsed = parseStateGetArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - try { - const state = getRunState(parsed.repoFullName, parsed.apiBaseUrl); - if (parsed.json) { - console.log(JSON.stringify({ repoFullName: parsed.repoFullName, state })); - } else { - console.log(state ?? "none"); + const parsed = parseStateGetArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + try { + const state = getRunState(parsed.repoFullName, parsed.apiBaseUrl); + if (parsed.json) { + console.log(JSON.stringify({ repoFullName: parsed.repoFullName, state })); + } + else { + console.log(state ?? "none"); + } + return 0; + } + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); } - return 0; - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } } - export function runStateSet(args) { - const parsed = parseStateSetArgs(args); - if ("error" in parsed) { - return reportCliFailure(argsWantJson(args), parsed.error); - } - - if (parsed.dryRun) { - const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, state: parsed.state }; - if (parsed.json) { - console.log(JSON.stringify(dryRunResult)); - } else { - console.log(`DRY RUN: would set ${parsed.repoFullName}'s run state to "${parsed.state}". No run-state write was made.`); + const parsed = parseStateSetArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, state: parsed.state }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult)); + } + else { + console.log(`DRY RUN: would set ${parsed.repoFullName}'s run state to "${parsed.state}". No run-state write was made.`); + } + return 0; + } + try { + const write = setRunState(parsed.repoFullName, parsed.state, parsed.apiBaseUrl); + if (parsed.json) { + console.log(JSON.stringify(write)); + } + else { + console.log(write.state); + } + return 0; } - return 0; - } - - try { - const write = setRunState(parsed.repoFullName, parsed.state, parsed.apiBaseUrl); - if (parsed.json) { - console.log(JSON.stringify(write)); - } else { - console.log(write.state); + catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); } - return 0; - } catch (error) { - return reportCliFailure(parsed.json, describeCliError(error)); - } } - export function runStateCli(subcommand, args) { - if (subcommand === "get") return runStateGet(args); - if (subcommand === "set") return runStateSet(args); - return reportCliFailure(argsWantJson(args), `Unknown state subcommand: ${subcommand ?? ""}. ${STATE_GET_USAGE}`); + if (subcommand === "get") + return runStateGet(args); + if (subcommand === "set") + return runStateSet(args); + return reportCliFailure(argsWantJson(args), `Unknown state subcommand: ${subcommand ?? ""}. ${STATE_GET_USAGE}`); } +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicnVuLXN0YXRlLWNsaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInJ1bi1zdGF0ZS1jbGkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxXQUFXLEVBQUUsV0FBVyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDdEUsT0FBTyxFQUFFLFlBQVksRUFBRSxnQkFBZ0IsRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRWxGLE1BQU0sZUFBZSxHQUFHLDhFQUE4RSxDQUFDO0FBQ3ZHLE1BQU0sZUFBZSxHQUNuQixnSUFBZ0ksQ0FBQztBQUluSSxNQUFNLGdCQUFnQixHQUFHLElBQUksR0FBRyxDQUFTLFVBQVUsQ0FBQyxDQUFDO0FBc0JyRCxTQUFTLFlBQVksQ0FBQyxLQUF5QixFQUFFLEtBQWE7SUFDNUQsSUFBSSxDQUFDLEtBQUs7UUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxDQUFDO0lBQ3BDLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUM3QixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxPQUFPLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVMsRUFBRSxDQUFDO1FBQzNDLE9BQU8sRUFBRSxLQUFLLEVBQUUsd0NBQXdDLEVBQUUsQ0FBQztJQUM3RCxDQUFDO0lBQ0QsT0FBTyxFQUFFLFlBQVksRUFBRSxHQUFHLEtBQUssSUFBSSxJQUFJLEVBQUUsRUFBRSxDQUFDO0FBQzlDLENBQUM7QUFFRCxNQUFNLFVBQVUsaUJBQWlCLENBQUMsSUFBYztJQUM5QyxNQUFNLE9BQU8sR0FBc0QsRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxTQUFTLEVBQUUsQ0FBQztJQUMxRyxNQUFNLFVBQVUs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 \ No newline at end of file diff --git a/packages/loopover-miner/lib/run-state-cli.ts b/packages/loopover-miner/lib/run-state-cli.ts new file mode 100644 index 0000000000..c5c378c4f6 --- /dev/null +++ b/packages/loopover-miner/lib/run-state-cli.ts @@ -0,0 +1,180 @@ +import { RUN_STATES, getRunState, setRunState } from "./run-state.js"; +import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js"; + +const STATE_GET_USAGE = "Usage: loopover-miner state get [--api-base-url ] [--json]"; +const STATE_SET_USAGE = + "Usage: loopover-miner state set [--api-base-url ] [--dry-run] [--json]"; + +type RunStateValue = "idle" | "discovering" | "planning" | "preparing"; + +const allowedRunStates = new Set(RUN_STATES); + +export type ParsedStateGetArgs = + | { + repoFullName: string; + json: boolean; + apiBaseUrl: string | undefined; + } + | { error: string }; + +export type ParsedStateSetArgs = + | { + repoFullName: string; + state: RunStateValue; + dryRun: boolean; + json: boolean; + apiBaseUrl: string | undefined; + } + | { error: string }; + +type ParsedRepoArg = { repoFullName: string } | { error: string }; + +function parseRepoArg(value: string | undefined, usage: string): ParsedRepoArg { + if (!value) return { error: usage }; + const trimmed = value.trim(); + const [owner, repo, extra] = trimmed.split("/"); + if (!owner || !repo || extra !== undefined) { + return { error: "Repository must be in owner/repo form." }; + } + return { repoFullName: `${owner}/${repo}` }; +} + +export function parseStateGetArgs(args: string[]): ParsedStateGetArgs { + const options: { json: boolean; apiBaseUrl: string | undefined } = { json: false, apiBaseUrl: undefined }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + // #5563: scope the lookup to a non-default forge host, so it doesn't collide with (or get confused for) a + // same-named repo on the default github.com host. + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: STATE_GET_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 1) { + return { error: STATE_GET_USAGE }; + } + + const repo = parseRepoArg(positional[0], STATE_GET_USAGE); + if ("error" in repo) return repo; + + return { repoFullName: repo.repoFullName, ...options }; +} + +export function parseStateSetArgs(args: string[]): ParsedStateSetArgs { + const options: { json: boolean; dryRun: boolean; apiBaseUrl: string | undefined } = { + json: false, + dryRun: false, + apiBaseUrl: undefined, + }; + const positional: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const token = args[index]!; + if (token === "--json") { + options.json = true; + continue; + } + // #4847: reports what a real state set would do and returns before writing to the run-state store. + if (token === "--dry-run") { + options.dryRun = true; + continue; + } + if (token === "--api-base-url") { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + return { error: STATE_SET_USAGE }; + } + options.apiBaseUrl = value; + index += 1; + continue; + } + if (token.startsWith("-")) { + return { error: `Unknown option: ${token}` }; + } + positional.push(token); + } + + if (positional.length !== 2) { + return { error: STATE_SET_USAGE }; + } + + const repo = parseRepoArg(positional[0], STATE_SET_USAGE); + if ("error" in repo) return repo; + + const state = positional[1]!; + if (!allowedRunStates.has(state)) { + return { error: `Invalid state: ${state}. Expected one of ${RUN_STATES.join(", ")}.` }; + } + + return { repoFullName: repo.repoFullName, state: state as RunStateValue, ...options }; +} + +export function runStateGet(args: string[]): number { + const parsed = parseStateGetArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + try { + const state = getRunState(parsed.repoFullName, parsed.apiBaseUrl); + if (parsed.json) { + console.log(JSON.stringify({ repoFullName: parsed.repoFullName, state })); + } else { + console.log(state ?? "none"); + } + return 0; + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runStateSet(args: string[]): number { + const parsed = parseStateSetArgs(args); + if ("error" in parsed) { + return reportCliFailure(argsWantJson(args), parsed.error); + } + + if (parsed.dryRun) { + const dryRunResult = { outcome: "dry_run", repoFullName: parsed.repoFullName, state: parsed.state }; + if (parsed.json) { + console.log(JSON.stringify(dryRunResult)); + } else { + console.log(`DRY RUN: would set ${parsed.repoFullName}'s run state to "${parsed.state}". No run-state write was made.`); + } + return 0; + } + + try { + const write = setRunState(parsed.repoFullName, parsed.state, parsed.apiBaseUrl); + if (parsed.json) { + console.log(JSON.stringify(write)); + } else { + console.log(write.state); + } + return 0; + } catch (error) { + return reportCliFailure(parsed.json, describeCliError(error)); + } +} + +export function runStateCli(subcommand: string | undefined, args: string[]): number { + if (subcommand === "get") return runStateGet(args); + if (subcommand === "set") return runStateSet(args); + return reportCliFailure(argsWantJson(args), `Unknown state subcommand: ${subcommand ?? ""}. ${STATE_GET_USAGE}`); +}