UNPKG

sphica

Version:

Records Claude Code and Codex sessions on your machine and keeps past implementation and decisions, with their sources, for your agent to find.

393 lines (390 loc) • 12.9 kB
// server/src/git-worker.ts import { execFileSync as execFileSync2 } from "node:child_process"; import fs2 from "node:fs"; import os2 from "node:os"; import path2 from "node:path"; // server/src/git.ts import { execFileSync, spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; var gitHome = () => path.join(os.homedir(), ".sphica", "git"); function gitEnv(extra = {}) { const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))); return { ...env, GIT_ALLOW_PROTOCOL: "", GIT_NO_LAZY_FETCH: "1", GIT_TERMINAL_PROMPT: "0", GIT_NO_REPLACE_OBJECTS: "1", ...extra }; } var gitOptions = (hooks = path.join(gitHome(), "hooks")) => [ "-c", "core.fsmonitor=", "-c", `core.hooksPath=${hooks}`, "-c", "core.quotePath=false", "--no-pager", "--no-optional-locks" ]; var DIFF_OPTIONS = [ "--no-color", "--no-ext-diff", "--no-textconv", "--ignore-submodules=dirty", "--submodule=short" ]; function startDir() { const home = os.homedir(); return fs.statSync(home, { throwIfNoEntry: false })?.isDirectory() ? home : path.dirname(process.execPath); } function run(root, args, { max = 1024 * 1024, timeout = 1e4 } = {}) { return execFileSync("git", ["-C", root, ...gitOptions(), ...args], { cwd: startDir(), env: gitEnv(), maxBuffer: max, stdio: ["ignore", "pipe", "ignore"], timeout, killSignal: "SIGKILL" }); } var text = (root, args, limits) => run(root, args, limits).toString("utf8"); function revision(rev) { if (!rev || rev.startsWith("-") || /[\0\n]/.test(rev)) throw new Error(`not a revision: ${JSON.stringify(rev)}`); return rev; } var commitOf = (root, rev, limits) => text(root, ["rev-parse", "--verify", "-q", `${revision(rev)}^{commit}`], limits).trim(); var gitPath = (root, name, limits) => path.resolve(root, text(root, ["rev-parse", "--git-path", name], limits).replace(/\n$/, "")); var commonDir = (root, limits) => path.resolve(root, text(root, ["rev-parse", "--git-common-dir"], limits).replace(/\n$/, "")); function unbornHead(root, limits) { let ref; try { ref = text(root, ["symbolic-ref", "-q", "HEAD"], limits).trim(); } catch { return false; } try { run(root, ["show-ref", "--verify", "-q", "--", revision(ref)], limits); return false; } catch (e) { return e.status === 1; } } var SAFE_KEYS = { "core.ignorecase": "bool", "core.precomposeunicode": "bool", "core.filemode": "bool", "core.symlinks": "bool", "core.trustctime": "bool", "core.sparseCheckout": "bool", "core.sparseCheckoutCone": "bool", "index.sparse": "bool", "core.autocrlf": "bool-or-input", "core.eol": "text", "core.checkStat": "text", "extensions.objectFormat": "text", "core.excludesFile": "path" }; function configGet(root, key, limits) { const type = SAFE_KEYS[key]; if (type === "bool-or-input") try { return text(root, ["config", "--type=bool", "--get", key], limits).replace(/\n$/, ""); } catch {} try { return text(root, ["config", ...type === "bool" || type === "path" ? [`--type=${type}`] : [], "--get", key], limits).replace(/\n$/, ""); } catch { return null; } } // server/src/git-worker.ts class Refused extends Error { code; constructor(code) { super(code); this.code = code; } } var INDEX_LIMIT = 256 * 1024 * 1024; var FILE_LIMIT = 1024 * 1024; var STALE_MS = 60 * 60 * 1000; var inside = (root, p) => { const rel = path2.relative(root, p); return rel === "" || rel !== ".." && !rel.startsWith(`..${path2.sep}`) && !path2.isAbsolute(rel); }; var overlap = (a, b) => inside(a, b) || inside(b, a); function ownDir(parent, name) { const dir = path2.join(parent, name); let st = fs2.lstatSync(dir, { throwIfNoEntry: false }); if (!st) { fs2.mkdirSync(dir, { mode: 448 }); st = fs2.lstatSync(dir); } if (!st.isDirectory() || st.isSymbolicLink()) throw new Refused("not_plain_dir"); return dir; } function isolatedHome(root) { const home = fs2.realpathSync(os2.homedir()); const git = ownDir(ownDir(home, ".sphica"), "git"); const hooks = ownDir(git, "hooks"); if (fs2.readdirSync(hooks).length) throw new Refused("hooks_not_empty"); const iso = fs2.mkdtempSync(path2.join(git, "iso-")); try { const st = fs2.lstatSync(iso); if (!st.isDirectory() || st.isSymbolicLink()) throw new Refused("not_plain_dir"); const real = fs2.realpathSync(iso); if (!inside(home, real) || real !== iso) throw new Refused("outside_home"); for (const [other, code] of [ [root, "overlaps_worktree"], [os2.tmpdir(), "overlaps_tmp"] ]) { const otherReal = fs2.realpathSync(other); if (overlap(real, otherReal) || overlap(git, otherReal)) throw new Refused(code); } } catch (e) { fs2.rmSync(iso, { recursive: true, force: true }); throw e; } return { git, iso, hooks }; } function sweep(git, now = Date.now()) { let names; try { names = fs2.readdirSync(git).filter((n) => n.startsWith("iso-")); } catch { return; } for (const name of names) { const dir = path2.join(git, name); try { const st = fs2.lstatSync(dir); if (st.isDirectory() && !st.isSymbolicLink() && now - st.mtimeMs > STALE_MS) fs2.rmSync(dir, { recursive: true, force: true }); } catch {} } } var NO_FOLLOW = fs2.constants.O_NOFOLLOW ?? 0; var NON_BLOCK = fs2.constants.O_NONBLOCK ?? 0; function readLimited(file, limit) { return readStamped(file, limit)?.bytes ?? null; } function readStamped(file, limit) { let before; if (!NO_FOLLOW) { before = fs2.lstatSync(file, { throwIfNoEntry: false }); if (!before) return null; if (!before.isFile()) throw new Error(`${file} is not a regular file`); } let fd; try { fd = fs2.openSync(file, fs2.constants.O_RDONLY | NO_FOLLOW | NON_BLOCK); } catch (e) { if (e.code === "ENOENT") return null; throw new Error(`${file} cannot be opened as a regular file`); } try { const st = fs2.fstatSync(fd); if (!st.isFile()) throw new Error(`${file} is not a regular file`); if (before && (st.dev !== before.dev || st.ino !== before.ino)) throw new Error(`${file} changed while opened`); if (st.size > limit) throw new Error(`${file} is over ${limit} bytes`); const buf = Buffer.alloc(limit + 1); let got = 0; for (;; ) { const n = fs2.readSync(fd, buf, got, buf.length - got, null); if (n === 0) break; got += n; if (got > limit) throw new Error(`${file} is over ${limit} bytes`); } return { bytes: buf.subarray(0, got), mtime: st.mtime }; } finally { fs2.closeSync(fd); } } var normalized = (value) => value.trim().toLowerCase(); function isolatedConfig(values) { const lines = ["[core]", "\tbare = false", "\tquotePath = false"]; const extensions = []; const allowed = { "core.autocrlf": ["true", "false", "input"], "core.eol": ["lf", "crlf", "native"], "core.checkStat": ["default", "minimal"], "extensions.objectFormat": ["sha1", "sha256"] }; const sections = { core: lines, index: [] }; for (const [key, type] of Object.entries(SAFE_KEYS)) { const value = values[key]; if (value === null || value === undefined || type === "path") continue; const normal = normalized(value); const ok = type === "bool" ? ["true", "false"] : allowed[key] ?? []; if (!ok.includes(normal)) throw new Error(`${key} has a value Sphica does not copy: ${JSON.stringify(value.slice(0, 40))}`); const [section, name] = key.split("."); if (section === "extensions") extensions.push(` ${name} = ${normal}`); else sections[section]?.push(` ${name} = ${normal}`); } const sha256 = values["extensions.objectFormat"] === "sha256"; lines.splice(1, 0, ` repositoryformatversion = ${sha256 ? 1 : 0}`); return [ ...lines, ...sections.index?.length ? ["[index]", ...sections.index] : [], ...sha256 ? ["[extensions]", ...extensions] : [], "" ].join(` `); } function excludesFile(root, limits) { const set = configGet(root, "core.excludesFile", limits); if (set === "") return null; if (set) return set.startsWith("~/") ? path2.join(os2.homedir(), set.slice(2)) : path2.resolve(root, set); const xdg = process.env.XDG_CONFIG_HOME; return path2.join(xdg && path2.isAbsolute(xdg) ? xdg : path2.join(os2.homedir(), ".config"), "git", "ignore"); } function prepare(root, iso, hooks, remaining) { for (const dir of ["refs", "objects", "info"]) fs2.mkdirSync(path2.join(iso, dir)); let head; try { head = commitOf(root, "HEAD", remaining()); } catch (e) { if (!unbornHead(root, remaining())) throw e; head = "ref: refs/heads/sphica-unborn"; } fs2.writeFileSync(path2.join(iso, "HEAD"), `${head} `); const values = Object.fromEntries(Object.keys(SAFE_KEYS).map((k) => [k, configGet(root, k, remaining())])); fs2.writeFileSync(path2.join(iso, "config"), isolatedConfig(values)); const index = gitPath(root, "index", remaining()); const copyIndex = (from, name) => { const got = readStamped(from, INDEX_LIMIT); if (!got) return; fs2.writeFileSync(path2.join(iso, name), got.bytes); fs2.utimesSync(path2.join(iso, name), got.mtime, got.mtime); }; copyIndex(index, "index"); for (const name of fs2.readdirSync(path2.dirname(index)).filter((n) => /^sharedindex\.[0-9a-f]{40,64}$/.test(n))) copyIndex(path2.join(path2.dirname(index), name), name); for (const name of ["info/exclude", "info/attributes", "info/sparse-checkout"]) { const bytes = readLimited(gitPath(root, name, remaining()), FILE_LIMIT); if (bytes) fs2.writeFileSync(path2.join(iso, name), bytes); } const ignore = path2.join(iso, "global-ignore"); const global = excludesFile(root, remaining()); fs2.writeFileSync(ignore, (global && readLimited(global, FILE_LIMIT)) ?? ""); return { env: gitEnv({ GIT_DIR: iso, GIT_WORK_TREE: root, GIT_OBJECT_DIRECTORY: path2.join(commonDir(root, remaining()), "objects"), GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" }), options: [...gitOptions(hooks), "-c", `core.excludesFile=${ignore}`] }; } function argsOf(op) { const oid = (s) => { if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(s)) throw new Error(`not an object id: ${JSON.stringify(s)}`); return s; }; if (op.kind === "status") return ["status", "--porcelain=v2", "-z", "--untracked-files=all", "--ignore-submodules=dirty"]; if (op.kind === "diff") return [ "diff", ...DIFF_OPTIONS, "--no-renames", "--src-prefix=a/", "--dst-prefix=b/", oid(op.from), "--" ]; return ["diff", ...DIFF_OPTIONS, "-M", "-l1000", "--name-status", "-z", oid(op.commit), "--"]; } function work(request) { try { const { git, iso, hooks } = isolatedHome(request.root); sweep(git); const remaining = () => ({ timeout: Math.max(1, Math.floor(request.until - Date.now())) }); try { if (request.ops.every((op) => op.kind === "place")) return { ok: true, out: [] }; const { env, options } = prepare(request.root, iso, hooks, remaining); const out = request.ops.flatMap((op) => { if (op.kind === "place") return []; const run2 = (args) => execFileSync2("git", ["-C", request.root, ...options, ...args], { cwd: startDir(), env, ...remaining(), killSignal: "SIGKILL", encoding: "utf8", maxBuffer: request.max, stdio: ["ignore", "pipe", "ignore"] }); if (op.kind === "diff") return [ run2(argsOf(op)), run2(["diff", ...DIFF_OPTIONS, "--no-renames", "--name-only", "-z", op.from, "--"]) ]; return [run2(argsOf(op))]; }); return { ok: true, out }; } finally { fs2.rmSync(iso, { recursive: true, force: true }); } } catch (e) { if (e instanceof Refused) return { ok: false, refused: e.code }; const code = e.code; return { ok: false, failed: code === "ETIMEDOUT" ? "timeout" : code === "ENOBUFS" ? "too_large" : "failed" }; } } if (process.argv[1] && path2.resolve(process.argv[1]) === path2.resolve(import.meta.filename ?? "")) { const chunks = []; process.stdin.on("data", (c) => chunks.push(c)); process.stdin.on("end", () => { let request; try { request = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { process.stdout.write(JSON.stringify({ ok: false, failed: "failed" })); return; } process.stdout.write(JSON.stringify(work(request))); }); } export { isolatedConfig, isolatedHome, readLimited, sweep };