feat: zcode thinking-mode 0.3.0(state历史+rollback+分写+校验收紧+34用例全绿+文档)
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
#!/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); }
|
||||
Reference in New Issue
Block a user