capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
293 lines • 10.3 kB
JavaScript
import { BaseTool } from '../base.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
const execAsync = promisify(exec);
export class GitTool extends BaseTool {
name = 'git';
displayName = '🔀 Git';
description = 'Execute git commands - status, diff, log, branch, commit, push, pull, etc.';
category = 'system';
icon = '🔀';
parameters = [
{
name: 'command',
type: 'string',
description: 'Git command to execute',
required: true,
enum: ['status', 'diff', 'log', 'branch', 'add', 'commit', 'push', 'pull', 'checkout', 'merge', 'stash', 'reset', 'clone', 'remote', 'show', 'blame']
},
{
name: 'args',
type: 'array',
description: 'Additional arguments for the git command',
required: false,
items: {
type: 'string',
description: 'Argument'
}
},
{
name: 'path',
type: 'string',
description: 'Path to git repository (default: current directory)',
required: false,
default: '.'
},
{
name: 'message',
type: 'string',
description: 'Commit message (for commit command)',
required: false
},
{
name: 'branch',
type: 'string',
description: 'Branch name (for branch operations)',
required: false
},
{
name: 'files',
type: 'array',
description: 'Files to operate on (for add, etc.)',
required: false,
items: {
type: 'string',
description: 'File path'
}
},
{
name: 'showDiff',
type: 'boolean',
description: 'Show diff in output (for applicable commands)',
required: false,
default: false
},
{
name: 'includeUntracked',
type: 'boolean',
description: 'Include untracked files (for status)',
required: false,
default: true
}
];
permissions = {
fileSystem: 'write'
};
ui = {
showProgress: true,
collapsible: true,
dangerous: false
};
async run(params, context) {
const { command, args = [], path: repoPath = '.', message, branch, files = [], showDiff = false, includeUntracked = true } = params;
const resolvedPath = path.isAbsolute(repoPath)
? repoPath
: path.join(context.workingDirectory || process.cwd(), repoPath);
this.reportProgress(context, `Executing git ${command}...`);
try {
const gitCommand = 'git';
const gitArgs = [command];
switch (command) {
case 'status':
gitArgs.push('-s');
if (!includeUntracked) {
gitArgs.push('-uno');
}
break;
case 'diff':
if (showDiff || args.includes('--staged')) {
}
else {
gitArgs.push('--stat');
}
break;
case 'log':
gitArgs.push('--oneline', '-10');
if (showDiff) {
gitArgs.push('-p');
}
break;
case 'add':
if (files.length > 0) {
gitArgs.push(...files);
}
else if (args.includes('-A') || args.includes('--all')) {
gitArgs.push('-A');
}
else {
throw new Error('No files specified for git add');
}
break;
case 'commit':
if (!message) {
throw new Error('Commit message is required');
}
gitArgs.push('-m', message);
break;
case 'checkout':
if (branch) {
gitArgs.push(branch);
}
else if (args.length > 0) {
}
else {
throw new Error('Branch name or args required for checkout');
}
break;
case 'branch':
if (branch && args.includes('-d')) {
gitArgs.push('-d', branch);
}
else if (branch) {
gitArgs.push(branch);
}
break;
case 'push':
case 'pull':
break;
case 'stash':
if (args.includes('pop') || args.includes('apply')) {
}
else if (message) {
gitArgs.push('save', message);
}
break;
case 'clone':
if (args.length === 0) {
throw new Error('Repository URL required for clone');
}
break;
}
gitArgs.push(...args);
const fullCommand = `${gitCommand} ${gitArgs.join(' ')}`;
const { stdout, stderr } = await execAsync(fullCommand, {
cwd: resolvedPath,
maxBuffer: 10 * 1024 * 1024
});
const output = this.formatGitOutput(command, stdout, stderr);
const summary = this.createSummary(command, stdout, stderr);
return {
command: fullCommand,
output,
summary,
success: true,
display: summary || output
};
}
catch (error) {
if (error.code === 1 && command === 'diff') {
return {
command: `git ${command}`,
output: error.stdout || 'No differences found',
summary: 'No changes detected',
success: true
};
}
throw new Error(`Git command failed: ${error.message}\n${error.stderr || ''}`);
}
}
formatGitOutput(command, stdout, stderr) {
const output = stdout || stderr || '';
switch (command) {
case 'status':
if (!output.trim()) {
return 'Working tree clean';
}
return this.formatStatus(output);
case 'log':
return this.formatLog(output);
case 'branch':
return this.formatBranches(output);
default:
return output;
}
}
formatStatus(output) {
const lines = output.trim().split('\n');
const staged = [];
const modified = [];
const untracked = [];
for (const line of lines) {
if (line.startsWith('A ') || line.startsWith('M ') || line.startsWith('D ')) {
staged.push(line);
}
else if (line.startsWith(' M') || line.startsWith(' D')) {
modified.push(line);
}
else if (line.startsWith('??')) {
untracked.push(line);
}
}
let formatted = '';
if (staged.length > 0) {
formatted += '📦 Staged changes:\n';
formatted += staged.map(l => ` ${l}`).join('\n') + '\n\n';
}
if (modified.length > 0) {
formatted += '✏️ Modified files:\n';
formatted += modified.map(l => ` ${l}`).join('\n') + '\n\n';
}
if (untracked.length > 0) {
formatted += '❓ Untracked files:\n';
formatted += untracked.map(l => ` ${l}`).join('\n') + '\n';
}
return formatted || 'Working tree clean';
}
formatLog(output) {
const lines = output.trim().split('\n');
return lines.map(line => {
const match = line.match(/^([a-f0-9]+)\s+(.*)$/);
if (match) {
return `${match[1]} ${match[2]}`;
}
return line;
}).join('\n');
}
formatBranches(output) {
const lines = output.trim().split('\n');
return lines.map(line => {
if (line.startsWith('*')) {
return `${line} (current)`;
}
return line;
}).join('\n');
}
createSummary(command, stdout, stderr) {
const output = stdout || stderr || '';
switch (command) {
case 'status': {
const lines = output.trim().split('\n').filter(l => l);
if (lines.length === 0) {
return '✅ Working tree clean';
}
return `📝 ${lines.length} changes in working tree`;
}
case 'commit': {
const commitMatch = output.match(/\[([^\]]+)\s+([a-f0-9]+)\]/);
if (commitMatch) {
return `✅ Committed to ${commitMatch[1]} (${commitMatch[2]})`;
}
return '✅ Changes committed';
}
case 'push':
return '⬆️ Pushed to remote';
case 'pull':
if (output.includes('Already up to date')) {
return '✅ Already up to date';
}
return '⬇️ Pulled from remote';
case 'branch': {
const branchCount = output.trim().split('\n').length;
return `🌿 ${branchCount} branches`;
}
case 'log': {
const commitCount = output.trim().split('\n').length;
return `📜 Showing ${commitCount} commits`;
}
default:
return '';
}
}
}
//# sourceMappingURL=git.js.map