codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
1,184 lines (1,038 loc) • 32.7 kB
text/typescript
import { MCPServer } from '../core/mcp-server-manager.js';
// Simple base class for MCP servers to maintain compatibility
class BaseMCPServer {
protected tools: Record<string, (...args: any[]) => any> = {};
constructor(
public id: string,
public description: string
) {}
}
import { ApprovalManager, Operation, OperationContext } from '../core/approval/approval-manager.js';
import { logger } from '../core/logger.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs/promises';
const execAsync = promisify(exec);
export interface GitStatus {
branch: string;
staged: string[];
modified: string[];
untracked: string[];
ahead: number;
behind: number;
clean: boolean;
}
export interface GitCommitInfo {
hash: string;
author: string;
date: string;
message: string;
files: string[];
}
export interface GitDiffResult {
file: string;
additions: number;
deletions: number;
patch: string;
}
export interface CommitArgs {
message: string;
files?: string[];
amend?: boolean;
signoff?: boolean;
}
export interface BranchArgs {
name: string;
checkout?: boolean;
delete?: boolean;
remote?: string;
}
export interface MergeArgs {
branch: string;
strategy?: 'merge' | 'rebase' | 'squash';
noFastForward?: boolean;
}
export interface TagArgs {
name: string;
message?: string;
delete?: boolean;
push?: boolean;
}
export interface ToolResult {
success: boolean;
data?: any;
error?: string;
warning?: string;
suggestion?: string;
}
/**
* Git operations MCP server with approval workflow integration
* Provides safe Git operations with user confirmation for destructive actions
*/
export class GitMCPServer extends BaseMCPServer {
private approvalManager: ApprovalManager;
private workspaceRoot: string;
constructor(approvalManager: ApprovalManager, workspaceRoot: string) {
super('git-operations', 'Git version control operations with safety checks');
this.approvalManager = approvalManager;
this.workspaceRoot = workspaceRoot;
// Register tool handlers
this.tools = {
git_status: this.handleGitStatus.bind(this),
git_diff: this.handleGitDiff.bind(this),
git_log: this.handleGitLog.bind(this),
git_add: this.handleGitAdd.bind(this),
git_commit: this.handleGitCommit.bind(this),
git_push: this.handleGitPush.bind(this),
git_pull: this.handleGitPull.bind(this),
git_branch: this.handleGitBranch.bind(this),
git_checkout: this.handleGitCheckout.bind(this),
git_merge: this.handleGitMerge.bind(this),
git_rebase: this.handleGitRebase.bind(this),
git_reset: this.handleGitReset.bind(this),
git_tag: this.handleGitTag.bind(this),
git_remote: this.handleGitRemote.bind(this),
git_stash: this.handleGitStash.bind(this),
git_clean: this.handleGitClean.bind(this),
};
logger.info('Git MCP server initialized', { workspaceRoot });
}
/**
* Get Git repository status
*/
async handleGitStatus(): Promise<ToolResult> {
try {
const isRepo = await this.isGitRepository();
if (!isRepo) {
return {
success: false,
error: 'Not a Git repository',
};
}
const status = await this.getGitStatus();
return {
success: true,
data: status,
};
} catch (error) {
logger.error('Git status failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git status failed',
};
}
}
/**
* Get Git diff
*/
async handleGitDiff(args: {
files?: string[];
staged?: boolean;
commit?: string;
}): Promise<ToolResult> {
try {
const { files = [], staged = false, commit } = args;
let command = 'git diff';
if (staged) command += ' --cached';
if (commit) command += ` ${commit}`;
if (files.length > 0) command += ` -- ${files.join(' ')}`;
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
// Parse diff output
const diffs = this.parseDiffOutput(stdout);
return {
success: true,
data: diffs,
};
} catch (error) {
logger.error('Git diff failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git diff failed',
};
}
}
/**
* Get Git log
*/
async handleGitLog(args: {
limit?: number;
since?: string;
author?: string;
grep?: string;
}): Promise<ToolResult> {
try {
const { limit = 10, since, author, grep } = args;
let command = `git log --oneline --graph --decorate -${limit}`;
if (since) command += ` --since="${since}"`;
if (author) command += ` --author="${author}"`;
if (grep) command += ` --grep="${grep}"`;
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
// Parse log output
const commits = this.parseLogOutput(stdout);
return {
success: true,
data: commits,
};
} catch (error) {
logger.error('Git log failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git log failed',
};
}
}
/**
* Add files to Git staging area
*/
async handleGitAdd(args: { files: string[]; all?: boolean }): Promise<ToolResult> {
try {
const { files, all = false } = args;
const operation: Operation = {
type: 'git-operation',
target: all ? 'all files' : files.join(', '),
description: `Add ${all ? 'all files' : `${files.length} file(s)`} to staging area`,
};
const context: OperationContext = {
sandboxMode: 'workspace-write', // Git operations are generally safe in workspace
workspaceRoot: this.workspaceRoot,
userIntent: 'Stage files for commit',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Operation not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = 'git add';
if (all) {
command += ' .';
} else {
command += ` ${files.map(f => `"${f}"`).join(' ')}`;
}
await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: { staged: all ? 'all files' : files },
};
} catch (error) {
logger.error('Git add failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git add failed',
};
}
}
/**
* Commit changes
*/
async handleGitCommit(args: CommitArgs): Promise<ToolResult> {
try {
const { message, files, amend = false, signoff = false } = args;
// Check if there are staged changes
const status = await this.getGitStatus();
if (!amend && status.staged.length === 0) {
return {
success: false,
error: 'No staged changes to commit',
suggestion: 'Use git_add to stage files first',
};
}
const operation: Operation = {
type: 'git-operation',
target: amend ? 'previous commit' : 'staged changes',
description: `${amend ? 'Amend' : 'Create'} commit: "${message}"`,
metadata: { files, amend, signoff },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Commit changes to repository',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Commit not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = `git commit -m "${message}"`;
if (amend) command += ' --amend';
if (signoff) command += ' --signoff';
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
// Extract commit hash from output
const commitHash = this.extractCommitHash(stdout);
return {
success: true,
data: {
hash: commitHash,
message,
amend,
files: files || status.staged,
},
};
} catch (error) {
logger.error('Git commit failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git commit failed',
};
}
}
/**
* Push changes to remote
*/
async handleGitPush(args: {
remote?: string;
branch?: string;
force?: boolean;
setUpstream?: boolean;
}): Promise<ToolResult> {
try {
const { remote = 'origin', branch, force = false, setUpstream = false } = args;
const operation: Operation = {
type: 'git-operation',
target: `${remote}/${branch || 'current branch'}`,
description: `Push changes to remote repository${force ? ' (force push)' : ''}`,
metadata: { remote, branch, force, setUpstream },
};
const context: OperationContext = {
sandboxMode: force ? 'full-access' : 'workspace-write', // Force push is more dangerous
workspaceRoot: this.workspaceRoot,
userIntent: 'Push commits to remote repository',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Push not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = `git push ${remote}`;
if (branch) command += ` ${branch}`;
if (force) command += ' --force-with-lease'; // Safer than --force
if (setUpstream) command += ' --set-upstream';
const { stdout, stderr } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
remote,
branch: branch || 'current',
output: stdout + stderr,
},
};
} catch (error) {
logger.error('Git push failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git push failed',
};
}
}
/**
* Pull changes from remote
*/
async handleGitPull(args: {
remote?: string;
branch?: string;
rebase?: boolean;
}): Promise<ToolResult> {
try {
const { remote = 'origin', branch, rebase = false } = args;
const operation: Operation = {
type: 'git-operation',
target: `${remote}/${branch || 'current branch'}`,
description: `Pull changes from remote repository${rebase ? ' (with rebase)' : ''}`,
metadata: { remote, branch, rebase },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Pull latest changes from remote',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Pull not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = `git pull ${remote}`;
if (branch) command += ` ${branch}`;
if (rebase) command += ' --rebase';
const { stdout, stderr } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
remote,
branch: branch || 'current',
rebase,
output: stdout + stderr,
},
};
} catch (error) {
logger.error('Git pull failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git pull failed',
};
}
}
/**
* Branch operations
*/
async handleGitBranch(args: BranchArgs): Promise<ToolResult> {
try {
const { name, checkout = false, delete: deleteBranch = false, remote } = args;
if (deleteBranch) {
const operation: Operation = {
type: 'git-operation',
target: `branch: ${name}`,
description: `Delete branch "${name}"`,
metadata: { name, delete: true, remote },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Delete Git branch',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Branch deletion not approved',
suggestion: approval.suggestions?.[0],
};
}
const command = `git branch -d "${name}"`;
await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'deleted', branch: name },
};
}
// Create and optionally checkout branch
let command = `git branch "${name}"`;
if (remote) command = `git branch "${name}" "${remote}/${name}"`;
await execAsync(command, { cwd: this.workspaceRoot });
if (checkout) {
await execAsync(`git checkout "${name}"`, { cwd: this.workspaceRoot });
}
return {
success: true,
data: {
action: checkout ? 'created_and_checked_out' : 'created',
branch: name,
remote,
},
};
} catch (error) {
logger.error('Git branch operation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git branch operation failed',
};
}
}
/**
* Checkout branch or commit
*/
async handleGitCheckout(args: {
target: string;
createBranch?: boolean;
force?: boolean;
}): Promise<ToolResult> {
try {
const { target, createBranch = false, force = false } = args;
// Check if there are uncommitted changes
const status = await this.getGitStatus();
if (!force && (status.modified.length > 0 || status.staged.length > 0)) {
return {
success: false,
error: 'Uncommitted changes would be lost',
suggestion: 'Commit or stash changes before checkout, or use force option',
};
}
let command = 'git checkout';
if (createBranch) command += ' -b';
if (force) command += ' --force';
command += ` "${target}"`;
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
target,
createBranch,
force,
output: stdout,
},
};
} catch (error) {
logger.error('Git checkout failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git checkout failed',
};
}
}
/**
* Reset repository state
*/
async handleGitReset(args: {
mode?: 'soft' | 'mixed' | 'hard';
target?: string;
}): Promise<ToolResult> {
try {
const { mode = 'mixed', target = 'HEAD' } = args;
const operation: Operation = {
type: 'git-operation',
target: `${target} (${mode} reset)`,
description: `Reset repository to ${target} with ${mode} mode`,
metadata: { mode, target },
};
const context: OperationContext = {
sandboxMode: mode === 'hard' ? 'full-access' : 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Reset repository state',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Reset not approved',
suggestion: approval.suggestions?.[0],
};
}
const command = `git reset --${mode} "${target}"`;
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
mode,
target,
output: stdout,
},
};
} catch (error) {
logger.error('Git reset failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git reset failed',
};
}
}
/**
* Handle Git merges
*/
async handleGitMerge(args: MergeArgs): Promise<ToolResult> {
try {
const { branch, strategy = 'merge', noFastForward = false } = args;
const operation: Operation = {
type: 'git-operation',
target: `branch: ${branch}`,
description: `Merge branch "${branch}" using ${strategy} strategy`,
metadata: { branch, strategy, noFastForward },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Merge Git branch',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Merge not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = `git merge "${branch}"`;
if (noFastForward) command += ' --no-ff';
if (strategy === 'squash') command += ' --squash';
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
branch,
strategy,
noFastForward,
output: stdout,
},
};
} catch (error) {
logger.error('Git merge failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git merge failed',
suggestion: 'Check for merge conflicts and resolve them manually',
};
}
}
/**
* Handle Git rebase
*/
async handleGitRebase(args: {
target: string;
interactive?: boolean;
abort?: boolean;
continue?: boolean;
}): Promise<ToolResult> {
try {
const { target, interactive = false, abort = false, continue: continueRebase = false } = args;
if (abort) {
await execAsync('git rebase --abort', { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'aborted' },
};
}
if (continueRebase) {
await execAsync('git rebase --continue', { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'continued' },
};
}
const operation: Operation = {
type: 'git-operation',
target: `rebase onto ${target}`,
description: `Rebase current branch onto "${target}"${interactive ? ' (interactive)' : ''}`,
metadata: { target, interactive },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Rebase Git branch',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Rebase not approved',
suggestion: approval.suggestions?.[0],
};
}
let command = `git rebase "${target}"`;
if (interactive) command += ' -i';
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
target,
interactive,
output: stdout,
},
};
} catch (error) {
logger.error('Git rebase failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git rebase failed',
suggestion: 'Check for conflicts and resolve them, then use git_rebase with continue=true',
};
}
}
/**
* Handle Git tags
*/
async handleGitTag(args: TagArgs): Promise<ToolResult> {
try {
const { name, message, delete: deleteTag = false, push = false } = args;
if (deleteTag) {
const operation: Operation = {
type: 'git-operation',
target: `tag: ${name}`,
description: `Delete tag "${name}"`,
metadata: { name, delete: true },
};
const context: OperationContext = {
sandboxMode: 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Delete Git tag',
sessionId: this.generateSessionId(),
};
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Tag deletion not approved',
suggestion: approval.suggestions?.[0],
};
}
await execAsync(`git tag -d "${name}"`, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'deleted', tag: name },
};
}
// Create tag
let command = `git tag "${name}"`;
if (message) command += ` -m "${message}"`;
await execAsync(command, { cwd: this.workspaceRoot });
// Push tag if requested
if (push) {
await execAsync(`git push origin "${name}"`, { cwd: this.workspaceRoot });
}
return {
success: true,
data: {
action: 'created',
tag: name,
message,
pushed: push,
},
};
} catch (error) {
logger.error('Git tag operation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git tag operation failed',
};
}
}
/**
* Handle Git remote operations
*/
async handleGitRemote(args: {
action: 'add' | 'remove' | 'list' | 'show';
name?: string;
url?: string;
}): Promise<ToolResult> {
try {
const { action, name, url } = args;
switch (action) {
case 'list': {
const { stdout: listOutput } = await execAsync('git remote -v', {
cwd: this.workspaceRoot,
});
return {
success: true,
data: { remotes: listOutput.trim().split('\n') },
};
}
case 'show': {
if (!name) {
return { success: false, error: 'Remote name required for show action' };
}
const { stdout: showOutput } = await execAsync(`git remote show "${name}"`, {
cwd: this.workspaceRoot,
});
return {
success: true,
data: { remote: name, info: showOutput },
};
}
case 'add':
if (!name || !url) {
return { success: false, error: 'Remote name and URL required for add action' };
}
await execAsync(`git remote add "${name}" "${url}"`, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'added', remote: name, url },
};
case 'remove':
if (!name) {
return { success: false, error: 'Remote name required for remove action' };
}
await execAsync(`git remote remove "${name}"`, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'removed', remote: name },
};
default:
return { success: false, error: `Unknown action: ${action}` };
}
} catch (error) {
logger.error('Git remote operation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git remote operation failed',
};
}
}
/**
* Handle Git stash operations
*/
async handleGitStash(args: {
action: 'save' | 'pop' | 'list' | 'show' | 'drop';
message?: string;
index?: number;
}): Promise<ToolResult> {
try {
const { action, message, index } = args;
switch (action) {
case 'save': {
let saveCommand = 'git stash';
if (message) saveCommand += ` -m "${message}"`;
const { stdout: saveOutput } = await execAsync(saveCommand, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'saved', message, output: saveOutput },
};
}
case 'pop': {
let popCommand = 'git stash pop';
if (index !== undefined) popCommand += ` stash@{${index}}`;
const { stdout: popOutput } = await execAsync(popCommand, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'popped', index, output: popOutput },
};
}
case 'list': {
const { stdout: listOutput } = await execAsync('git stash list', {
cwd: this.workspaceRoot,
});
return {
success: true,
data: {
stashes: listOutput
.trim()
.split('\n')
.filter(line => line),
},
};
}
case 'show': {
let showCommand = 'git stash show';
if (index !== undefined) showCommand += ` stash@{${index}}`;
const { stdout: showOutput } = await execAsync(showCommand, { cwd: this.workspaceRoot });
return {
success: true,
data: { index, diff: showOutput },
};
}
case 'drop':
if (index === undefined) {
return { success: false, error: 'Stash index required for drop action' };
}
await execAsync(`git stash drop stash@{${index}}`, { cwd: this.workspaceRoot });
return {
success: true,
data: { action: 'dropped', index },
};
default:
return { success: false, error: `Unknown stash action: ${action}` };
}
} catch (error) {
logger.error('Git stash operation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git stash operation failed',
};
}
}
/**
* Handle Git clean operations
*/
async handleGitClean(args: {
dryRun?: boolean;
directories?: boolean;
ignored?: boolean;
force?: boolean;
}): Promise<ToolResult> {
try {
const { dryRun = true, directories = false, ignored = false, force = false } = args;
const operation: Operation = {
type: 'git-operation',
target: 'untracked files',
description: `Clean untracked files${dryRun ? ' (dry run)' : ''}`,
metadata: { dryRun, directories, ignored, force },
};
const context: OperationContext = {
sandboxMode: force && !dryRun ? 'full-access' : 'workspace-write',
workspaceRoot: this.workspaceRoot,
userIntent: 'Clean untracked files',
sessionId: this.generateSessionId(),
};
if (!dryRun) {
const approval = await this.approvalManager.requestApproval(operation, context);
if (!approval.granted) {
return {
success: false,
error: 'Clean operation not approved',
suggestion: approval.suggestions?.[0],
};
}
}
let command = 'git clean';
if (dryRun) command += ' -n';
else if (force) command += ' -f';
if (directories) command += ' -d';
if (ignored) command += ' -x';
const { stdout } = await execAsync(command, { cwd: this.workspaceRoot });
return {
success: true,
data: {
dryRun,
directories,
ignored,
force,
output: stdout,
},
};
} catch (error) {
logger.error('Git clean failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Git clean failed',
};
}
}
/**
* Check if current directory is a Git repository
*/
private async isGitRepository(): Promise<boolean> {
try {
await execAsync('git rev-parse --git-dir', { cwd: this.workspaceRoot });
return true;
} catch {
return false;
}
}
/**
* Get comprehensive Git status
*/
private async getGitStatus(): Promise<GitStatus> {
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
cwd: this.workspaceRoot,
});
const { stdout: branchOutput } = await execAsync('git branch --show-current', {
cwd: this.workspaceRoot,
});
const staged: string[] = [];
const modified: string[] = [];
const untracked: string[] = [];
statusOutput.split('\n').forEach(line => {
if (!line.trim()) return;
const statusCode = line.substring(0, 2);
const filename = line.substring(3);
if (!statusCode.startsWith(' ') && !statusCode.startsWith('?')) {
staged.push(filename);
}
if (statusCode[1] !== ' ') {
modified.push(filename);
}
if (statusCode === '??') {
untracked.push(filename);
}
});
// Get ahead/behind info
let ahead = 0;
let behind = 0;
try {
const { stdout: aheadBehind } = await execAsync(
'git rev-list --left-right --count HEAD...@{upstream}',
{ cwd: this.workspaceRoot }
);
const [aheadStr, behindStr] = aheadBehind.trim().split('\t');
ahead = parseInt(aheadStr) || 0;
behind = parseInt(behindStr) || 0;
} catch {
// No upstream branch
}
return {
branch: branchOutput.trim(),
staged,
modified,
untracked,
ahead,
behind,
clean: staged.length === 0 && modified.length === 0 && untracked.length === 0,
};
}
/**
* Parse Git diff output
*/
private parseDiffOutput(diffOutput: string): GitDiffResult[] {
const diffs: GitDiffResult[] = [];
const lines = diffOutput.split('\n');
let currentFile = '';
let currentPatch = '';
let additions = 0;
let deletions = 0;
for (const line of lines) {
if (line.startsWith('diff --git')) {
// New file diff
if (currentFile) {
diffs.push({
file: currentFile,
additions,
deletions,
patch: currentPatch.trim(),
});
}
currentFile = line.split(' ')[2].substring(2); // Remove 'a/' prefix
currentPatch = `${line}\n`;
additions = 0;
deletions = 0;
} else {
currentPatch += `${line}\n`;
if (line.startsWith('+') && !line.startsWith('+++')) {
additions++;
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions++;
}
}
}
// Add last file
if (currentFile) {
diffs.push({
file: currentFile,
additions,
deletions,
patch: currentPatch.trim(),
});
}
return diffs;
}
/**
* Parse Git log output
*/
private parseLogOutput(logOutput: string): GitCommitInfo[] {
const commits: GitCommitInfo[] = [];
const lines = logOutput.split('\n').filter(line => line.trim());
for (const line of lines) {
// Parse oneline format: hash message
const match = line.match(/^[*|\\\s]*([a-f0-9]+)\s+(.+)$/);
if (match) {
const [, hash, message] = match;
commits.push({
hash,
author: 'unknown', // Would need different format to get author
date: 'unknown', // Would need different format to get date
message: message.trim(),
files: [], // Would need different format to get files
});
}
}
return commits;
}
/**
* Extract commit hash from git commit output
*/
private extractCommitHash(commitOutput: string): string {
const match = commitOutput.match(/\[.+\s([a-f0-9]+)\]/);
return match ? match[1] : 'unknown';
}
/**
* Generate session ID for approval tracking
*/
private generateSessionId(): string {
return `git_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}