Add shared instruction files and copilot history maintenance scripts

- Add 12 cross-project instruction files to resources/instructions/
- Add cleanup-copilot-history.mjs/.sh: quarantine-based history cleanup with dry-run and aggressive scope
- Add restore-copilot-history.mjs/.sh: restore from quarantine by run, path, or filter
- Add purge-copilot-history-quarantine.mjs/.sh: purge old quarantine runs with keep-latest guard
- Add copilot-history-maintenance.test.mjs: 3 passing unit tests covering all three scripts
This commit is contained in:
2026-06-30 17:18:11 -04:00
parent 14b71eac0a
commit 71e89ca5c0
19 changed files with 1488 additions and 0 deletions
@@ -0,0 +1,510 @@
#!/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 <path> Audit directory used as completion boundary context.',
' --completion-cutoff <iso> Delete targets with mtime at or before this timestamp.',
' --scope <scope> Cleanup scope. Supported: aggressive. Default: aggressive',
' --quarantine-dir <path> Override quarantine root directory.',
' --repo-root <path> 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();
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
repo_root="$(cd -- "$script_dir/../.." && pwd -P)"
if command -v node >/dev/null 2>&1; then
node_bin="$(command -v node)"
elif command -v nodejs >/dev/null 2>&1; then
node_bin="$(command -v nodejs)"
else
printf 'Node.js is required to run Copilot history cleanup.\n' >&2
exit 1
fi
exec "$node_bin" "$script_dir/cleanup-copilot-history.mjs" --repo-root "$repo_root" "$@"
@@ -0,0 +1,150 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { runCleanup } from "./cleanup-copilot-history.mjs";
import { runRestore } from "./restore-copilot-history.mjs";
import { runPurge } from "./purge-copilot-history-quarantine.mjs";
function makeDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
return dirPath;
}
function writeFile(filePath, content = "x") {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, "utf8");
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
test("cleanup writes a quarantine manifest for exact restore targeting", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-history-cleanup-"));
const homeDir = path.join(tempRoot, "home");
const repoRoot = path.join(tempRoot, "repo");
const auditDir = makeDir(path.join(repoRoot, ".local", "audits", "forced-run"));
writeFile(path.join(auditDir, "selection-manifest.tsv"), "decision\treview_note\n");
writeFile(path.join(auditDir, "promotion-summary.md"), "summary\n");
const oldAuditDir = makeDir(path.join(repoRoot, ".local", "audits", "old-run"));
writeFile(path.join(oldAuditDir, "selection-manifest.tsv"), "decision\treview_note\n");
const workspaceArtifact = makeDir(
path.join(
homeDir,
"Library",
"Application Support",
"Code",
"User",
"workspaceStorage",
"abc123",
"chatSessions",
),
);
writeFile(path.join(workspaceArtifact, "session.jsonl"), "{}\n");
const previousHome = process.env.HOME;
process.env.HOME = homeDir;
try {
const result = runCleanup({
repoRoot,
auditDir,
completionCutoff: "2100-01-01T00:00:00.000Z",
scope: "aggressive",
quarantineDir: path.join(homeDir, ".copilot-resources-state", "quarantine", "copilot-history-cleanup"),
execute: true,
});
assert.equal(result.processed, 2);
assert.equal(Boolean(result.manifestPath), true);
const manifest = readJson(result.manifestPath);
assert.equal(manifest.rows.length, 2);
assert.deepEqual(
manifest.rows.map((row) => row.originalPath).sort(),
[
path.join(oldAuditDir),
path.join(homeDir, "Library", "Application Support", "Code", "User", "workspaceStorage", "abc123", "chatSessions"),
].sort(),
);
} finally {
process.env.HOME = previousHome;
}
});
test("restore exact path limits candidates to one original target", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-history-restore-"));
const quarantineDir = makeDir(path.join(tempRoot, "quarantine"));
const runDir = makeDir(path.join(quarantineDir, "2026-05-24T01-21-04-653Z"));
const firstTarget = makeDir(path.join(runDir, "Users", "fragginwagon", "Library", "Application Support", "Code", "User", "workspaceStorage", "aaa", "chatSessions"));
const secondTarget = makeDir(path.join(runDir, "Users", "fragginwagon", "Library", "Application Support", "Code", "User", "workspaceStorage", "bbb", "chatEditingSessions"));
writeFile(path.join(firstTarget, "session.jsonl"), "{}\n");
writeFile(path.join(secondTarget, "state.json"), "{}\n");
fs.writeFileSync(
path.join(runDir, "manifest.json"),
JSON.stringify(
{
createdAt: "2026-05-24T01:21:04.653Z",
rows: [
{
originalPath: "/Users/fragginwagon/Library/Application Support/Code/User/workspaceStorage/aaa/chatSessions",
quarantinePath: firstTarget,
},
{
originalPath: "/Users/fragginwagon/Library/Application Support/Code/User/workspaceStorage/bbb/chatEditingSessions",
quarantinePath: secondTarget,
},
],
},
null,
2,
),
);
const result = runRestore({
quarantineDir,
quarantineRun: runDir,
exactPath: "/Users/fragginwagon/Library/Application Support/Code/User/workspaceStorage/aaa/chatSessions",
filter: "",
overwrite: false,
execute: false,
});
assert.equal(result.candidates, 1);
const summary = fs.readFileSync(result.summaryPath, "utf8");
assert.match(summary, /workspaceStorage\/aaa\/chatSessions/);
assert.doesNotMatch(summary, /workspaceStorage\/bbb\/chatEditingSessions/);
});
test("purge keeps newest quarantine run and selects older ones before cutoff", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-history-purge-"));
const quarantineDir = makeDir(path.join(tempRoot, "quarantine"));
const oldRun = makeDir(path.join(quarantineDir, "2026-05-20T00-00-00-000Z"));
const newRun = makeDir(path.join(quarantineDir, "2026-05-24T00-00-00-000Z"));
writeFile(path.join(oldRun, "manifest.json"), "{}\n");
writeFile(path.join(newRun, "manifest.json"), "{}\n");
const oldTime = new Date("2026-05-20T00:00:00.000Z");
const newTime = new Date("2026-05-24T00:00:00.000Z");
fs.utimesSync(oldRun, oldTime, oldTime);
fs.utimesSync(newRun, newTime, newTime);
const result = runPurge({
quarantineDir,
before: "2026-05-23T00:00:00.000Z",
keepLatest: 1,
execute: false,
});
assert.equal(result.considered, 2);
assert.equal(result.purged, 0);
const summary = fs.readFileSync(result.summaryPath, "utf8");
assert.match(summary, /Protected by keep-latest/);
assert.match(summary, /Dry-run: no changes were applied/);
});
@@ -0,0 +1,242 @@
#!/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 <path> Override quarantine root directory.',
' --before <iso> Purge runs created before or at this timestamp.',
' --keep-latest <n> Keep the newest <n> 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();
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
if command -v node >/dev/null 2>&1; then
node_bin="$(command -v node)"
elif command -v nodejs >/dev/null 2>&1; then
node_bin="$(command -v nodejs)"
else
printf 'Node.js is required to purge Copilot history quarantine runs.\n' >&2
exit 1
fi
exec "$node_bin" "$script_dir/purge-copilot-history-quarantine.mjs" "$@"
@@ -0,0 +1,383 @@
#!/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 <path> Restore a specific quarantine run directory.',
' --quarantine-dir <path> Override quarantine root directory.',
' --path <absolute-path> Restore only one exact original path.',
' --filter <text> 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();
}
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
if command -v node >/dev/null 2>&1; then
node_bin="$(command -v node)"
elif command -v nodejs >/dev/null 2>&1; then
node_bin="$(command -v nodejs)"
else
printf 'Node.js is required to run Copilot history restore.\n' >&2
exit 1
fi
exec "$node_bin" "$script_dir/restore-copilot-history.mjs" "$@"