#!/usr/bin/env node import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { parseArgs } from 'node:util'; import { normalizeArtifactTag } from '../src/audit/artifactTag.js'; import { planRollingPhase0ArtifactRetention, type Phase0RollingArtifactRetentionPlan, } from '../src/audit/phase0BaselineArtifactRetention.js'; function usage(): string { return [ 'Usage: node --import tsx/esm scripts/prune-phase0-baseline-artifacts.ts [options]', '', 'Options:', ' --artifacts-dir Artifacts directory (default: docs/plans/artifacts)', ' --keep-per-family Keep newest rolling tags per family (default: 8)', ' --apply Apply deletions (default: dry-run)', ' --report-tag Report tag suffix (default: current UTC date)', ' --write-default-artifacts Write report files to artifacts dir', ' --summary-json-out Write JSON report to path', ' --summary-md-out Write markdown report to path', ' --format Output format (default: text)', ' --help Show usage', ].join('\n'); } function isoDateTagNow(): string { return new Date().toISOString().slice(0, 10); } function expandHomePath(pathValue: string): string { if (!pathValue.startsWith('~')) { return pathValue; } const home = process.env.HOME; if (!home) { return pathValue; } return resolve(home, pathValue.slice(1)); } function parseOptionalInteger(raw: string | undefined, flag: string): number | undefined { if (!raw) { return undefined; } const parsed = Number(raw); if (!Number.isFinite(parsed)) { throw new Error(`Invalid ${flag} value "${raw}". Expected an integer.`); } if (!Number.isInteger(parsed)) { throw new Error(`Invalid ${flag} value "${raw}". Expected an integer.`); } if (parsed < 0) { throw new Error(`${flag} must be greater than or equal to 0.`); } return parsed; } function renderText(plan: Phase0RollingArtifactRetentionPlan, artifactsDir: string, keepPerFamily: number, apply: boolean): string { const lines: string[] = []; lines.push('# Phase-0 Rolling Artifact Prune'); lines.push(''); lines.push(`Artifacts dir: ${artifactsDir}`); lines.push(`Keep per family: ${keepPerFamily}`); lines.push(`Mode: ${apply ? 'apply' : 'dry-run'}`); lines.push(`Keep files: ${plan.keep.length}`); lines.push(`Remove files: ${plan.remove.length}`); lines.push(''); lines.push('## Families'); for (const row of plan.families) { lines.push(`- ${row.family}: tags total=${row.total_tags} keep=${row.keep_tags} remove=${row.remove_tags}`); } lines.push(''); lines.push('## Remove List'); if (plan.remove.length === 0) { lines.push('- none'); } else { for (const row of plan.remove) { lines.push(`- ${row.file_name}`); } } return lines.join('\n'); } async function writeTextFile(pathValue: string, contents: string): Promise { await mkdir(dirname(pathValue), { recursive: true }); await writeFile(pathValue, `${contents}\n`, 'utf8'); } async function readArtifactFileNames(artifactsDir: string): Promise { try { return await readdir(artifactsDir); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { return []; } throw error; } } async function main(): Promise { const { values } = parseArgs({ options: { 'artifacts-dir': { type: 'string' }, 'keep-per-family': { type: 'string' }, apply: { type: 'boolean' }, 'report-tag': { type: 'string' }, 'write-default-artifacts': { type: 'boolean' }, 'summary-json-out': { type: 'string' }, 'summary-md-out': { type: 'string' }, format: { type: 'string' }, help: { type: 'boolean', short: 'h' }, }, strict: true, allowPositionals: false, }); if (values.help) { process.stdout.write(`${usage()}\n`); return; } const artifactsDir = resolve(expandHomePath(values['artifacts-dir'] ?? 'docs/plans/artifacts')); const keepPerFamily = parseOptionalInteger(values['keep-per-family'], '--keep-per-family') ?? 8; const apply = Boolean(values.apply); const format = values.format ?? 'text'; const reportTag = normalizeArtifactTag(values['report-tag'] ?? isoDateTagNow(), '--report-tag'); const writeDefaultArtifacts = Boolean(values['write-default-artifacts']); if (format !== 'text' && format !== 'json') { throw new Error(`Invalid --format value "${format}".`); } const defaultBaseName = resolve(artifactsDir, `phase0_baseline_live_prune_${reportTag}`); const summaryJsonOut = values['summary-json-out'] ? resolve(expandHomePath(values['summary-json-out'])) : writeDefaultArtifacts ? `${defaultBaseName}.json` : undefined; const summaryMdOut = values['summary-md-out'] ? resolve(expandHomePath(values['summary-md-out'])) : writeDefaultArtifacts ? `${defaultBaseName}.md` : undefined; const files = await readArtifactFileNames(artifactsDir); const plan = planRollingPhase0ArtifactRetention(files, keepPerFamily); if (apply) { for (const row of plan.remove) { await rm(resolve(artifactsDir, row.file_name)); } } const payload = { generated_at: new Date().toISOString(), artifacts_dir: artifactsDir, keep_per_family: keepPerFamily, apply, report_tag: reportTag, reports: { summary_json_out: summaryJsonOut, summary_md_out: summaryMdOut, }, plan, }; const jsonOutput = JSON.stringify(payload, null, 2); const textOutput = renderText(plan, artifactsDir, keepPerFamily, apply); if (summaryJsonOut) { await writeTextFile(summaryJsonOut, jsonOutput); } if (summaryMdOut) { await writeTextFile(summaryMdOut, textOutput); } if (format === 'json') { process.stdout.write(`${jsonOutput}\n`); } else { process.stdout.write(`${textOutput}\n`); } } main().catch((error) => { const message = error instanceof Error ? error.message : String(error); process.stderr.write(`${message}\n\n${usage()}\n`); process.exitCode = 1; });