UNPKG

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
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