import { spawn } from "node:child_process"; function run(cmd, args, opts = {}) { return new Promise((resolve, reject) => { let out = ""; let err = ""; const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts }); child.stdout.on("data", (b) => (out += b)); child.stderr.on("data", (b) => (err += b)); child.on("error", reject); child.on("close", (code) => code === 0 ? resolve(out) : reject(new Error(`git ${args.join(" ")} exited ${code}: ${err.trim()}`)) ); }); } /** * Stage the given paths (relative to projectRoot) and commit if anything changed. * Best-effort: never throws, so a git hiccup can't block a live publish. Returns * the new commit hash, or null if nothing was staged / git failed. * Keeping the working tree committed is what lets the console's git worktree + * `merge --ff-only` operations run against a clean tree. */ export async function gitCommitPaths(projectRoot, paths, message) { try { await run("git", ["-C", projectRoot, "add", "--", ...paths]); const staged = await run("git", ["-C", projectRoot, "diff", "--cached", "--name-only"]); if (!staged.trim()) return null; await run("git", ["-C", projectRoot, "commit", "-q", "-m", message]); return (await run("git", ["-C", projectRoot, "rev-parse", "HEAD"])).trim(); } catch (e) { console.warn(`[git] commit skipped: ${e.message}`); return null; } }