termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
176 lines (175 loc) • 6.93 kB
JavaScript
import { spawnSync, spawn } from "node:child_process";
import path from "node:path";
function runGit(args, cwd) {
try {
const res = spawnSync("git", args, {
cwd,
encoding: "utf8",
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
timeout: 30000 // 30 second timeout
});
if (res.error) {
// Handle ENOBUFS specifically
if ('code' in res.error && res.error.code === 'ENOBUFS') {
return { ok: false, error: "Git output too large. Try cleaning up your repository or using .gitignore." };
}
return { ok: false, error: res.error.message };
}
if (res.status !== 0) {
const errorMsg = res.stderr?.trim() || "Git command failed";
return { ok: false, error: errorMsg };
}
return { ok: true, data: res.stdout?.trim() || "" };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown git error";
return { ok: false, error: errorMsg };
}
}
async function runGitAsync(args, cwd) {
return new Promise((resolve) => {
const child = spawn("git", args, { cwd });
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
if (code === 0) {
resolve({ ok: true, data: stdout.trim() });
}
else {
resolve({ ok: false, error: stderr.trim() || `Git exited with code ${code}` });
}
});
child.on("error", (error) => {
resolve({ ok: false, error: error.message });
});
// Set timeout
const timeout = setTimeout(() => {
child.kill();
resolve({ ok: false, error: "Git command timeout" });
}, 30000);
child.on("close", () => {
clearTimeout(timeout);
});
});
}
export async function ensureCleanGit(cwd) {
try {
// First check if we're in a git repo
const isRepo = runGit(["rev-parse", "--git-dir"], cwd);
if (!isRepo.ok) {
return { ok: false, error: `Not a git repository: ${cwd}. Initialize with 'git init' first.` };
}
// Use async for potentially large status output
const status = await runGitAsync(["status", "--porcelain"], cwd);
if (!status.ok) {
// If still failing, try a more lightweight check
const fallbackStatus = runGit(["status", "--porcelain", "--untracked-files=no"], cwd);
if (!fallbackStatus.ok) {
return { ok: false, error: `Git status check failed: ${fallbackStatus.error}` };
}
if (fallbackStatus.data) {
return { ok: false, error: "Uncommitted changes present. Commit or stash changes first." };
}
return { ok: true, data: undefined };
}
if (status.data) {
const lines = status.data.split('\n').filter(Boolean);
// Filter out nested git repositories (submodules or independent repos)
const relevantChanges = lines.filter(line => {
// Skip changes that are in nested git directories
const filePath = line.slice(3); // Remove status prefix like "M "
// Check if this file is in a nested git repository
const potentialGitDir = path.join(cwd, filePath);
const nestedRepoCheck = runGit(["rev-parse", "--git-dir"], potentialGitDir);
// If it's a git directory itself, skip it
if (nestedRepoCheck.ok) {
return false;
}
// Check if this file is inside a nested git repository
const parentDir = path.dirname(path.join(cwd, filePath));
const parentRepoCheck = runGit(["rev-parse", "--git-dir"], parentDir);
if (parentRepoCheck.ok) {
// This file is in a nested repo, check if that repo is different from current
const currentGitDir = runGit(["rev-parse", "--git-dir"], cwd);
if (currentGitDir.ok && parentRepoCheck.data !== currentGitDir.data) {
return false; // Skip files in different nested repos
}
}
return true;
});
// If no relevant changes after filtering, we're clean
if (relevantChanges.length === 0) {
return { ok: true, data: undefined };
}
// If there are too many files, just show a summary
if (relevantChanges.length > 50) {
return {
ok: false,
error: `${relevantChanges.length} uncommitted changes present. Commit or stash changes first.`
};
}
const summary = relevantChanges.length > 3 ?
`${relevantChanges.slice(0, 3).join(', ')} and ${relevantChanges.length - 3} more files` :
relevantChanges.join(', ');
return {
ok: false,
error: `Uncommitted changes present: ${summary}. Commit or stash changes first.`
};
}
return { ok: true, data: undefined };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown git error";
return { ok: false, error: errorMsg };
}
}
export function createBranch(cwd, name) {
return runGit(["checkout", "-b", name], cwd);
}
export function addAll(cwd) {
return runGit(["add", "-A"], cwd);
}
export function commitAll(cwd, message) {
return runGit(["commit", "-m", message], cwd);
}
export function commitWithMessage(cwd, message) {
// Add all changes first
const addResult = addAll(cwd);
if (!addResult.ok)
return addResult;
// Then commit
return commitAll(cwd, message);
}
export function getUnstagedChanges(cwd) {
return runGit(["diff", "--name-only"], cwd);
}
export function getStagedChanges(cwd) {
return runGit(["diff", "--cached", "--name-only"], cwd);
}
export function getAllChanges(cwd) {
return runGit(["diff", "--name-only", "HEAD"], cwd);
}
export function getDiffSummary(cwd) {
return runGit(["diff", "--stat"], cwd);
}
export function getDiffContent(cwd, file) {
const args = ["diff"];
if (file)
args.push(file);
return runGit(args, cwd);
}
export function checkoutBranch(cwd, name) {
return runGit(["checkout", name], cwd);
}
export function deleteBranch(cwd, name) {
return runGit(["branch", "-D", name], cwd);
}
export function mergeBranch(cwd, name) {
return runGit(["merge", name], cwd);
}