sushil-gitmate
Version:
Professional Git workflow automation powered by AI. Streamline your development process with natural language commands and intelligent automation.
1,362 lines (1,223 loc) ⢠99.3 kB
JavaScript
import logger from '../src/utils/logger.js';
import UI from '../src/utils/ui.js';
import * as prompter from '../src/utils/prompter.js';
import * as githubService from '../src/services/githubService.js';
import * as gitService from '../src/services/gitService.js';
import { aiService, AI_PROVIDERS, setProvider } from '../src/services/aiServiceFactory.js';
import { getToken, clearAllTokens, storeToken } from '../src/utils/tokenManager.js';
import inquirer from 'inquirer';
import chalk from 'chalk';
import dotenv from 'dotenv';
import crypto from 'crypto';
// Load environment variables
dotenv.config();
const ENCRYPTION_KEY = "12345678901234567890123456789012"; // Must be the same as backend
const IV_LENGTH = 16;
function decrypt(text) {
try {
if (!text || typeof text !== 'string' || !text.includes(':')) return text;
const [ivHex, encryptedHex] = text.split(':');
if (!ivHex || !encryptedHex) return text; // Not encrypted
const iv = Buffer.from(ivHex, 'hex');
const encryptedText = Buffer.from(encryptedHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY, 'utf8'), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString('utf8');
} catch (error) {
console.error('Decryption error:', error);
return null;
}
}
const serviceName = 'CommandHandler';
let diffViewerInitialized = false;
async function ensureAuthenticated() {
let token = await getToken('github_access_token');
if (!token) {
logger.warn('User is not authenticated. Please authenticate first.', { service: serviceName });
// Determine the correct auth URL
const renderAuthUrl = process.env.RENDER_AUTH_URL || 'https://gitbot-jtp2.onrender.com/auth/github';
const authInitiateUrl = renderAuthUrl;
UI.error('Authentication Required',
'You need to authenticate with GitHub to use this feature.');
UI.info('Follow these steps:',
`1. Please open your browser and navigate to: ${authInitiateUrl}\n2. Authorize the application\n3. Paste the encrypted token below:`);
const { token: enteredToken } = await inquirer.prompt([
{
type: 'password',
name: 'token',
message: 'Paste the encrypted token you received from the browser:',
mask: '*',
validate: input => input.trim() !== '' || 'Token is required',
},
]);
// Store the encrypted token as-is
await storeToken('github_access_token', enteredToken.trim());
// Now decrypt it for use
const decryptedToken = decrypt(enteredToken.trim());
if (!decryptedToken) {
UI.error('Invalid Token', 'The token you provided could not be decrypted. Please try again.');
process.exitCode = 1;
return null;
}
token = decryptedToken;
}
return token;
}
export async function handleRepoCommand(args) {
const token = await ensureAuthenticated();
if (!token) {
process.exitCode = 1;
return;
}
const [subCommand, ...options] = args;
logger.debug(`Handling 'repo' command: ${subCommand}`, { options, service: serviceName });
switch (subCommand) {
case 'create': {
const repoName = options[0];
if (!repoName) {
UI.error('Repository Name Required',
'Please provide a repository name. Usage: repo create <repo-name> [--private] [--description "Your description"]');
return;
}
const repoOptions = {
description: '',
private: false,
auto_init: true,
};
for (let i = 1; i < options.length; i++) {
if (options[i] === '--private') {
repoOptions.private = true;
} else if (options[i] === '--description' && options[i + 1]) {
repoOptions.description = options[i + 1];
i++;
} else if (options[i] === '--no-init') {
repoOptions.auto_init = false;
}
}
try {
UI.progress('Creating repository...');
const repo = await githubService.createRepository(repoName, repoOptions);
UI.success('Repository Created Successfully',
`Repository: ${repo.full_name}\nURL: ${repo.html_url}`);
logger.info(`Repository created: ${repo.full_name}`, { url: repo.html_url, service: serviceName });
} catch (error) {
UI.error('Failed to Create Repository', error.message);
logger.error(`Failed to create repository '${repoName}':`, { message: error.message, stack: error.stack, service: serviceName });
}
break;
}
case 'list': {
const listOptions = {};
for (let i = 0; i < options.length; i++) {
const [key, value] = options[i].split('=');
if (key && value) {
if (key.startsWith('--')) {
listOptions[key.substring(2)] = value;
} else {
listOptions[key] = value;
}
}
}
try {
UI.progress('Fetching repositories...');
const repos = await githubService.listUserRepositories(listOptions);
if (repos.length === 0) {
UI.info('No repositories found', 'No repositories match your current criteria.');
} else {
UI.section('Your GitHub Repositories', `Found ${repos.length} repository(ies)`);
const repoData = repos.map(repo => ({
Name: repo.full_name,
Type: repo.private ? 'Private' : 'Public',
Updated: new Date(repo.updated_at).toLocaleDateString(),
URL: repo.html_url
}));
UI.table(repoData);
}
} catch (error) {
UI.error('Failed to List Repositories', error.message);
logger.error('Failed to list repositories:', { message: error.message, stack: error.stack, service: serviceName });
}
break;
}
default:
UI.warning('Unknown Command', `Unknown 'repo' subcommand: ${subCommand}`);
UI.help([
{ name: 'create', description: 'Create a new repository', examples: ['repo create my-repo', 'repo create my-repo --private'] },
{ name: 'list', description: 'List your repositories', examples: ['repo list', 'repo list --type=owner'] }
]);
}
}
export async function handleGitCommand(args, currentWorkingDirectory = '.') {
const [subCommand, ...options] = args;
logger.info(`Handling 'git' command: ${subCommand} in ${currentWorkingDirectory}`, { options, service: serviceName });
switch (subCommand) {
case 'init':
try {
const message = await gitService.initRepo(currentWorkingDirectory);
console.log(message);
} catch (error) {
console.error(`Error initializing Git repository: ${error.message}`);
}
break;
case 'status':
try {
const formattedStatus = await gitService.getFormattedStatus(currentWorkingDirectory);
console.log(formattedStatus);
} catch (error) {
console.error(`Error getting Git status: ${error.message}`);
}
break;
case 'add':
const filesToAdd = options.length > 0 ? options : '.';
try {
await gitService.addFiles(filesToAdd, currentWorkingDirectory);
console.log(`Files staged: ${Array.isArray(filesToAdd) ? filesToAdd.join(', ') : filesToAdd}`);
} catch (error) {
console.error(`Error staging files: ${error.message}`);
}
break;
case 'commit':
const commitMessage = options.join(' ');
if (!commitMessage) {
console.error("Error: Commit message is required. Usage: git commit <message>");
return;
}
try {
const result = await gitService.commitChanges(commitMessage, currentWorkingDirectory);
console.log(`Committed: [${result.branch}] ${result.commit} - ${result.summary.changes} changes.`);
} catch (error) {
console.error(`Error committing changes: ${error.message}`);
}
break;
case 'branch':
try {
const branches = await gitService.listBranches(currentWorkingDirectory);
console.log(chalk.blue('Branches:'), branches.join(', '));
} catch (error) {
console.error(`Error listing branches: ${error.message}`);
}
break;
case 'remote':
try {
const remotes = await gitService.getRemotes(currentWorkingDirectory);
if (remotes.length === 0) {
console.log(chalk.yellow('No remotes configured.'));
} else {
console.log(chalk.blue('Remotes:'));
remotes.forEach(remote => {
console.log(` ${remote.name}: ${remote.refs.fetch}`);
});
}
} catch (error) {
console.error(`Error listing remotes: ${error.message}`);
}
break;
case 'log':
try {
const log = await gitService.getLog(currentWorkingDirectory);
log.all.forEach(entry => {
console.log(chalk.yellow(entry.hash), entry.date, '-', entry.message);
});
} catch (error) {
console.error(`Error getting log: ${error.message}`);
}
break;
case 'diff':
try {
const diff = await gitService.getDiff(options.join(' '), currentWorkingDirectory);
console.log(diff);
} catch (error) {
console.error(`Error getting diff: ${error.message}`);
}
break;
case 'push':
try {
const remoteName = options[0] || 'origin';
const branchName = options[1];
const pushOptions = {};
// Parse push options
if (options.includes('--set-upstream') || options.includes('-u')) {
pushOptions.setUpstream = true;
}
if (options.includes('--force') || options.includes('-f')) {
pushOptions.force = true;
}
const result = await gitService.pushChanges(remoteName, branchName, currentWorkingDirectory, pushOptions);
console.log(result);
} catch (error) {
console.error(`Error pushing changes: ${error.message}`);
}
break;
case 'pull':
try {
const remoteName = options[0] || 'origin';
const branchName = options[1];
const result = await gitService.pullChanges(remoteName, branchName, currentWorkingDirectory);
console.log(result);
} catch (error) {
console.error(`Error pulling changes: ${error.message}`);
}
break;
case 'clone':
try {
await handleInteractiveClone(options);
} catch (error) {
console.error(`Error cloning repository: ${error.message}`);
}
break;
case 'checkout':
try {
const branchName = options[0];
if (!branchName) {
console.error("Error: Branch name is required. Usage: git checkout <branch-name>");
return;
}
const result = await gitService.checkoutBranch(branchName, currentWorkingDirectory);
console.log(result);
} catch (error) {
console.error(`Error checking out branch: ${error.message}`);
}
break;
case 'merge':
try {
const sourceBranch = options[0];
if (!sourceBranch) {
console.error("Error: Source branch is required. Usage: git merge <source-branch>");
return;
}
const result = await gitService.mergeBranch(sourceBranch, currentWorkingDirectory);
console.log(result);
} catch (error) {
console.error(`Error merging branch: ${error.message}`);
}
break;
case 'rebase':
try {
const baseBranch = options[0];
if (!baseBranch) {
console.error("Error: Base branch is required. Usage: git rebase <base-branch>");
return;
}
const result = await gitService.rebaseBranch(baseBranch, currentWorkingDirectory);
console.log(result);
} catch (error) {
console.error(`Error rebasing branch: ${error.message}`);
}
break;
case 'revert':
try {
const commitHash = options[0] || 'HEAD';
const result = await gitService.revertCommit(commitHash, currentWorkingDirectory);
console.log(result);
} catch (error) {
console.error(`Error reverting commit: ${error.message}`);
}
break;
// Add more git subcommands: push, pull, branch, checkout, merge, rebase etc.
default:
logger.warn(`Unknown 'git' subcommand: ${subCommand}`, { service: serviceName });
console.log(`Unknown 'git' subcommand: ${subCommand}. Supported: init, status, add, commit, branch, remote, log, diff, push, pull, clone, checkout, merge, rebase, revert (more to come).`);
}
}
async function handleInteractiveClone(options) {
try {
// Check if URL is provided directly
const repoUrl = options[0];
if (repoUrl) {
const targetDirectory = options[1];
const result = await gitService.cloneRepository(repoUrl, { directory: targetDirectory });
console.log(result);
return;
}
// Interactive flow
console.log(chalk.blue('\nš Repository Cloning Options'));
const { cloneType } = await inquirer.prompt([
{
type: 'list',
name: 'cloneType',
message: 'What would you like to clone?',
choices: [
{ name: 'š Clone from your own repositories', value: 'own' },
{ name: 'š Clone from external URL', value: 'external' }
]
}
]);
if (cloneType === 'own') {
await handleCloneOwnRepos();
} else {
await handleCloneExternalRepo();
}
} catch (error) {
throw error;
}
}
async function handleCloneOwnRepos() {
try {
// Ensure user is authenticated
const token = await ensureAuthenticated();
if (!token) {
console.error(chalk.red('Authentication required to access your repositories.'));
return;
}
console.log(chalk.yellow('\nš Fetching your repositories...'));
const repos = await githubService.listUserRepositories({ per_page: 100 });
if (repos.length === 0) {
console.log(chalk.yellow('No repositories found.'));
return;
}
// Format repos for selection
const repoChoices = repos.map(repo => ({
name: `${repo.private ? 'š' : 'š'} ${repo.full_name} - ${repo.description || 'No description'}`,
value: repo.clone_url,
short: repo.full_name
}));
const { selectedRepo } = await inquirer.prompt([
{
type: 'list',
name: 'selectedRepo',
message: 'Select a repository to clone:',
choices: repoChoices,
pageSize: 15
}
]);
const { targetDirectory } = await inquirer.prompt([
{
type: 'input',
name: 'targetDirectory',
message: 'Enter target directory (leave empty for default):',
default: '',
validate: (input) => {
if (input.trim() === '.') {
return 'Cannot clone to current directory. Please enter a different directory name or leave empty for default.';
}
if (input.trim() === '..') {
return 'Cannot clone to parent directory. Please enter a different directory name or leave empty for default.';
}
return true;
}
}
]);
console.log(chalk.yellow('\nš„ Cloning repository...'));
const cloneOptions = {};
if (targetDirectory && targetDirectory.trim()) {
cloneOptions.directory = targetDirectory.trim();
}
const result = await gitService.cloneRepository(selectedRepo, cloneOptions);
console.log(chalk.green('ā
' + result));
// Show helpful next steps
const clonedDir = targetDirectory && targetDirectory.trim() ? targetDirectory.trim() : selectedRepo.split('/').pop().replace('.git', '');
console.log(chalk.blue('\nš Next steps:'));
console.log(chalk.blue(` cd ${clonedDir}`));
console.log(chalk.blue(` ls -la # Check if files were cloned successfully`));
} catch (error) {
if (error.message.includes('Authentication')) {
console.error(chalk.red('ā Authentication failed. Please run "gitmate auth github" first.'));
} else {
console.error(chalk.red('ā Error cloning repository:'), error.message);
}
}
}
async function handleCloneExternalRepo() {
try {
const { repoUrl } = await inquirer.prompt([
{
type: 'input',
name: 'repoUrl',
message: 'Enter repository URL (GitHub, GitLab, etc.):',
validate: input => {
if (!input.trim()) return 'Repository URL is required';
if (!input.includes('github.com') && !input.includes('gitlab.com') && !input.includes('bitbucket.org')) {
return 'Please enter a valid repository URL from GitHub, GitLab, or Bitbucket';
}
return true;
}
}
]);
const { targetDirectory } = await inquirer.prompt([
{
type: 'input',
name: 'targetDirectory',
message: 'Enter target directory (leave empty for default):',
default: '',
validate: (input) => {
if (input.trim() === '.') {
return 'Cannot clone to current directory. Please enter a different directory name or leave empty for default.';
}
if (input.trim() === '..') {
return 'Cannot clone to parent directory. Please enter a different directory name or leave empty for default.';
}
return true;
}
}
]);
console.log(chalk.yellow('\nš„ Attempting to clone repository...'));
try {
const cloneOptions = {};
if (targetDirectory && targetDirectory.trim()) {
cloneOptions.directory = targetDirectory.trim();
}
const result = await gitService.cloneRepository(repoUrl, cloneOptions);
console.log(chalk.green('ā
' + result));
// Show helpful next steps
const clonedDir = targetDirectory && targetDirectory.trim() ? targetDirectory.trim() : repoUrl.split('/').pop().replace('.git', '');
console.log(chalk.blue('\nš Next steps:'));
console.log(chalk.blue(` cd ${clonedDir}`));
console.log(chalk.blue(` ls -la # Check if files were cloned successfully`));
} catch (cloneError) {
if (cloneError.message.includes('Authentication failed') || cloneError.message.includes('not found')) {
console.log(chalk.yellow('\nš This appears to be a private repository.'));
console.log(chalk.blue('To clone private repositories, you need to:'));
console.log(chalk.blue('1. Ensure you have access to the repository'));
console.log(chalk.blue('2. Use SSH keys or personal access tokens'));
console.log(chalk.blue('3. Or ask the repository owner to make it public'));
} else {
throw cloneError;
}
}
} catch (error) {
console.error(chalk.red('ā Error:'), error.message);
}
}
async function handleConfirmationFlow(parsed, maxRetries = 4) {
let retryCount = 0;
let currentParsed = parsed;
while (retryCount < maxRetries) {
try {
// Generate confirmation message
const confirmationMessage = await aiService.generateConfirmation(currentParsed);
if (!confirmationMessage) {
throw new Error('Failed to generate confirmation message');
}
console.log(chalk.cyan("\n" + confirmationMessage));
// Ask for confirmation
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Am I right?',
default: true
}
]);
if (confirmed) {
return currentParsed; // Return the current parsed result
}
retryCount++;
if (retryCount < maxRetries) {
console.log(chalk.yellow(`\nLet me try again. Please rephrase your request (attempt ${retryCount + 1}/${maxRetries}):`));
const { newQuery } = await inquirer.prompt([
{
type: 'input',
name: 'newQuery',
message: 'Your request:',
validate: input => input.trim() !== '' || 'Please enter your request'
}
]);
// Parse the new query
const newParsed = await aiService.parseIntent(newQuery);
if (newParsed && newParsed.intent !== 'unknown') {
currentParsed = newParsed;
} else {
console.log(chalk.red("I still couldn't understand that. Let me try again."));
}
}
} catch (error) {
logger.error('Error in confirmation flow:', {
message: error.message,
stack: error.stack,
retryCount,
service: serviceName
});
if (retryCount < maxRetries) {
console.log(chalk.yellow(`\nI encountered an error. Let me try again (attempt ${retryCount + 1}/${maxRetries}):`));
const { newQuery } = await inquirer.prompt([
{
type: 'input',
name: 'newQuery',
message: 'Please rephrase your request:',
validate: input => input.trim() !== '' || 'Please enter your request'
}
]);
try {
const newParsed = await aiService.parseIntent(newQuery);
if (newParsed && newParsed.intent !== 'unknown') {
currentParsed = newParsed;
}
} catch (parseError) {
console.log(chalk.red("I'm still having trouble understanding. Let me try again."));
}
}
}
}
// If we've exhausted all retries, show help
console.log(chalk.yellow("\nI'm having trouble understanding your request. Let me show you what I can do:"));
try {
const helpMessage = await aiService.generateCommandHelp();
console.log(chalk.cyan("\n" + helpMessage));
} catch (helpError) {
console.log(chalk.yellow("\nHere are some common commands you can try:"));
console.log(chalk.cyan(`
1. Push changes:
- "push my changes to main"
- "push with commit message 'update feature'"
- "force push to main"
2. Create and manage branches:
- "create a new branch called feature-x"
- "switch to main branch"
- "list all branches"
3. Commit changes:
- "commit with message 'fix bug'"
- "commit all changes"
- "commit specific files"
4. Pull and merge:
- "pull latest changes"
- "merge feature branch"
- "get updates from main"
`));
}
console.log(chalk.blue("\nFor more detailed documentation, visit:"));
console.log(chalk.blue("https://github.com/yourusername/gitbot-assistant/wiki"));
return null; // Return null to indicate failure
}
export async function handleGenerateGitignore(projectDescription) {
logger.info(`Handling .gitignore generation for: "${projectDescription}"`, { service: serviceName });
if (!projectDescription || projectDescription.trim() === '') {
console.error(".gitignore project description cannot be empty.");
return;
}
const aiReady = await aiService.checkStatus();
if (!aiReady) {
console.error("AI service is not available. Please check your AI provider setup.");
// Check if we have any environment variables set
const hasEnvConfig = process.env.MISTRAL_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!hasEnvConfig) {
console.log("\nTo set up GitMate, you have two options:");
console.log("\nOption 1 - Environment Variables (Recommended):");
console.log(" export AI_PROVIDER=mistral");
console.log(" export MISTRAL_API_KEY=your_api_key_here");
console.log(" # Then run: gitmate \"your command\"");
console.log("\nOption 2 - Interactive Setup:");
console.log(" gitmate init");
console.log(" # This will guide you through configuration");
} else {
console.log("\nConfiguration issue detected. Please check your API keys.");
}
return;
}
try {
const gitignoreContent = await aiService.generateGitignore(projectDescription);
if (gitignoreContent) {
console.log("\n--- Suggested .gitignore content ---\n");
console.log(gitignoreContent);
console.log("\n--- End of .gitignore content ---");
// TODO: Add option to write this to .gitignore file
} else {
console.log("Could not generate .gitignore content. The AI service might have returned an empty response.");
}
} catch (error) {
console.error(`Error generating .gitignore: ${error.message}`);
}
}
export async function handleGenerateCommitMessage(directoryPath = '.') {
logger.info(`Handling commit message generation for path: "${directoryPath}"`, { service: serviceName });
const aiReady = await aiService.checkStatus();
if (!aiReady) {
console.error("AI service is not available. Please check your AI provider setup.");
// Check if we have any environment variables set
const hasEnvConfig = process.env.MISTRAL_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!hasEnvConfig) {
console.log("\nTo set up GitMate, you have two options:");
console.log("\nOption 1 - Environment Variables (Recommended):");
console.log(" export AI_PROVIDER=mistral");
console.log(" export MISTRAL_API_KEY=your_api_key_here");
console.log(" # Then run: gitmate \"your command\"");
console.log("\nOption 2 - Interactive Setup:");
console.log(" gitmate init");
console.log(" # This will guide you through configuration");
} else {
console.log("\nConfiguration issue detected. Please check your API keys.");
}
return;
}
try {
const status = await gitService.getStatus(directoryPath);
if (status.files.length === 0) {
console.log("No changes to commit. Working tree clean.");
return;
}
const git = simpleGit(directoryPath);
const diffOutput = await git.diff(['HEAD']);
if (!diffOutput || diffOutput.trim() === '') {
console.log("No diff output available. Ensure changes are staged or present in the working directory.");
return;
}
const commitMessage = await aiService.generateCommitMessage(diffOutput);
if (commitMessage) {
console.log(`\nSuggested commit message:`);
console.log(`"${commitMessage}"`);
// TODO: Add option to use this message for an actual commit
} else {
console.log("Could not generate a commit message.");
}
} catch (error) {
console.error(`Error generating commit message: ${error.message}`);
}
}
export async function handleAuthLogout() {
try {
await clearAllTokens();
UI.success('Logged Out Successfully', 'All authentication tokens have been cleared.');
logger.info('User logged out successfully', { service: serviceName });
} catch (error) {
UI.error('Logout Failed', error.message);
logger.error('Logout failed:', { message: error.message, stack: error.stack, service: serviceName });
}
}
// Add a new command to switch AI providers
export async function handleSwitchAIProvider(provider) {
if (!provider || !Object.values(AI_PROVIDERS).includes(provider)) {
console.error(`Invalid AI provider. Supported providers: ${Object.values(AI_PROVIDERS).join(', ')}`);
return;
}
const success = setProvider(provider);
if (success) {
console.log(`Switched to AI provider: ${provider}`);
const isReady = await aiService.checkStatus();
if (isReady) {
console.log(`${provider} service is ready to use.`);
} else {
console.error(`${provider} service is not available. Please check your setup.`);
}
} else {
console.error(`Failed to switch to AI provider: ${provider}`);
}
}
async function handleMergeRequest(sourceBranch, targetBranch = 'main') {
logger.info(`Handling merge request from ${sourceBranch} to ${targetBranch}`, { service: serviceName });
try {
// 1. Check if we're in a git repository
const isGitRepo = await gitService.isGitRepository('.');
if (!isGitRepo) {
console.error("Error: Not a git repository. Please run this command from a git repository.");
return;
}
// 2. Get current branch and verify it's not the target branch
const currentBranch = await gitService.getCurrentBranch('.');
if (currentBranch === targetBranch) {
console.error(`Error: You are already on the target branch '${targetBranch}'. Please switch to your feature branch first.`);
return;
}
// 3. Check if source branch exists
const branches = await gitService.listBranches('.');
if (!branches.all.includes(sourceBranch)) {
console.error(`Error: Source branch '${sourceBranch}' does not exist.`);
return;
}
// 4. Check if target branch exists
if (!branches.all.includes(targetBranch)) {
console.error(`Error: Target branch '${targetBranch}' does not exist.`);
return;
}
// 5. Check for uncommitted changes
const status = await gitService.getStatus('.');
if (status.files.length > 0) {
console.log("\nYou have uncommitted changes:");
status.files.forEach(file => {
console.log(` ${file.path} (${file.working_dir})`);
});
const shouldCommit = await prompter.askYesNo("\nWould you like to commit these changes before creating the merge request?", true);
if (shouldCommit) {
const { commitMessage } = await inquirer.prompt([
{
type: 'input',
name: 'commitMessage',
message: 'Enter commit message:',
default: `feat: changes before merge request to ${targetBranch}`,
validate: input => input.trim() !== '' || 'Commit message cannot be empty'
}
]);
await gitService.addFiles('.', '.');
await gitService.commitChanges(commitMessage, '.');
console.log("Changes committed successfully.");
} else {
console.log("Please commit or stash your changes before creating a merge request.");
return;
}
}
// 6. Get repository info
const repoInfo = await gitService.getRemoteInfo('.');
if (!repoInfo) {
throw new Error('Could not determine repository information. Please ensure you have a remote repository configured.');
}
// Parse owner and repo from the remote URL
let owner, repo;
try {
// Handle different GitHub URL formats
if (repoInfo.includes('github.com')) {
// Handle HTTPS URLs (https://github.com/owner/repo.git)
const httpsMatch = repoInfo.match(/github\.com[:/]([^/]+)\/([^/]+)(?:\.git)?$/);
if (httpsMatch) {
[, owner, repo] = httpsMatch;
} else {
// Handle SSH URLs (git@github.com:owner/repo.git)
const sshMatch = repoInfo.match(/github\.com:([^/]+)\/([^/]+)(?:\.git)?$/);
if (sshMatch) {
[, owner, repo] = sshMatch;
} else {
throw new Error('Could not parse GitHub repository information from remote URL.');
}
}
} else {
// Handle direct owner/repo format
const parts = repoInfo.split('/');
if (parts.length === 2) {
[owner, repo] = parts;
} else {
throw new Error('Could not parse GitHub repository information from remote URL.');
}
}
// Remove .git suffix if present
repo = repo.replace(/\.git$/, '');
if (!owner || !repo) {
throw new Error('Invalid repository information: missing owner or repository name.');
}
} catch (error) {
logger.error('Failed to parse repository URL:', {
url: repoInfo,
error: error.message,
service: serviceName
});
throw new Error(`Could not parse GitHub repository information: ${error.message}`);
}
// 7. Check if source branch has been pushed to remote
const remoteBranches = await gitService.listBranches('.', ['-r']);
const sourceBranchRemote = `origin/${sourceBranch}`;
const targetBranchRemote = `origin/${targetBranch}`;
// Check if target branch exists on remote
if (!remoteBranches.all.includes(targetBranchRemote)) {
console.log(`\nTarget branch '${targetBranch}' does not exist on remote.`);
const createTargetBranch = await prompter.askYesNo(`Would you like to push the ${targetBranch} branch to remote?`, true);
if (createTargetBranch) {
try {
// Check if we need to switch to the target branch first
const currentBranch = await gitService.getCurrentBranch('.');
if (currentBranch !== targetBranch) {
await gitService.checkoutBranch(targetBranch, '.');
}
// Push the target branch to remote
await gitService.pushChanges('origin', targetBranch, '.', true);
console.log(`Successfully pushed branch '${targetBranch}' to remote.`);
} catch (error) {
throw new Error(`Failed to push target branch: ${error.message}`);
}
} else {
console.log(`Please push the ${targetBranch} branch to remote before creating a pull request.`);
return;
}
}
// Check if source branch exists on remote
if (!remoteBranches.all.includes(sourceBranchRemote)) {
console.log(`\nBranch '${sourceBranch}' has not been pushed to remote yet.`);
const shouldPush = await prompter.askYesNo("Would you like to push this branch to remote now?", true);
if (shouldPush) {
try {
await gitService.pushChanges('origin', sourceBranch, '.', true);
console.log(`Successfully pushed branch '${sourceBranch}' to remote.`);
} catch (pushError) {
throw new Error(`Failed to push branch: ${pushError.message}`);
}
} else {
console.log("Please push your branch to remote before creating a pull request.");
return;
}
}
// 8. Check if there are any differences between branches
const diff = await gitService.getDiffBetweenBranches(sourceBranch, targetBranch, '.');
if (!diff || diff.trim() === '') {
console.log(`\nNo differences found between '${sourceBranch}' and '${targetBranch}'.`);
console.log("This could mean:");
console.log("1. The branches are identical");
console.log("2. All changes from source branch are already in target branch");
console.log("3. The source branch has no commits");
const showLog = await prompter.askYesNo("\nWould you like to see the commit history of both branches?", true);
if (showLog) {
console.log(`\n=== Commits in ${sourceBranch} ===`);
const sourceLog = await gitService.getLog('.', { branch: sourceBranch });
if (sourceLog.total === 0) {
console.log("No commits found in source branch.");
} else {
sourceLog.all.forEach(commit => {
console.log(`- ${commit.hash.substring(0, 7)} ${commit.message}`);
});
}
console.log(`\n=== Commits in ${targetBranch} ===`);
const targetLog = await gitService.getLog('.', { branch: targetBranch });
if (targetLog.total === 0) {
console.log("No commits found in target branch.");
} else {
targetLog.all.forEach(commit => {
console.log(`- ${commit.hash.substring(0, 7)} ${commit.message}`);
});
}
}
return;
}
// 9. Generate a summary of changes using AI
console.log("\nGenerating summary of changes...");
const changeSummary = await aiService.generateResponse(
`Please analyze this git diff and provide a concise summary of the changes. Focus on the key modifications and their impact:\n\n${diff}`,
{ max_tokens: 500 }
);
// 10. Show summary and open GitHub's PR creation page
console.log("\n=== Changes Summary ===");
console.log(chalk.cyan(changeSummary));
// Construct GitHub PR URL
const prUrl = `https://github.com/${owner}/${repo}/compare/${targetBranch}...${sourceBranch}?expand=1`;
console.log("\nOpening GitHub's pull request creation page...");
console.log(chalk.blue("\nYou can also manually open this URL:"));
console.log(chalk.blue(prUrl));
// Copy the summary to clipboard for easy pasting
try {
const clipboard = await import('clipboardy');
// Ensure we're copying a string
const summaryText = typeof changeSummary === 'string' ? changeSummary : JSON.stringify(changeSummary, null, 2);
await clipboard.default.write(summaryText);
console.log(chalk.green("\nThe changes summary has been copied to your clipboard."));
console.log(chalk.green("You can paste it directly into the PR description on GitHub."));
} catch (clipboardError) {
logger.warn('Failed to copy to clipboard:', { error: clipboardError.message });
console.log(chalk.yellow("\nNote: Could not copy summary to clipboard. You can copy it manually from above."));
}
// Open the URL in the default browser
try {
const open = await import('open');
await open.default(prUrl);
} catch (openError) {
logger.warn('Failed to open browser:', { error: openError.message });
console.log(chalk.yellow("\nNote: Could not open browser automatically. Please open the URL manually:"));
console.log(chalk.blue(prUrl));
}
} catch (error) {
logger.error('Failed to create merge request:', {
message: error.message,
stack: error.stack,
sourceBranch,
targetBranch,
service: serviceName
});
console.error(`Error creating merge request: ${error.message}`);
}
}
function formatDiff(diff) {
if (!diff) return '';
return diff.split('\n').map(line => {
if (line.startsWith('+')) {
return chalk.green(line);
} else if (line.startsWith('-')) {
return chalk.red(line);
} else if (line.startsWith('@@')) {
return chalk.cyan(line);
} else if (line.startsWith('diff --git')) {
return chalk.yellow(line);
} else if (line.startsWith('index')) {
return chalk.gray(line);
} else if (line.startsWith('---') || line.startsWith('+++')) {
return chalk.blue(line);
}
return line;
}).join('\n');
}
function displayChangesSummary(diff) {
const files = new Set();
const additions = [];
const deletions = [];
let currentFile = '';
diff.split('\n').forEach(line => {
if (line.startsWith('diff --git')) {
currentFile = line.split(' ')[2].replace('a/', '');
files.add(currentFile);
} else if (line.startsWith('+') && !line.startsWith('+++')) {
additions.push({ file: currentFile, line: line.substring(1) });
} else if (line.startsWith('-') && !line.startsWith('---')) {
deletions.push({ file: currentFile, line: line.substring(1) });
}
});
console.log('\n=== Changes Summary ===');
console.log(chalk.bold(`Total files changed: ${files.size}`));
console.log(chalk.green(`Total additions: ${additions.length}`));
console.log(chalk.red(`Total deletions: ${deletions.length}`));
console.log('\n=== Changed Files ===');
files.forEach(file => {
const fileAdditions = additions.filter(a => a.file === file).length;
const fileDeletions = deletions.filter(d => d.file === file).length;
console.log(chalk.yellow(`\n${file}`));
console.log(` ${chalk.green(`+${fileAdditions}`)} ${chalk.red(`-${fileDeletions}`)}`);
});
}
async function createBackupBranch(currentBranch) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupBranchName = `gitmate-backup-with-${timestamp}`;
try {
await gitService.createAndCheckoutBranch(backupBranchName, '.');
await gitService.pushChanges('origin', backupBranchName, '.', true);
console.log(chalk.green(`Created backup branch: ${backupBranchName}`));
await gitService.checkoutBranch(currentBranch, '.'); // Switch back to original branch
return backupBranchName;
} catch (error) {
logger.error('Failed to create backup branch:', { message: error.message, service: serviceName });
console.error(chalk.red(`Failed to create backup branch: ${error.message}`));
return null;
}
}
export async function handleAuth(args) {
if (args.length === 0) {
UI.error('Provider Required', 'Please specify an authentication provider (e.g., github)');
process.exitCode = 1;
return;
}
const provider = args[0];
switch (provider.toLowerCase()) {
case 'github': {
// Print Render auth URL and prompt for token
const renderAuthUrl = 'https://gitbot-jtp2.onrender.com/auth/github';
exit();
UI.info('GitHub Authentication',
`Please open the following URL in your browser to authenticate:\n\n${renderAuthUrl}\n\nAfter authenticating, you will receive a token.\nPaste the token here when prompted.`
);
const inquirer = (await import('inquirer')).default;
const { token } = await inquirer.prompt([
{
type: 'password',
name: 'token',
message: 'Paste the token you received from the browser:',
mask: '*',
validate: input => input.trim() !== '' || 'Token is required',
},
]);
tokensaved = await storeToken('github_access_token', token.trim());
console.log(tokensaved)
UI.success('Authentication Complete', 'Your GitHub token has been saved. You are now authenticated!');
break;
}
default:
UI.error('Unknown Provider', `Unknown authentication provider: ${provider}`);
process.exitCode = 1;
break;
}
}
export async function handleNlpCommand(query) {
logger.info(`Handling NLP query: "${query}"`, { service: serviceName });
if (!query || query.trim() === '') {
console.error("NLP query cannot be empty.");
return;
}
// First check if this is a simple conversation that doesn't need AI
const lowerQuery = query.toLowerCase().trim();
const greetings = ['hello', 'hi', 'hey', 'greetings', 'good morning', 'good afternoon', 'good evening'];
const thanks = ['thank', 'thanks', 'appreciate'];
// Handle simple greetings immediately
if (greetings.some(g => lowerQuery.includes(g))) {
try {
const token = await ensureAuthenticated();
let userName = null;
if (token) {
const profile = await githubService.getUserProfile();
if (profile && (profile.name || profile.login)) {
userName = profile.name || profile.login;
}
}
console.log(chalk.cyan(`\n${userName ? `Hello, ${userName}! š` : 'Hello! š'}`));
console.log(chalk.gray("\nHow can I help you with Git today?"));
if (!userName) {
console.log(chalk.yellow("\nTip: Authenticate with GitHub for personalized experience using 'gitmate auth'"));
}
return;
} catch (error) {
// Fallback if GitHub profile fetch fails
console.log(chalk.cyan("\nHello! š\nHow can I help you with Git today?"));
return;
}
}
// Handle thanks immediately
if (thanks.some(t => lowerQuery.includes(t))) {
try {
const token = await ensureAuthenticated();
let userName = null;
if (token) {
const profile = await githubService.getUserProfile();
if (profile && (profile.name || profile.login)) {
userName = profile.name || profile.login;
}
}
console.log(chalk.green(`\n${userName ? `You're welcome, ${userName}! š` : 'You\'re welcome! š'}`));
console.log(chalk.gray("\nLet me know if you need any more help with Git."));
return;
} catch (error) {
// Fallback if GitHub profile fetch fails
console.log(chalk.green("\nYou're welcome! š\nLet me know if you need any more help with Git."));
return;
}
}
// Check for unrelated questions
const gitKeywords = ['git', 'push', 'pull', 'commit', 'branch', 'merge', 'repo'];
const githubKeywords = ['github', 'pull request', 'pr', 'issue'];
const isGitRelated = gitKeywords.some(k => lowerQuery.includes(k)) ||
githubKeywords.some(k => lowerQuery.includes(k));
if (!isGitRelated) {
try {
const token = await ensureAuthenticated();
let userName = null;
if (token) {
const profile = await githubService.getUserProfile();
if (profile && (profile.name || profile.login)) {
userName = profile.login || profile.name;
}
}
console.log(chalk.yellow(`\n${userName ? `Hi ${userName},` : 'Hi there,'} I'm specialized in Git operations.`));
console.log(chalk.gray("I can help you with version control, repositories, and GitHub-related tasks."));
console.log(chalk.gray("What would you like to do with your code?"));
if (!userName) {
console.log(chalk.yellow("\nTip: Authenticate with GitHub for personalized experience using 'gitmate auth'"));
}
return;
} catch (error) {
// Fallback if GitHub profile fetch fails
console.log(chalk.yellow("\nHi there, I'm specialized in Git operations."));
console.log(chalk.gray("I can help you with version control, repositories, and GitHub-related tasks."));
console.log(chalk.gray("What would you like to do with your code?"));
return;
}
}
// Proceed with AI service for Git-related queries
const aiReady = await aiService.checkStatus();
if (!aiReady) {
console.error("AI service is not available. Please check your AI provider setup.");
const hasEnvConfig = process.env.MISTRAL_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
if (!hasEnvConfig) {
console.log("\nTo set up GitMate, you have two options:");
console.log("\nOption 1 - Environment Variables (Recommended):");
console.log(" export AI_PROVIDER=mistral");
console.log(" export MISTRAL_API_KEY=your_api_key_here");
console.log(" # Then run: gitmate \"your command\"");
console.log("\nOption 2 - Interactive Setup:");
console.log(" gitmate init");
console.log(" # This will guide you through configuration");
} else {
console.log("\nConfiguration issue detected. Please check your API keys.");
}
logger.error("AI service not ready, aborting NLP command.", { service: serviceName });
return;
}
try {
// Get user profile for personalized responses
let userName = null;
try {
const token = await ensureAuthenticated();
if (token) {
const profile = await githubService.getUserProfile();
if (profile && (profile.name || profile.login)) {
userName = profile.name || profile.login;
}
}
} catch (error) {
logger.warn('Failed to fetch GitHub profile', { error: error.message });
}
// Handle with AI service
const { response, requiresConfirmation, intent } = await aiService.handleUserQuery(query, userName);
// Output the response immediately
console.log(chalk.cyan("\n" + response));
// Only proceed with confirmation if needed
if (requiresConfirmation && intent) {
const confirmed = await handleConfirmationFlow(intent, userName);
if (confirmed) {
// Execute the actual Git operation based on intent
await executeGitOperation(intent, userName);
}
} else {
console.log(response);
}
} catch (error) {
console.error(`Error processing NLP query: ${error.message}`);
logger.error('Failed to process NLP query:', {
message: error.message,
stack: error.stack,
query,
service: serviceName
});
}
}
export async function executeGitOperation(intentObj, userName) {
const { intent, entities = {} } = intentObj;
// Normalize intent for common variants
let normalizedIntent = intent;
if ([
'list_repo', 'list_repos', 'list_repositories', 'list_repository'
].includes(intent)) {
normalizedIntent = 'list_repos';
} else if ([
'create_repo', 'create_repository', 'new_repo', 'new_repository'
].includes(intent)) {
normalizedIntent = 'create_repo';
} else if ([
'get_log', 'show_log', 'log', 'git_log'
].includes(intent)) {
normalizedIntent = 'get_log';
} else if ([
'get_diff', 'show_diff', 'diff', 'git_diff'
].includes(intent)) {
normalizedIntent = 'get_diff';
} else if ([
'get_status', 'show_status', 'status', 'git_status'
].includes(intent)) {
normalizedIntent = 'git_status';
} else if ([
'add_remote', 'remote_add', 'git_add_remote'
].includes(intent)) {
normalizedIntent = 'add_remote';
} else if ([
'get_remotes', 'list_remotes', 'remotes', 'git_remotes'
].includes(intent)) {
normalizedIntent = 'get_remotes';
} else if ([
'get_current_branch', 'current_branch', 'show_current_branch', 'branch_current'
].includes(intent)) {
normalizedIntent = 'get_current_branch';
} else if ([
'list_branches', 'branches', 'show_branches', 'get_branches'
].includes(intent)) {
normalizedIntent = 'list_branches';
} else if ([
'revert_commit', 'undo_commit', 'git_revert_commit'
].includes(intent)) {
normalizedIntent = 'revert_commit';
} else if ([
'create_and_checkout_branch', 'create_checkout_branch', 'new_and_checkout_branch'
].includes(intent)) {
normalizedIntent = 'create_and_checkout_branch';
} else if ([
'clone_repo', 'clone_repository', 'clone', 'git_clone', 'repo_clone'
].includes(intent)) {
normalizedIntent = 'clone_repo';
}
// Add more normalization rules as needed for other commands
// Helper function for interactive commit message handling
const getCommitMessage = async (prefilledMessage = '') => {
const { commitMessage } = await inquirer.prompt([
{
type: 'input',
name: 'commitMessage',
message: prefilledMessage
? `Commit message (press enter to use suggested or edit):`
: 'Enter commit message:',
default: prefilledMessage,
validate: input => input.trim() !== '' || 'Commit message cannot be empty'
}
]);
return commitMessage.trim();
};
try {
switch (normalizedIntent) {
case 'git_commit': {
let commitMessage = entities.commit_message;
if (!commitMessage) {
// Try to generate a commit mess