@sema-agent/core
Version:
Stateless, task-oriented AI agent core
76 lines (75 loc) • 3.09 kB
JavaScript
import { randomUUID } from "node:crypto";
import { thrownLabel, thrownText } from "./thrown-value.js";
export const WORKTREE_PARENT = ".sema-worktrees";
function shq(s) {
return `'${s.replace(/'/g, "'\\''")}'`;
}
async function removeWorktree(baseEnv, repoRoot, worktreeDir) {
let detail = "";
for (let attempt = 0; attempt < 2; attempt++) {
try {
const r = await baseEnv.exec(`git worktree remove --force ${shq(worktreeDir)}`, { cwd: repoRoot });
if (r.ok && r.value.exitCode === 0)
return { ok: true };
detail = r.ok ? r.value.stderr || r.value.stdout || `exit ${r.value.exitCode}` : thrownText(r.error);
}
catch (err) {
detail = thrownLabel(err);
}
}
return { ok: false, detail };
}
export async function addWorktree(baseEnv, opts) {
const safe = (opts.sessionId || "task").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
const worktreeDir = `${opts.repoRoot}/${WORKTREE_PARENT}/${safe}-${randomUUID().slice(0, 8)}`;
const add = await baseEnv.exec(`git worktree add --detach ${shq(worktreeDir)} ${shq(opts.commit ?? "HEAD")}`, {
cwd: opts.repoRoot,
});
if (!add.ok || add.value.exitCode !== 0) {
const detail = add.ok ? add.value.stderr || add.value.stdout : thrownText(add.error);
throw new Error(`git worktree add failed: ${detail}`);
}
const excludeLine = shq(`/${WORKTREE_PARENT}/`);
await baseEnv
.exec(`ex="$(git rev-parse --git-path info/exclude)" && mkdir -p "$(dirname "$ex")" && ` +
`{ grep -qxF ${excludeLine} "$ex" 2>/dev/null || printf '%s\\n' ${excludeLine} >> "$ex"; }`, { cwd: opts.repoRoot })
.catch(() => undefined);
let inner;
try {
inner = await opts.rootEnvAt(worktreeDir);
}
catch (err) {
await removeWorktree(baseEnv, opts.repoRoot, worktreeDir);
throw err;
}
const innerDestroy = inner.destroy;
const env = inner;
env.destroy = async () => {
try {
if (typeof innerDestroy === "function")
await innerDestroy.call(inner);
}
finally {
const removed = await removeWorktree(baseEnv, opts.repoRoot, worktreeDir);
if (!removed.ok) {
throw new Error(`git worktree remove failed twice — ${worktreeDir} is now an orphan (dir + registration remain; ` +
`\`git worktree prune\` cannot reclaim it while the dir exists — remove it manually): ${removed.detail}`);
}
}
};
return { env, worktreeDir };
}
export async function pruneWorktrees(baseEnv, repoRoot) {
try {
const r = await baseEnv.exec("git worktree prune", { cwd: repoRoot });
if (r.ok && r.value.exitCode === 0)
return { ok: true };
return {
ok: false,
detail: r.ok ? (r.value.stderr || r.value.stdout || `exit ${r.value.exitCode}`).trim() : thrownText(r.error),
};
}
catch (err) {
return { ok: false, detail: thrownLabel(err) };
}
}