Files
zcode-tooling/zcode-env-sync/scripts/prune.mjs
T

129 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* /sync-prune:按保留策略清理远端旧快照。
* 用法: node prune.mjs [--keep N] [--apply] [--exclude <快照名>]...
*
* 默认 dry-run:只打印将要删除什么,不实际执行。加 --apply 才真删并推送。
* 永远保留:最新的一个快照、latest.json 指向的那个、以及 --exclude 指定的。
* 环境变量: ZCODE_SYNC_KEEP=N 保留数量(默认 10), ZCODE_SYNC_NO_PUSH=1 只本地删不推。
*/
import fs from "node:fs";
import path from "node:path";
import {
REPO_DIR, nowTag, readJson, writeJson, ensureRepo, gitPush,
listSnapshots, latestSnapshotName, humanSize,
} from "./sync-core.mjs";
function fail(e) { console.error(`清理失败:${e?.message ?? e}`);
if (process.env.DEBUG_PRUNE) console.error(e.stack);
process.exitCode = 1; }
/** 解析参数:--keep N / --apply / --exclude NAME / --help */
function parseArgs(argv) {
const out = { keep: Number(process.env.ZCODE_SYNC_KEEP ?? 10), apply: false, exclude: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--apply") out.apply = true;
else if (a === "--keep") {
const v = Number(argv[++i]);
if (!Number.isInteger(v) || v < 1) throw new Error(`--keep 需要 ≥1 的整数,收到:${argv[i]}`);
out.keep = v;
} else if (a.startsWith("--keep=")) {
const v = Number(a.slice(7));
if (!Number.isInteger(v) || v < 1) throw new Error(`--keep 需要 ≥1 的整数,收到:${a.slice(7)}`);
out.keep = v;
} else if (a === "--exclude") {
const v = argv[++i];
if (!v) throw new Error("--exclude 需要一个快照名");
out.exclude.push(v);
} else if (a.startsWith("--exclude=")) out.exclude.push(a.slice(10));
else if (a === "--help" || a === "-h") out.help = true;
else throw new Error(`未知参数:${a}(支持 --keep N / --apply / --exclude NAME)`);
}
return out;
}
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(`# /sync-prune 用法
node prune.mjs [--keep N] [--apply] [--exclude <快照名>]...
默认只预演(dry-run),不删任何东西;加 --apply 才真删并推送。
保留规则:最新一个 + latest.json 指向的 + --exclude 指定的,永远不动。
环境变量 ZCODE_SYNC_KEEP 可设默认保留数(当前 ${args.keep})。`);
process.exit(0);
}
ensureRepo();
const all = listSnapshots();
if (!all.length) {
console.log("# 远端尚无快照\n- 先 /sync-export 推第一版");
process.exit(0);
}
const latestName = latestSnapshotName();
const pinned = new Set(args.exclude);
if (latestName) pinned.add(latestName);
// 列表已是新→旧;按时间序决定保留
const keepSet = new Set();
const mustKeep = (s) => pinned.has(s.name);
const keepTarget = Math.max(args.keep, pinned.size);
for (const s of all) if (mustKeep(s)) keepSet.add(s.name);
for (const s of all) {
if (keepSet.size >= keepTarget) break;
keepSet.add(s.name);
}
const doomed = all.filter((s) => !keepSet.has(s.name));
const invalid = all.filter((s) => !s.valid);
const out = [
`# 快照清理${args.apply ? "" : "(预演,未删除)"}`,
`- 现有 ${all.length} 个,保留 ${keepSet.size} 个,${args.apply ? "删除" : "将删除"} ${doomed.length} 个`,
`- 保留策略:keep=${args.keep}${pinned.size ? `,钉住 ${[...pinned].join(", ")}` : ""}`,
];
if (invalid.length) out.push(`- ⚠ 其中 ${invalid.length} 个快照的 manifest 读不出来:${invalid.map((s) => s.name).join(", ")}`);
if (!doomed.length) {
out.push(`- 无需清理`);
} else {
out.push(`- ${args.apply ? "已删除" : "待删除"}:`);
for (const s of doomed) {
out.push(` ${s.name} ${humanSize(s.bytes)} ${s.createdAt ?? "?"}`
+ ` (官方${s.enabledCount} 自研${s.custom.length} MCP${s.mcpCount} 密钥${s.secretCount})`);
}
const freed = doomed.reduce((n, s) => n + s.bytes, 0);
out.push(`- ${args.apply ? "释放" : "将释放"}:${humanSize(freed)}`);
}
if (args.apply && doomed.length) {
const removed = [];
for (const s of doomed) {
try { fs.rmSync(s.dir, { recursive: true, force: true }); removed.push(s.name); }
catch (e) { out.push(`- ⚠ 删除失败 ${s.name}:${e.message}`); }
}
// latest.json 指向被删的快照时要重指,否则后续 /sync-status、/sync-import 会找不到
if (latestName && !keepSet.has(latestName)) {
const nxt = all.find((s) => keepSet.has(s.name) && s.valid);
if (nxt) {
writeJson(path.join(REPO_DIR, "latest.json"), {
snapshot: `snapshots/${nxt.name}`, host: nxt.host, createdAt: nxt.createdAt, bytes: nxt.bytes,
});
out.push(`- latest.json 已重指 → ${nxt.name}`);
}
}
out.push(`- 实际删除 ${removed.length} 个`);
const stamp = nowTag();
// 限在 snapshots/ 与 latest.json:删除动作需要 -A 才能被捕获,
// 但路径限定能挡住别的会话未推送的内容被顺带提交
out.push(`- 推送:${gitPush(`sync-prune ${stamp} (removed:${removed.length} kept:${keepSet.size})`,
{ paths: ["snapshots", "latest.json"] })}`);
} else if (!args.apply && doomed.length) {
out.push(`- 确认无误后重跑并加 --apply 才会真正删除`);
}
console.log(out.join("\n"));
} catch (e) { fail(e); }