UNPKG

sushil-gitmate

Version:

Professional Git workflow automation powered by AI. Streamline your development process with natural language commands and intelligent automation.

209 lines (183 loc) 8.88 kB
#!/usr/bin/env node import dotenv from 'dotenv'; import { Command, Option } from 'commander'; import logger from './utils/logger.js'; import * as commandHandler from '../commands/commandHandler.js'; import { aiService, getCurrentProvider } from './services/aiServiceFactory.js'; dotenv.config(); const program = new Command(); async function main() { logger.info('GitBot CLI starting...', { service: 'CLI' }); console.log(process.argv); program .name('gitbot') .description('An intelligent GitBot powered by AI and GitHub integrations.') .version('1.0.0'); // Consider fetching from package.json // GitHub Repository Commands const repoCommand = program.command('repo') .description('Manage GitHub repositories.'); repoCommand .command('create <repo-name>') .description('Create a new GitHub repository.') .option('-d, --description <text>', 'Repository description') .option('-p, --private', 'Make the repository private (default is public)') .option('--no-init', 'Do not create an initial commit with a README') .action(async (repoName, options) => { const handlerOptions = [ repoName, ...(options.private ? ['--private'] : []), ...(options.description ? ['--description', options.description] : []), ...(options.init === false ? ['--no-init'] : []), // commander sets init to true by default if --no-init is not present ]; await commandHandler.handleRepoCommand(['create', ...handlerOptions]); }); repoCommand .command('list') .description('List your GitHub repositories.') .addOption(new Option('-t, --type <type>', 'Type of repositories to list').choices(['all', 'owner', 'public', 'private', 'member']).default('all')) .addOption(new Option('-s, --sort <sort>', 'Sort criteria').choices(['created', 'updated', 'pushed', 'full_name']).default('full_name')) .addOption(new Option('--direction <direction>', 'Sort direction').choices(['asc', 'desc']).default('asc')) .option('--per-page <number>', 'Number of items to return per page', '30') .option('--page <number>', 'Page number of the results to fetch', '1') .action(async (options) => { const handlerOptions = []; if(options.type) handlerOptions.push(`type=${options.type}`); if(options.sort) handlerOptions.push(`sort=${options.sort}`); if(options.direction) handlerOptions.push(`direction=${options.direction}`); if(options.perPage) handlerOptions.push(`per_page=${options.perPage}`); if(options.page) handlerOptions.push(`page=${options.page}`); await commandHandler.handleRepoCommand(['list', ...handlerOptions]); }); // Local Git Operations Commands const gitCommand = program.command('git') .description('Perform local Git operations.'); gitCommand .command('init [directory]') .description('Initialize a new Git repository in the specified or current directory.') .action(async (directory) => { await commandHandler.handleGitCommand(['init'], directory || '.'); }); gitCommand .command('status [directory]') .description('Show the working tree status.') .action(async (directory) => { await commandHandler.handleGitCommand(['status'], directory || '.'); }); gitCommand .command('add [files...]') .description('Add file contents to the index (stage files). Defaults to all files.') .action(async (files) => { await commandHandler.handleGitCommand(['add', ...(files.length > 0 ? files : ['.'])], '.'); }); gitCommand .command('commit <message>') .description('Record changes to the repository.') .action(async (message) => { await commandHandler.handleGitCommand(['commit', message], '.'); }); // AI / NLP Commands program .command('nlp <query>') .description('Use natural language to perform Git/GitHub actions (via Ollama).') .action(async (query) => { await commandHandler.handleNlpCommand(query); }); program .command('generate-gitignore <description>') .alias('gg') .description('Generate .gitignore content based on project description (e.g., "node react python").') .action(async (description) => { await commandHandler.handleGenerateGitignore(description); }); program .command('generate-commit [directory]') .alias('gc') .description('Generate a commit message based on the current diff in the specified or current directory.') .action(async (directory) => { await commandHandler.handleGenerateCommitMessage(directory || '.'); }); // Auth Commands const authCommand = program.command('auth') .description('Manage authentication.'); authCommand .command('logout') .description('Clear stored GitHub authentication token.') .action(async () => { await commandHandler.handleAuthLogout(); }); // Utility/Status Commands program .command('check-ai') .description('Check the status of the current AI service provider.') .action(async () => { const currentProvider = getCurrentProvider(); logger.info(`Checking ${currentProvider} service status...`, { service: 'CLI' }); const isReady = await aiService.checkStatus(); if (isReady) { console.log(`${currentProvider} service is up and ready to use.`); } else { console.error(`${currentProvider} service is not available. Please check your setup and .env configuration.`); } }); program .command('switch-ai') .description('Switch between different AI service providers.') .argument('<provider>', 'The AI provider to switch to (ollama or mistral)') .action(async (provider) => { await commandHandler.handleSwitchAIProvider(provider); }); // Global options program.option('-v, --verbose', 'Enable verbose logging for this command'); program.on('option:verbose', () => { // This is a simple way; a more robust solution might involve a custom logger level // or passing a verbosity flag to handlers. // For now, we can adjust the logger's console transport level if needed, // but winston's levels are fixed once set. This is more for future thought. logger.transports.find(t => t.name === 'console').level = 'debug'; logger.debug('Verbose logging enabled via CLI flag.', { service: 'CLI' }); }); // Listener for unknown commands, treating them as NLP queries program.on('command:*', async (operands) => { // 'operands' is an array of the parts of the command that were not recognized. // e.g., if user types "gitbot i need to push", operands would be ['i', 'need', 'to', 'push'] const nlpQuery = operands.join(' '); if (nlpQuery.trim()) { // If there's an actual query logger.info(`Unknown command sequence treated as NLP query: "${nlpQuery}"`, { service: 'CLI' }); await commandHandler.handleNlpCommand(nlpQuery); } else { // This case means 'command:*' was triggered but operands were empty or whitespace. logger.warn('Empty or unhandled NLP query from unknown command sequence. Displaying help.', { service: 'CLI' }); program.outputHelp(); } }); try { await program.parseAsync(process.argv); // After parsing, if no command was explicitly run (e.g., only 'gitbot' was typed), // and 'command:*' didn't handle it as an NLP query (e.g., because it was empty or only global options were passed), // then display help. // Commander's `parseAsync` doesn't throw for unknown commands if `command:*` is listened to. // We check if any actual command arguments were passed beyond the script name. if (process.argv.slice(2).length === 0) { // This handles the case of 'gitbot' with no arguments. program.outputHelp(); } // If arguments were passed, they should either match a known command // or be caught by 'command:*'. If 'command:*' was triggered and the query was empty, // it would have called program.outputHelp() itself. } catch (error) { // This catch block handles errors from command actions or critical parsing errors. logger.error('CLI execution error:', { message: error.message, stack: error.stack, service: 'CLI' }); if (error.message !== 'Authentication required. Please visit the auth URL and try again.') { // Exit for general errors, but not for auth prompts from handlers. process.exit(1); } else { process.exitCode = 1; // Indicate failure for auth prompts but allow message to be primary output. } } } main().catch(error => { // This catch is for unhandled promise rejections from main itself, // though most errors should be caught within main's try/catch or by Commander. logger.fatal('Unhandled error in CLI main function:', { message: error.message, stack: error.stack, service: 'CLI' }); process.exit(1); });