// mp-sync 公共函数(Node 24 / Deno 2 均可运行,无第三方依赖) import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); export const LEDGER_PATH = join(ROOT, "scripts/mp-sync/ledger.json"); export const MANUAL_PATH = join(ROOT, "scripts/mp-sync/ledger.manual.json"); export const DOC_PATH = join(ROOT, "docs/porting/mp-sync-ledger.md"); export const DEFAULT_MP_REPO = "/opt/moviepilot-plus/upstream/MoviePilot"; /** 需要监视"新增文件"的上游目录(MP 开源站点框架) */ export const WATCH_DIRS = ["app/modules/indexer/spider", "app/modules/indexer/parser"]; /** 扫描 @mp-source 标签的本项目目录 */ export const SCAN_DIRS = ["packages/adapters/src", "packages/testkit/src"]; export function parseArgs(argv) { const out = { _: [] }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith("--")) { const k = a.slice(2); const v = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true; out[k] = v; } else out._.push(a); } return out; } export function git(repo, ...args) { return execFileSync("git", ["-C", repo, ...args], { encoding: "buffer", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], }); } export function tryGit(repo, ...args) { try { return git(repo, ...args); } catch { return null; } } export function sha256(buf) { return createHash("sha256").update(buf).digest("hex"); } export function resolveCommit(repo, ref) { const out = tryGit(repo, "rev-parse", "--verify", `${ref}^{commit}`); if (!out) throw new Error(`上游仓库 ${repo} 中找不到提交 ${ref}(浅克隆需先 git fetch --depth=1 origin )`); return out.toString().trim(); } /** 上游文件在某提交的内容 sha256;不存在返回 null */ export function upstreamHash(repo, commit, path) { const buf = tryGit(repo, "show", `${commit}:${path}`); return buf ? sha256(buf) : null; } export function listUpstreamDir(repo, commit, dir) { const out = tryGit(repo, "ls-tree", "--name-only", `${commit}:${dir}`); if (!out) return []; return out .toString() .split("\n") .filter((f) => f.endsWith(".py")) .map((f) => `${dir}/${f}`); } function walk(dir, acc) { for (const name of readdirSync(dir)) { if (name === "node_modules") continue; const p = join(dir, name); if (statSync(p).isDirectory()) walk(p, acc); else if (/\.(ts|mjs)$/.test(name)) acc.push(p); } return acc; } /** 扫描源码文件头 `@mp-source @ ` 标签 */ export function scanTags() { const tags = []; for (const d of SCAN_DIRS) { for (const file of walk(join(ROOT, d), [])) { const text = readFileSync(file, "utf8"); for (const m of text.matchAll(/@mp-source\s+(\S+)\s+@\s+([0-9a-f]{7,40})/g)) { tags.push({ upstream: m[1], commit: m[2], ours: relative(ROOT, file) }); } } } return tags; } /** 用仓库 biome 格式化(与 lint 一致,生成物重跑幂等);不可用时原样返回 */ export function biomeFormat(text, fakePath) { const bin = join(ROOT, "node_modules/.bin/biome"); if (!existsSync(bin)) return text; try { return execFileSync(bin, ["format", `--stdin-file-path=${fakePath}`], { input: text, encoding: "utf8", cwd: ROOT }); } catch { return text; } }