#!/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)); const SCOPES = new Set(['aggressive']); function usage() { console.error( [ 'Usage: cleanup-copilot-history.mjs [options]', '', 'Options:', ' --audit-dir Audit directory used as completion boundary context.', ' --completion-cutoff Delete targets with mtime at or before this timestamp.', ' --scope Cleanup scope. Supported: aggressive. Default: aggressive', ' --quarantine-dir Override quarantine root directory.', ' --repo-root Override repository root.', ' --execute Perform cleanup. Default is dry-run.', ' --help Show this help text.', ].join('\n'), ); } function parseArgs(argv) { const options = { auditDir: '', completionCutoff: '', execute: false, scope: 'aggressive', repoRoot: path.resolve(__dirname, '../..'), quarantineDir: path.join( os.homedir(), '.copilot-resources-state', 'quarantine', 'copilot-history-cleanup', ), }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') { usage(); process.exit(0); } if (arg === '--audit-dir') { options.auditDir = path.resolve(argv[index + 1] ?? ''); index += 1; continue; } if (arg === '--completion-cutoff') { options.completionCutoff = argv[index + 1] ?? ''; index += 1; continue; } if (arg === '--scope') { options.scope = argv[index + 1] ?? options.scope; index += 1; continue; } if (arg === '--quarantine-dir') { options.quarantineDir = path.resolve(argv[index + 1] ?? options.quarantineDir); index += 1; continue; } if (arg === '--repo-root') { options.repoRoot = path.resolve(argv[index + 1] ?? options.repoRoot); index += 1; continue; } if (arg === '--execute') { options.execute = true; continue; } throw new Error(`Unknown argument: ${arg}`); } if (!SCOPES.has(options.scope)) { throw new Error(`Unsupported scope: ${options.scope}`); } return options; } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function safeStat(filePath) { try { return fs.statSync(filePath); } catch { return null; } } function listDirs(rootPath) { if (!fs.existsSync(rootPath)) { return []; } return fs.readdirSync(rootPath, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => path.join(rootPath, entry.name)); } function findLatestAuditDir(repoRoot) { const auditRoot = path.join(repoRoot, '.local', 'audits'); const dirs = []; for (const firstLevel of listDirs(auditRoot)) { if (safeStat(path.join(firstLevel, 'selection-manifest.tsv'))) { dirs.push(firstLevel); continue; } for (const secondLevel of listDirs(firstLevel)) { if (safeStat(path.join(secondLevel, 'selection-manifest.tsv'))) { dirs.push(secondLevel); } } } let latest = null; for (const dirPath of dirs) { const stat = safeStat(dirPath); if (!stat) { continue; } if (!latest || stat.mtimeMs > latest.mtimeMs) { latest = { path: dirPath, mtimeMs: stat.mtimeMs }; } } return latest?.path ?? null; } function resolveCutoffMs(options, auditDir) { if (options.completionCutoff) { const parsed = Date.parse(options.completionCutoff); if (Number.isNaN(parsed)) { throw new Error(`Invalid --completion-cutoff timestamp: ${options.completionCutoff}`); } return parsed; } const summaryPath = path.join(auditDir, 'promotion-summary.md'); const summaryStat = safeStat(summaryPath); if (summaryStat) { return summaryStat.mtimeMs; } const auditStat = safeStat(auditDir); if (auditStat) { return auditStat.mtimeMs; } return Date.now(); } function normalizePath(filePath) { return path.resolve(filePath); } function pathIsInside(candidatePath, parentPath) { const relativePath = path.relative(parentPath, candidatePath); return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); } function collectAuditRunTargets(repoRoot, currentAuditDir, cutoffMs) { const auditRoot = path.join(repoRoot, '.local', 'audits'); const candidates = []; for (const firstLevel of listDirs(auditRoot)) { const nested = listDirs(firstLevel); if (safeStat(path.join(firstLevel, 'selection-manifest.tsv'))) { nested.push(firstLevel); } for (const runDir of nested) { const manifestPath = path.join(runDir, 'selection-manifest.tsv'); if (!safeStat(manifestPath)) { continue; } const normalizedRunDir = normalizePath(runDir); if (normalizedRunDir === currentAuditDir) { continue; } const runStat = safeStat(runDir); if (!runStat) { continue; } if (runStat.mtimeMs > cutoffMs) { continue; } candidates.push({ path: normalizedRunDir, category: 'audit-run', reason: 'Historical audit run older than completion cutoff.', mtimeMs: runStat.mtimeMs, }); } } return candidates; } function collectWorkspaceTargets(cutoffMs) { const workspaceRoot = path.join( os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage', ); const relativeTargets = [ 'chatSessions', 'chatEditingSessions', path.join('GitHub.copilot-chat', 'transcripts'), path.join('GitHub.copilot-chat', 'chat-session-resources'), ]; const candidates = []; for (const workspaceDir of listDirs(workspaceRoot)) { for (const relativeTarget of relativeTargets) { const absoluteTarget = path.join(workspaceDir, relativeTarget); const targetStat = safeStat(absoluteTarget); if (!targetStat || !targetStat.isDirectory()) { continue; } if (targetStat.mtimeMs > cutoffMs) { continue; } candidates.push({ path: normalizePath(absoluteTarget), category: 'workspace-history', reason: 'Copilot or chat history artifact older than completion cutoff.', mtimeMs: targetStat.mtimeMs, }); } } return candidates; } function dedupeNestedTargets(targets) { const sorted = [...targets].sort((left, right) => left.path.localeCompare(right.path)); const result = []; for (const target of sorted) { const parentAlreadySelected = result.some((existing) => pathIsInside(target.path, existing.path)); if (!parentAlreadySelected) { result.push(target); } } return result; } 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; const entries = fs.readdirSync(filePath, { withFileTypes: true }); for (const entry of entries) { const childPath = path.join(filePath, entry.name); if (entry.isDirectory()) { total += measureSize(childPath); } else if (entry.isFile()) { const childStat = safeStat(childPath); total += childStat?.size ?? 0; } } return total; } function quarantineRelativePath(targetPath) { const relativeToRoot = path.relative(path.parse(targetPath).root, targetPath); return relativeToRoot.replace(/:/g, '_'); } function moveToQuarantine(targetPath, quarantineRoot) { const destinationPath = path.join(quarantineRoot, quarantineRelativePath(targetPath)); ensureDir(path.dirname(destinationPath)); try { fs.renameSync(targetPath, destinationPath); } catch (error) { if (error?.code !== 'EXDEV') { throw error; } fs.cpSync(targetPath, destinationPath, { recursive: true }); fs.rmSync(targetPath, { recursive: true, force: true }); } return destinationPath; } function writeQuarantineManifest(quarantineRunDir, results) { const manifestPath = path.join(quarantineRunDir, 'manifest.json'); const rows = results .filter((result) => result.status === 'quarantined' && result.destination) .map((result) => ({ originalPath: result.path, quarantinePath: result.destination, bytes: result.bytes, reason: result.reason, })); fs.writeFileSync(manifestPath, `${JSON.stringify({ createdAt: new Date().toISOString(), rows }, null, 2)}\n`, 'utf8'); return manifestPath; } function writeSummary({ auditDir, execute, scope, cutoffMs, quarantineRunDir, targets, results, totalBytes, }) { const summaryPath = path.join(auditDir, 'cleanup-summary.md'); const lines = [ '# Copilot History Cleanup Summary', '', `- Generated: ${new Date().toISOString()}`, `- Mode: ${execute ? 'execute' : 'dry-run'}`, `- Scope: ${scope}`, `- Completion cutoff: ${new Date(cutoffMs).toISOString()}`, `- Targets discovered: ${targets.length}`, `- Targets processed: ${results.filter((entry) => entry.status === 'quarantined').length}`, `- Targets skipped: ${results.filter((entry) => entry.status === 'skipped').length}`, `- Targets failed: ${results.filter((entry) => entry.status === 'failed').length}`, `- Estimated bytes affected: ${formatBytes(totalBytes)}`, `- Quarantine directory: ${quarantineRunDir}`, '', '## Results', '', ]; if (results.length === 0) { lines.push('- None'); } else { for (const result of results) { const sizeText = result.bytes > 0 ? ` (${formatBytes(result.bytes)})` : ''; const destinationText = result.destination ? ` -> ${result.destination}` : ''; lines.push(`- [${result.status}] ${result.path}${sizeText}${destinationText}`); if (result.message) { lines.push(` - ${result.message}`); } if (result.reason) { lines.push(` - Reason: ${result.reason}`); } } } fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`, 'utf8'); return summaryPath; } export function runCleanup(rawOptions) { const options = { ...rawOptions, repoRoot: path.resolve(rawOptions.repoRoot), auditDir: rawOptions.auditDir ? path.resolve(rawOptions.auditDir) : '', quarantineDir: path.resolve(rawOptions.quarantineDir), }; const auditDir = options.auditDir || findLatestAuditDir(options.repoRoot); if (!auditDir) { throw new Error('No audit directory was found. Run the audit first or pass --audit-dir.'); } const currentAuditDir = normalizePath(auditDir); const cutoffMs = resolveCutoffMs(options, currentAuditDir); const discoveredTargets = dedupeNestedTargets([ ...collectAuditRunTargets(options.repoRoot, currentAuditDir, cutoffMs), ...collectWorkspaceTargets(cutoffMs), ]); const runLabel = new Date().toISOString().replace(/[:.]/g, '-'); const quarantineRunDir = path.join(options.quarantineDir, runLabel); const results = []; let totalBytes = 0; if (options.execute) { ensureDir(quarantineRunDir); } for (const target of discoveredTargets) { const bytes = measureSize(target.path); totalBytes += bytes; if (!options.execute) { results.push({ status: 'skipped', path: target.path, bytes, reason: target.reason, message: 'Dry-run: no changes were applied.', }); continue; } try { const destination = moveToQuarantine(target.path, quarantineRunDir); results.push({ status: 'quarantined', path: target.path, destination, bytes, reason: target.reason, }); } catch (error) { results.push({ status: 'failed', path: target.path, bytes, reason: target.reason, message: error?.message || String(error), }); } } let manifestPath = ''; if (options.execute) { manifestPath = writeQuarantineManifest(quarantineRunDir, results); } const summaryPath = writeSummary({ auditDir: currentAuditDir, execute: options.execute, scope: options.scope, cutoffMs, quarantineRunDir, targets: discoveredTargets, results, totalBytes, }); return { auditDir: currentAuditDir, cutoffIso: new Date(cutoffMs).toISOString(), targetsDiscovered: discoveredTargets.length, processed: results.filter((entry) => entry.status === 'quarantined').length, failed: results.filter((entry) => entry.status === 'failed').length, summaryPath, manifestPath, quarantineRunDir, }; } function main() { const options = parseArgs(process.argv.slice(2)); const result = runCleanup(options); console.log('Copilot history cleanup complete.'); console.log(`Audit directory: ${result.auditDir}`); console.log(`Completion cutoff: ${result.cutoffIso}`); console.log(`Targets discovered: ${result.targetsDiscovered}`); console.log(`Targets quarantined: ${result.processed}`); console.log(`Targets failed: ${result.failed}`); console.log(`Quarantine directory: ${result.quarantineRunDir}`); if (result.manifestPath) { console.log(`Manifest: ${result.manifestPath}`); } console.log(`Summary: ${result.summaryPath}`); } const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (isMainModule) { main(); }