UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

293 lines 10.3 kB
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