#!/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: restore-copilot-history.mjs [options]', '', 'Options:', ' --quarantine-run Restore a specific quarantine run directory.', ' --quarantine-dir Override quarantine root directory.', ' --path Restore only one exact original path.', ' --filter Restore only entries whose original path contains the text.', ' --overwrite Replace existing destination paths during restore.', ' --execute Perform restore. Default is dry-run.', ' --help Show this help text.', ].join('\n'), ); } function parseArgs(argv) { const options = { quarantineRun: '', quarantineDir: path.join( os.homedir(), '.copilot-resources-state', 'quarantine', 'copilot-history-cleanup', ), exactPath: '', filter: '', overwrite: false, 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-run') { options.quarantineRun = path.resolve(argv[index + 1] ?? ''); index += 1; continue; } if (arg === '--quarantine-dir') { options.quarantineDir = path.resolve(argv[index + 1] ?? options.quarantineDir); index += 1; continue; } if (arg === '--path') { options.exactPath = path.resolve(argv[index + 1] ?? ''); index += 1; continue; } if (arg === '--filter') { options.filter = argv[index + 1] ?? ''; index += 1; continue; } if (arg === '--overwrite') { options.overwrite = true; continue; } if (arg === '--execute') { options.execute = true; continue; } throw new Error(`Unknown argument: ${arg}`); } 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 findLatestQuarantineRun(quarantineRoot) { let latest = null; for (const runDir of listDirs(quarantineRoot)) { const stat = safeStat(runDir); if (!stat) { continue; } if (!latest || stat.mtimeMs > latest.mtimeMs) { latest = { path: runDir, mtimeMs: stat.mtimeMs }; } } return latest?.path ?? null; } 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 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 readManifestCandidates(rootDir) { const manifestPath = path.join(rootDir, 'manifest.json'); if (!safeStat(manifestPath)) { return []; } const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); return Array.isArray(manifest?.rows) ? manifest.rows.map((row) => ({ quarantinePath: path.resolve(row.quarantinePath), originalPath: path.resolve(row.originalPath), })) : []; } function isRestoreTargetDirectory(dirPath) { const baseName = path.basename(dirPath); if (['chatSessions', 'chatEditingSessions', 'transcripts', 'chat-session-resources'].includes(baseName)) { return true; } return Boolean(safeStat(path.join(dirPath, 'selection-manifest.tsv'))); } function readHeuristicCandidates(rootDir) { const candidates = []; function walk(currentDir) { for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) { const entryPath = path.join(currentDir, entry.name); if (!entry.isDirectory()) { continue; } if (isRestoreTargetDirectory(entryPath)) { const relativePath = path.relative(rootDir, entryPath); candidates.push({ quarantinePath: entryPath, originalPath: path.join(path.parse(rootDir).root, relativePath), }); continue; } walk(entryPath); } } walk(rootDir); return candidates; } function collectRestoreCandidates(rootDir, filterText, exactPath) { const manifestCandidates = readManifestCandidates(rootDir); const baseCandidates = manifestCandidates.length > 0 ? manifestCandidates : readHeuristicCandidates(rootDir); const uniqueCandidates = []; const seenPaths = new Set(); for (const candidate of baseCandidates) { if (seenPaths.has(candidate.originalPath)) { continue; } seenPaths.add(candidate.originalPath); uniqueCandidates.push(candidate); } let filtered = uniqueCandidates; if (exactPath) { filtered = filtered.filter((candidate) => candidate.originalPath === exactPath); } if (filterText) { filtered = filtered.filter((candidate) => candidate.originalPath.includes(filterText)); } return filtered.sort((left, right) => left.originalPath.localeCompare(right.originalPath)); } function movePath(sourcePath, destinationPath, overwrite) { if (safeStat(destinationPath)) { if (!overwrite) { return { status: 'skipped', message: 'Destination already exists.' }; } fs.rmSync(destinationPath, { recursive: true, force: true }); } ensureDir(path.dirname(destinationPath)); try { fs.renameSync(sourcePath, destinationPath); } catch (error) { if (error?.code !== 'EXDEV') { throw error; } fs.cpSync(sourcePath, destinationPath, { recursive: true }); fs.rmSync(sourcePath, { recursive: true, force: true }); } return { status: 'restored', message: '' }; } function pruneEmptyParents(startDir, stopDir) { let currentDir = path.dirname(startDir); while (currentDir.startsWith(stopDir) && currentDir !== stopDir) { const entries = fs.readdirSync(currentDir); if (entries.length > 0) { break; } fs.rmdirSync(currentDir); currentDir = path.dirname(currentDir); } } function writeSummary(runDir, execute, overwrite, results, totalBytes) { const summaryPath = path.join(runDir, 'restore-summary.md'); const lines = [ '# Copilot History Restore Summary', '', `- Generated: ${new Date().toISOString()}`, `- Mode: ${execute ? 'execute' : 'dry-run'}`, `- Overwrite existing destinations: ${overwrite ? 'yes' : 'no'}`, `- Entries considered: ${results.length}`, `- Entries restored: ${results.filter((entry) => entry.status === 'restored').length}`, `- Entries skipped: ${results.filter((entry) => entry.status === 'skipped').length}`, `- Entries 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.originalPath}${sizeText}`); lines.push(` - Quarantine path: ${result.quarantinePath}`); if (result.message) { lines.push(` - ${result.message}`); } } } fs.writeFileSync(summaryPath, `${lines.join('\n')}\n`, 'utf8'); return summaryPath; } export function runRestore(rawOptions) { const options = { ...rawOptions, quarantineDir: path.resolve(rawOptions.quarantineDir), quarantineRun: rawOptions.quarantineRun ? path.resolve(rawOptions.quarantineRun) : '', }; const runDir = options.quarantineRun || findLatestQuarantineRun(options.quarantineDir); if (!runDir) { throw new Error('No quarantine run was found. Pass --quarantine-run or create one with cleanup first.'); } const candidates = collectRestoreCandidates(runDir, options.filter, options.exactPath); const results = []; let totalBytes = 0; for (const candidate of candidates) { const bytes = measureSize(candidate.quarantinePath); totalBytes += bytes; if (!options.execute) { results.push({ status: 'skipped', quarantinePath: candidate.quarantinePath, originalPath: candidate.originalPath, bytes, message: 'Dry-run: no changes were applied.', }); continue; } try { const moveResult = movePath(candidate.quarantinePath, candidate.originalPath, options.overwrite); results.push({ status: moveResult.status, quarantinePath: candidate.quarantinePath, originalPath: candidate.originalPath, bytes, message: moveResult.message, }); if (moveResult.status === 'restored') { pruneEmptyParents(candidate.quarantinePath, runDir); } } catch (error) { results.push({ status: 'failed', quarantinePath: candidate.quarantinePath, originalPath: candidate.originalPath, bytes, message: error?.message || String(error), }); } } const summaryPath = writeSummary(runDir, options.execute, options.overwrite, results, totalBytes); return { runDir, candidates: results.length, restored: results.filter((entry) => entry.status === 'restored').length, failed: results.filter((entry) => entry.status === 'failed').length, summaryPath, }; } function main() { const options = parseArgs(process.argv.slice(2)); const result = runRestore(options); console.log('Copilot history restore complete.'); console.log(`Quarantine run: ${result.runDir}`); console.log(`Entries considered: ${result.candidates}`); console.log(`Entries restored: ${result.restored}`); console.log(`Entries 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(); }