#!/usr/bin/env node // @ts-nocheck import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function usage() { console.error( [ 'Usage: purge-copilot-history-quarantine.mjs [options]', '', 'Options:', ' --quarantine-dir Override quarantine root directory.', ' --before Purge runs created before or at this timestamp.', ' --keep-latest Keep the newest run directories. Default: 1', ' --execute Perform purge. Default is dry-run.', ' --help Show this help text.', ].join('\n'), ); } function parseArgs(argv) { const options = { quarantineDir: path.join( os.homedir(), '.copilot-resources-state', 'quarantine', 'copilot-history-cleanup', ), before: '', keepLatest: 1, execute: false, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') { usage(); process.exit(0); } if (arg === '--quarantine-dir') { options.quarantineDir = path.resolve(argv[index + 1] ?? options.quarantineDir); index += 1; continue; } if (arg === '--before') { options.before = argv[index + 1] ?? ''; index += 1; continue; } if (arg === '--keep-latest') { options.keepLatest = Number.parseInt(argv[index + 1] ?? '1', 10); index += 1; continue; } if (arg === '--execute') { options.execute = true; continue; } throw new Error(`Unknown argument: ${arg}`); } if (!Number.isInteger(options.keepLatest) || options.keepLatest < 0) { throw new Error(`Invalid --keep-latest value: ${options.keepLatest}`); } return options; } function safeStat(filePath) { try { return fs.statSync(filePath); } catch { return null; } } function listRuns(rootDir) { if (!fs.existsSync(rootDir)) { return []; } return fs.readdirSync(rootDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => { const runPath = path.join(rootDir, entry.name); const stat = safeStat(runPath); return stat ? { path: runPath, mtimeMs: stat.mtimeMs } : null; }) .filter(Boolean) .sort((left, right) => right.mtimeMs - left.mtimeMs); } function formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = Number(bytes) || 0; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex += 1; } return `${value.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`; } function measureSize(filePath) { const stat = safeStat(filePath); if (!stat) { return 0; } if (stat.isFile()) { return stat.size; } if (!stat.isDirectory()) { return 0; } let total = 0; for (const entry of fs.readdirSync(filePath, { withFileTypes: true })) { const childPath = path.join(filePath, entry.name); if (entry.isDirectory()) { total += measureSize(childPath); } else if (entry.isFile()) { total += safeStat(childPath)?.size ?? 0; } } return total; } function writeSummary(rootDir, execute, beforeIso, keepLatest, results, totalBytes) { const summaryPath = path.join(rootDir, 'purge-summary.md'); const lines = [ '# Copilot History Quarantine Purge Summary', '', `- Generated: ${new Date().toISOString()}`, `- Mode: ${execute ? 'execute' : 'dry-run'}`, `- Before: ${beforeIso || '(none)'}`, `- Keep latest: ${keepLatest}`, `- Runs considered: ${results.length}`, `- Runs purged: ${results.filter((entry) => entry.status === 'purged').length}`, `- Runs skipped: ${results.filter((entry) => entry.status === 'skipped').length}`, `- Runs failed: ${results.filter((entry) => entry.status === 'failed').length}`, `- Estimated bytes affected: ${formatBytes(totalBytes)}`, '', '## Results', '', ]; if (results.length === 0) { lines.push('- None'); } else { for (const result of results) { const sizeText = result.bytes > 0 ? ` (${formatBytes(result.bytes)})` : ''; lines.push(`- [${result.status}] ${result.path}${sizeText}`); if (result.message) { lines.push(` - ${result.message}`); } } } fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`, 'utf8'); return summaryPath; } export function runPurge(rawOptions) { const options = { ...rawOptions, quarantineDir: path.resolve(rawOptions.quarantineDir), }; const beforeMs = options.before ? Date.parse(options.before) : Number.POSITIVE_INFINITY; if (Number.isNaN(beforeMs)) { throw new Error(`Invalid --before timestamp: ${options.before}`); } const runs = listRuns(options.quarantineDir); const protectedRuns = new Set(runs.slice(0, options.keepLatest).map((run) => run.path)); const results = []; let totalBytes = 0; for (const run of runs) { const bytes = measureSize(run.path); totalBytes += bytes; if (protectedRuns.has(run.path)) { results.push({ status: 'skipped', path: run.path, bytes, message: 'Protected by keep-latest.' }); continue; } if (run.mtimeMs > beforeMs) { results.push({ status: 'skipped', path: run.path, bytes, message: 'Newer than cutoff.' }); continue; } if (!options.execute) { results.push({ status: 'skipped', path: run.path, bytes, message: 'Dry-run: no changes were applied.' }); continue; } try { fs.rmSync(run.path, { recursive: true, force: true }); results.push({ status: 'purged', path: run.path, bytes, message: '' }); } catch (error) { results.push({ status: 'failed', path: run.path, bytes, message: error?.message || String(error) }); } } const summaryPath = writeSummary( options.quarantineDir, options.execute, options.before, options.keepLatest, results, totalBytes, ); return { quarantineDir: options.quarantineDir, considered: results.length, purged: results.filter((entry) => entry.status === 'purged').length, failed: results.filter((entry) => entry.status === 'failed').length, summaryPath, }; } function main() { const options = parseArgs(process.argv.slice(2)); const result = runPurge(options); console.log('Copilot history quarantine purge complete.'); console.log(`Quarantine directory: ${result.quarantineDir}`); console.log(`Runs considered: ${result.considered}`); console.log(`Runs purged: ${result.purged}`); console.log(`Runs failed: ${result.failed}`); console.log(`Summary: ${result.summaryPath}`); } const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMainModule) { main(); }