UNPKG

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
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); }