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
JavaScript
// 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
};