import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; import { getPresetsDir } from "../config/user-data-dir.js"; import type { PresetPackage } from "../types/preset.js"; /** * 预设版本树:每个预设一个 JSON 文件,节点是完整快照。 * - 保存:在指定父节点(缺省为当前节点)下追加子节点; * - 分叉:父节点选历史中的任意一个,形成新分支,原分支不动; * - 追溯:沿 parentId 往上走即为修改历史。 * 不依赖 git,不做远程同步。 */ export type PresetVersionNode = { id: string; parentId: string | null; createdAt: string; /** 被「保存至当前版本」覆盖过的时间;没改过就不写 */ updatedAt?: string; note: string; preset: PresetPackage; }; export type PresetVersionTree = { presetId: string; currentId: string | null; nodes: PresetVersionNode[]; }; const SAFE_ID = /^[\w\u4e00-\u9fff-]+$/; function versionsDir(): string { return path.join(getPresetsDir(), "versions"); } function treeFile(presetId: string): string { if (!SAFE_ID.test(presetId)) throw new Error(`非法预设 id:${presetId}`); return path.join(versionsDir(), `${presetId}.json`); } function writeTree(tree: PresetVersionTree): void { mkdirSync(versionsDir(), { recursive: true }); writeFileSync(treeFile(tree.presetId), JSON.stringify(tree, null, 2), "utf8"); } export function loadVersionTree(presetId: string): PresetVersionTree { const file = treeFile(presetId); if (!existsSync(file)) return { presetId, currentId: null, nodes: [] }; return JSON.parse(readFileSync(file, "utf8")) as PresetVersionTree; } export function getVersionNode( presetId: string, nodeId: string, ): PresetVersionNode | null { return loadVersionTree(presetId).nodes.find((n) => n.id === nodeId) ?? null; } /** 从根到指定节点的路径(含两端),即该版本的修改历史。 */ export function versionLineage( tree: PresetVersionTree, nodeId: string, ): PresetVersionNode[] { const byId = new Map(tree.nodes.map((n) => [n.id, n])); const out: PresetVersionNode[] = []; let cursor = byId.get(nodeId) ?? null; const guard = new Set(); while (cursor && !guard.has(cursor.id)) { guard.add(cursor.id); out.unshift(cursor); cursor = cursor.parentId ? byId.get(cursor.parentId) ?? null : null; } return out; } /** * 提交一个版本。 * parentId 缺省为当前节点;显式传入历史节点即为分叉。 * 树为空时 parentId 必须为 null(建根);已有根时不允许再建第二个根。 */ export function commitVersion(input: { presetId: string; preset: PresetPackage; note?: string; parentId?: string | null; }): PresetVersionTree { const tree = loadVersionTree(input.presetId); const parentId = input.parentId !== undefined ? input.parentId : tree.currentId; if (parentId === null) { if (tree.nodes.length > 0) throw new Error("已有版本,不能再新建根版本"); } else if (!tree.nodes.some((n) => n.id === parentId)) { throw new Error(`父版本不存在:${parentId}`); } let id = randomUUID().slice(0, 8); while (tree.nodes.some((n) => n.id === id)) id = randomUUID().slice(0, 8); const node: PresetVersionNode = { id, parentId, createdAt: new Date().toISOString(), note: (input.note ?? "").trim(), preset: structuredClone(input.preset), }; tree.nodes.push(node); tree.currentId = node.id; writeTree(tree); return tree; } /** * 保存到当前版本(amend):把快照直接换掉 `currentId` 那个节点,不产生新节点。 * 还没有任何版本时退化成建根。 */ export function amendCurrentVersion(input: { presetId: string; preset: PresetPackage; note?: string; }): PresetVersionTree { const tree = loadVersionTree(input.presetId); if (!tree.currentId) return commitVersion({ ...input, parentId: null }); const node = tree.nodes.find((item) => item.id === tree.currentId); if (!node) throw new Error(`当前版本不存在:${tree.currentId}`); node.preset = structuredClone(input.preset); const note = (input.note ?? "").trim(); if (note) node.note = note; node.updatedAt = new Date().toISOString(); writeTree(tree); return tree; } /** 切换当前编辑位置(不产生新节点)。 */ export function checkoutVersion(presetId: string, nodeId: string): PresetVersionTree { const tree = loadVersionTree(presetId); if (!tree.nodes.some((n) => n.id === nodeId)) { throw new Error(`版本不存在:${nodeId}`); } tree.currentId = nodeId; writeTree(tree); return tree; } export function deleteVersionTree(presetId: string): void { try { rmSync(treeFile(presetId), { force: true }); } catch { /* ignore */ } }