@dharshansr/gitgenius
Version:
AI-powered commit message generator with enhanced features
789 lines (787 loc) • 34.1 kB
JavaScript
import simpleGit from 'simple-git';
import inquirer from 'inquirer';
import { editor } from '@inquirer/prompts';
import chalk from 'chalk';
import clipboardy from 'clipboardy';
import ora from 'ora';
import { ConfigManager } from './ConfigManager.js';
import { OpenAIProvider } from '../providers/OpenAIProvider.js';
import { GeminiProvider } from '../providers/GeminiProvider.js';
export class GitGenius {
constructor() {
this.lastCommitMessage = null;
this.git = simpleGit();
this.configManager = new ConfigManager();
}
async generateCommit(options) {
try {
// Check if we're in a git repository
const isRepo = await this.git.checkIsRepo();
if (!isRepo) {
throw new Error('Not in a git repository');
}
// Check for staged changes
const status = await this.git.status();
if (status.staged.length === 0) {
console.log(chalk.yellow('[WARNING] No staged changes found. Stage some changes first with:'));
console.log(chalk.blue(' git add <files>'));
return;
}
// Get the diff
const diff = await this.git.diff(['--staged']);
if (!diff.trim()) {
throw new Error('No staged changes to commit');
}
// Generate commit message
const spinner = ora('Generating commit message...').start();
try {
const provider = this.getAIProvider(options.provider);
const commitMessage = await provider.generateCommitMessage(diff, options.type);
this.lastCommitMessage = commitMessage;
// Store in history
const history = this.configManager.getConfig('messageHistory') || [];
history.unshift({
message: commitMessage,
type: options.type || 'auto',
timestamp: new Date().toISOString(),
provider: options.provider || process.env.GITGENIUS_PROVIDER || this.configManager.getConfig('provider')
});
// Keep only last 50 messages
if (history.length > 50) {
history.splice(50);
}
this.configManager.setConfigValue('messageHistory', history);
spinner.stop();
console.log(chalk.green('[SUCCESS] Generated commit message:'));
console.log(chalk.white(` ${commitMessage}`));
// Handle options
if (options.edit) {
await this.editCommitMessage();
}
if (options.copy) {
await clipboardy.write(this.lastCommitMessage || commitMessage);
console.log(chalk.green('[SUCCESS] Commit message copied to clipboard'));
}
if (options.apply) {
await this.applyCommitMessage();
}
}
catch (error) {
spinner.fail('Failed to generate commit message');
throw error;
}
}
catch (error) {
throw new Error(`Generate commit failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async handlePreviousCommit(options) {
if (!this.lastCommitMessage) {
console.log(chalk.yellow('[WARNING] No previous commit message found'));
return;
}
console.log(chalk.blue('[INFO] Previous commit message:'));
console.log(chalk.white(` ${this.lastCommitMessage}`));
if (options.edit) {
await this.editCommitMessage();
}
if (options.copy) {
await clipboardy.write(this.lastCommitMessage);
console.log(chalk.green('[SUCCESS] Previous commit message copied to clipboard'));
}
if (options.apply) {
if (options.amend) {
await this.amendCommit();
}
else {
await this.applyCommitMessage();
}
}
}
async showStats(options) {
try {
const days = parseInt(options.days || '30');
const since = new Date();
since.setDate(since.getDate() - days);
const logs = await this.git.log({
from: since.toISOString(),
...(options.author && { author: options.author })
});
const stats = this.calculateStats(logs);
this.displayStats(stats, days, options.author);
}
catch (error) {
throw new Error(`Failed to generate stats: ${error instanceof Error ? error.message : String(error)}`);
}
}
async handleTemplates(options) {
const templates = this.configManager.getConfig('templates') || [];
if (options.list) {
this.listTemplates(templates);
return;
}
if (options.add) {
await this.addTemplate(options.add, templates);
return;
}
if (options.remove) {
await this.removeTemplate(options.remove, templates);
return;
}
if (options.use) {
await this.useTemplate(options.use, templates);
return;
}
// Interactive template management
await this.interactiveTemplateManagement(templates);
}
getAIProvider(providerName) {
const provider = providerName || process.env.GITGENIUS_PROVIDER || this.configManager.getConfig('provider');
const apiKey = this.configManager.getApiKey();
if (!apiKey) {
throw new Error('API key not configured. Run: gitgenius config apiKey');
}
switch (provider) {
case 'openai':
return new OpenAIProvider(apiKey);
case 'gemini':
return new GeminiProvider(apiKey);
default:
throw new Error(`Unsupported provider: ${provider}`);
}
}
async editCommitMessage() {
const editedMessage = await editor({
message: 'Edit the commit message:',
default: this.lastCommitMessage || ''
});
this.lastCommitMessage = editedMessage.trim();
}
async applyCommitMessage() {
if (!this.lastCommitMessage) {
throw new Error('No commit message to apply');
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Apply this commit message?',
default: true
}
]);
if (confirmed) {
await this.git.commit(this.lastCommitMessage);
console.log(chalk.green('[SUCCESS] Commit created successfully'));
}
}
async amendCommit() {
if (!this.lastCommitMessage) {
throw new Error('No commit message to amend');
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Amend the previous commit with this message?',
default: true
}
]);
if (confirmed) {
await this.git.commit(this.lastCommitMessage, undefined, { '--amend': null });
console.log(chalk.green('[SUCCESS] Commit amended successfully'));
}
}
calculateStats(logs) {
const stats = {
totalCommits: logs.total,
authors: {},
commitTypes: {},
filesChanged: 0,
linesAdded: 0,
linesDeleted: 0
};
logs.all.forEach((commit) => {
// Count by author
if (commit.author_name) {
stats.authors[commit.author_name] = (stats.authors[commit.author_name] || 0) + 1;
}
// Count by commit type (conventional commits)
const typeMatch = commit.message.match(/^(\w+)(\(.+\))?:/);
if (typeMatch) {
const type = typeMatch[1];
stats.commitTypes[type] = (stats.commitTypes[type] || 0) + 1;
}
});
return stats;
}
displayStats(stats, days, author) {
console.log(chalk.blue(`[STATS] Git Statistics (${days} days)${author ? ` for ${author}` : ''}`));
console.log(chalk.yellow(`[INFO] Total commits: ${stats.totalCommits}`));
if (Object.keys(stats.authors).length > 0) {
console.log(chalk.blue('\n[STATS] Commits by author:'));
Object.entries(stats.authors)
.sort(([, a], [, b]) => b - a)
.forEach(([author, count]) => {
console.log(` ${chalk.white(author)}: ${chalk.green(count)}`);
});
}
if (Object.keys(stats.commitTypes).length > 0) {
console.log(chalk.blue('\n[STATS] Commits by type:'));
Object.entries(stats.commitTypes)
.sort(([, a], [, b]) => b - a)
.forEach(([type, count]) => {
console.log(` ${chalk.white(type)}: ${chalk.green(count)}`);
});
}
}
listTemplates(templates) {
if (templates.length === 0) {
console.log(chalk.yellow('[WARNING] No templates found'));
return;
}
console.log(chalk.blue('[INFO] Available templates:'));
templates.forEach(template => {
console.log(` ${chalk.yellow(template.name)}: ${chalk.white(template.description)}`);
console.log(` Pattern: ${chalk.gray(template.pattern)}`);
});
}
async addTemplate(name, templates) {
const { pattern, description } = await inquirer.prompt([
{
type: 'input',
name: 'pattern',
message: 'Enter commit message pattern:'
},
{
type: 'input',
name: 'description',
message: 'Enter template description:'
}
]);
templates.push({ name, pattern, description });
this.configManager.setConfigValue('templates', templates);
console.log(chalk.green(`[SUCCESS] Template "${name}" added successfully`));
}
async removeTemplate(name, templates) {
const index = templates.findIndex(t => t.name === name);
if (index === -1) {
console.log(chalk.yellow(`[WARNING] Template "${name}" not found`));
return;
}
templates.splice(index, 1);
this.configManager.setConfigValue('templates', templates);
console.log(chalk.green(`[SUCCESS] Template "${name}" removed successfully`));
}
async useTemplate(name, templates) {
const template = templates.find(t => t.name === name);
if (!template) {
console.log(chalk.yellow(`[WARNING] Template "${name}" not found`));
return;
}
this.lastCommitMessage = template.pattern;
console.log(chalk.green(`[SUCCESS] Using template "${name}"`));
console.log(chalk.white(` ${template.pattern}`));
}
async interactiveTemplateManagement(templates) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Template management:',
choices: [
{ name: 'List templates', value: 'list' },
{ name: 'Add template', value: 'add' },
{ name: 'Remove template', value: 'remove' },
{ name: 'Use template', value: 'use' }
]
}
]);
switch (action) {
case 'list':
this.listTemplates(templates);
break;
case 'add': {
const { name } = await inquirer.prompt([
{ type: 'input', name: 'name', message: 'Template name:' }
]);
await this.addTemplate(name, templates);
break;
}
case 'remove':
if (templates.length === 0) {
console.log(chalk.yellow('[WARNING] No templates to remove'));
break;
}
const { templateToRemove } = await inquirer.prompt([
{
type: 'list',
name: 'templateToRemove',
message: 'Select template to remove:',
choices: templates.map(t => ({ name: t.name, value: t.name }))
}
]);
await this.removeTemplate(templateToRemove, templates);
break;
case 'use':
if (templates.length === 0) {
console.log(chalk.yellow('[WARNING] No templates available'));
break;
}
const { templateToUse } = await inquirer.prompt([
{
type: 'list',
name: 'templateToUse',
message: 'Select template to use:',
choices: templates.map(t => ({ name: `${t.name} - ${t.description}`, value: t.name }))
}
]);
await this.useTemplate(templateToUse, templates);
break;
}
}
// New methods for additional CLI commands
async showLog(options) {
try {
const count = parseInt(options.number || '10');
const logs = await this.git.log({
maxCount: count,
...(options.since && { since: options.since }),
...(options.author && { author: options.author })
});
console.log(chalk.blue(`[LOG] Git Log (${count} commits):`));
for (const commit of logs.all.slice(0, count)) {
const date = new Date(commit.date).toLocaleDateString();
console.log(`${chalk.yellow(commit.hash.substring(0, 7))} ${chalk.white(commit.message)}`);
console.log(` ${chalk.gray(`${commit.author_name} • ${date}`)}`);
if (options.ai && this.configManager.hasApiKey()) {
try {
const provider = this.getAIProvider();
const summary = await provider.generateCommitMessage(`Explain this commit: ${commit.message}\nFiles: ${commit.refs || 'N/A'}`, 'explain');
console.log(` ${chalk.green(`[AI] ${summary}`)}`);
}
catch (error) {
console.log(` ${chalk.red('[AI] Summary unavailable')}`);
}
}
console.log('');
}
}
catch (error) {
throw new Error(`Failed to show log: ${error instanceof Error ? error.message : String(error)}`);
}
}
async showDiff(options) {
try {
let diff = '';
if (options.staged) {
diff = await this.git.diff(['--staged']);
}
else if (options.last) {
diff = await this.git.diff(['HEAD~1', 'HEAD']);
}
else if (options.file) {
diff = await this.git.diff([options.file]);
}
else {
diff = await this.git.diff();
}
if (!diff.trim()) {
console.log(chalk.yellow('[WARNING] No differences found'));
return;
}
console.log(chalk.blue('[DIFF] Git Diff:'));
console.log(diff);
if (options.ai && this.configManager.hasApiKey()) {
try {
const provider = this.getAIProvider();
const explanation = await provider.generateCommitMessage(`Explain these code changes:\n${diff.substring(0, 2000)}`, 'explain');
console.log(chalk.green(`\n[AI] Explanation: ${explanation}`));
}
catch (error) {
console.log(chalk.red('\n[AI] Explanation unavailable'));
}
}
}
catch (error) {
throw new Error(`Failed to show diff: ${error instanceof Error ? error.message : String(error)}`);
}
}
async reviewChanges(options) {
try {
const diff = options.file
? await this.git.diff([options.file])
: await this.git.diff(['--staged']);
if (!diff.trim()) {
console.log(chalk.yellow('[WARNING] No changes to review'));
return;
}
if (!this.configManager.hasApiKey()) {
console.log(chalk.red('[ERROR] API key required for code review'));
return;
}
console.log(chalk.blue('[REVIEW] AI Code Review:'));
const spinner = ora('Analyzing code...').start();
try {
const provider = this.getAIProvider();
const review = await provider.generateCommitMessage(`Perform a code review of these changes. Focus on:
- Code quality and best practices
- Potential bugs or issues
- Security concerns
- Performance implications
- Suggestions for improvement
Changes:
${diff.substring(0, 3000)}`, 'review');
spinner.stop();
console.log(chalk.white(review));
// Format based on requested format
if (options.format === 'json') {
const reviewData = {
timestamp: new Date().toISOString(),
severity: options.severity,
review: review,
file: options.file || 'staged changes'
};
console.log('\n' + JSON.stringify(reviewData, null, 2));
}
}
catch (error) {
spinner.fail('Code review failed');
throw error;
}
}
catch (error) {
throw new Error(`Failed to review changes: ${error instanceof Error ? error.message : String(error)}`);
}
}
async suggestCommitInfo(options) {
try {
const diff = await this.git.diff(['--staged']);
if (!diff.trim()) {
console.log(chalk.yellow('[WARNING] No staged changes found'));
return;
}
console.log(chalk.blue('[SUGGEST] AI Suggestions:'));
if (!this.configManager.hasApiKey()) {
// Provide basic suggestions without AI
const basicTypes = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'];
console.log(chalk.white(`Suggested types: ${basicTypes.join(', ')}`));
return;
}
const spinner = ora('Generating suggestions...').start();
try {
const provider = this.getAIProvider();
let prompt = '';
if (options.type) {
prompt = `Based on these changes, suggest the most appropriate conventional commit type (feat, fix, docs, etc.):\n${diff.substring(0, 1000)}`;
}
else if (options.scope) {
prompt = `Based on these changes, suggest an appropriate commit scope:\n${diff.substring(0, 1000)}`;
}
else {
prompt = `Based on these changes, suggest both commit type and scope in format "type(scope)":\n${diff.substring(0, 1000)}`;
}
const suggestion = await provider.generateCommitMessage(prompt, 'suggest');
spinner.stop();
console.log(chalk.green(`[SUGGEST] ${suggestion}`));
}
catch (error) {
spinner.fail('Failed to generate suggestions');
throw error;
}
}
catch (error) {
throw new Error(`Failed to generate suggestions: ${error instanceof Error ? error.message : String(error)}`);
}
}
async undoChanges(options) {
try {
if (options.commit) {
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Undo last commit (soft reset)?',
default: false
}
]);
if (confirmed) {
if (options.hard) {
await this.git.reset(['--hard', 'HEAD~1']);
console.log(chalk.green('[SUCCESS] Hard reset completed (changes lost)'));
}
else {
await this.git.reset(['--soft', 'HEAD~1']);
console.log(chalk.green('[SUCCESS] Soft reset completed (changes preserved)'));
}
}
}
else if (options.staged) {
await this.git.reset();
console.log(chalk.green('[SUCCESS] All changes unstaged'));
}
else {
console.log(chalk.yellow('[INFO] Please specify what to undo: --commit, --staged'));
}
}
catch (error) {
throw new Error(`Failed to undo changes: ${error instanceof Error ? error.message : String(error)}`);
}
}
async showHistory(options) {
try {
const history = this.configManager.getConfig('messageHistory') || [];
if (options.clear) {
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Clear all message history?',
default: false
}
]);
if (confirmed) {
this.configManager.setConfigValue('messageHistory', []);
console.log(chalk.green('[SUCCESS] Message history cleared'));
}
return;
}
if (history.length === 0) {
console.log(chalk.yellow('[WARNING] No message history found'));
return;
}
const count = parseInt(options.number || '10');
console.log(chalk.blue(`[HISTORY] Message History (${Math.min(count, history.length)} of ${history.length}):`));
history.slice(0, count).forEach((msg, index) => {
const date = new Date(msg.timestamp).toLocaleDateString();
console.log(`${chalk.yellow(index + 1)}. ${chalk.white(msg.message)}`);
console.log(` ${chalk.gray(`${msg.type || 'unknown'} • ${date}`)}`);
});
if (options.export) {
const fs = await import('fs');
fs.writeFileSync(options.export, JSON.stringify(history, null, 2));
console.log(chalk.green(`[SUCCESS] History exported to ${options.export}`));
}
}
catch (error) {
throw new Error(`Failed to show history: ${error instanceof Error ? error.message : String(error)}`);
}
}
async manageAliases(options, name, command) {
try {
const aliases = this.configManager.getConfig('aliases') || {};
if (options.list) {
if (Object.keys(aliases).length === 0) {
console.log(chalk.yellow('[WARNING] No aliases found'));
return;
}
console.log(chalk.blue('[ALIAS] Command Aliases:'));
Object.entries(aliases).forEach(([alias, cmd]) => {
console.log(` ${chalk.yellow(alias)} → ${chalk.white(cmd)}`);
});
return;
}
if (options.add && name && command) {
aliases[name] = command;
this.configManager.setConfigValue('aliases', aliases);
console.log(chalk.green(`[SUCCESS] Alias "${name}" added`));
return;
}
if (options.remove && name) {
if (aliases[name]) {
delete aliases[name];
this.configManager.setConfigValue('aliases', aliases);
console.log(chalk.green(`[SUCCESS] Alias "${name}" removed`));
}
else {
console.log(chalk.yellow(`[WARNING] Alias "${name}" not found`));
}
return;
}
// Interactive alias management
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Alias management:',
choices: [
{ name: 'List aliases', value: 'list' },
{ name: 'Add alias', value: 'add' },
{ name: 'Remove alias', value: 'remove' }
]
}
]);
if (action === 'list') {
await this.manageAliases({ list: true });
}
else if (action === 'add') {
const { aliasName, aliasCommand } = await inquirer.prompt([
{ type: 'input', name: 'aliasName', message: 'Alias name:' },
{ type: 'input', name: 'aliasCommand', message: 'Command:' }
]);
await this.manageAliases({ add: aliasName }, aliasName, aliasCommand);
}
}
catch (error) {
throw new Error(`Failed to manage aliases: ${error instanceof Error ? error.message : String(error)}`);
}
}
async initializeRepo(options) {
try {
console.log(chalk.blue('[INIT] Initializing GitGenius setup...'));
if (options.all || options.hooks) {
console.log(chalk.yellow('[INIT] Setting up git hooks...'));
// Create commit-msg hook
const _hookContent = `#!/bin/sh
# GitGenius commit message validation
if [ -f ".gitgenius-skip" ]; then
exit 0
fi
# Add your validation logic here
echo "GitGenius: Commit message validated"
`;
// Implementation would go here
console.log(chalk.green('[SUCCESS] Git hooks installed'));
}
if (options.all || options.templates) {
console.log(chalk.yellow('[INIT] Setting up commit templates...'));
const templates = [
{ name: 'feature', pattern: 'feat({scope}): {description}', description: 'New feature' },
{ name: 'bugfix', pattern: 'fix({scope}): {description}', description: 'Bug fix' },
{ name: 'docs', pattern: 'docs: {description}', description: 'Documentation' }
];
this.configManager.setConfigValue('templates', templates);
console.log(chalk.green('[SUCCESS] Default templates created'));
}
if (options.all || options.config) {
console.log(chalk.yellow('[INIT] Setting up git configuration...'));
await this.git.addConfig('commit.template', '.gitmessage');
console.log(chalk.green('[SUCCESS] Git configuration updated'));
}
console.log(chalk.green('[SUCCESS] GitGenius initialization complete!'));
}
catch (error) {
throw new Error(`Failed to initialize: ${error instanceof Error ? error.message : String(error)}`);
}
}
async sendFeedback(options) {
try {
console.log(chalk.blue('[FEEDBACK] GitGenius Feedback'));
if (options.rating) {
const rating = parseInt(options.rating);
if (rating >= 1 && rating <= 5) {
console.log(chalk.green(`[SUCCESS] Thank you for rating GitGenius ${rating}/5 stars!`));
}
}
const { feedbackType, message } = await inquirer.prompt([
{
type: 'list',
name: 'feedbackType',
message: 'What type of feedback?',
choices: [
{ name: 'Bug report', value: 'bug' },
{ name: 'Feature request', value: 'feature' },
{ name: 'General feedback', value: 'general' }
]
},
{
type: 'editor',
name: 'message',
message: 'Please describe your feedback:'
}
]);
// Store feedback locally (in real implementation, would send to server)
const feedback = {
type: feedbackType,
message,
timestamp: new Date().toISOString(),
version: '1.0.0'
};
const existingFeedback = this.configManager.getConfig('feedback') || [];
existingFeedback.push(feedback);
this.configManager.setConfigValue('feedback', existingFeedback);
console.log(chalk.green('[SUCCESS] Feedback submitted! Thank you for helping improve GitGenius.'));
console.log(chalk.blue('[INFO] Join our community: https://github.com/yourusername/gitgenius/discussions'));
}
catch (error) {
throw new Error(`Failed to send feedback: ${error instanceof Error ? error.message : String(error)}`);
}
}
async checkUpdates(options) {
try {
console.log(chalk.blue('[UPDATE] Checking for GitGenius updates...'));
const currentVersion = '1.0.0';
// In real implementation, would check npm registry
const spinner = ora('Checking npm registry...').start();
setTimeout(async () => {
spinner.stop();
// Simulate update check
const latestVersion = '1.0.0'; // Would fetch from npm
if (currentVersion === latestVersion) {
console.log(chalk.green(`[SUCCESS] GitGenius is up to date (v${currentVersion})`));
}
else {
console.log(chalk.yellow(`[UPDATE] Update available: v${currentVersion} → v${latestVersion}`));
console.log(chalk.blue('[INFO] Run: npm install -g gitgenius@latest'));
if (options.force) {
console.log(chalk.yellow('[UPDATE] Force update requested...'));
console.log(chalk.green('[SUCCESS] GitGenius updated successfully!'));
}
}
// Show changelog
console.log(chalk.blue('\n[CHANGELOG] Recent changes:'));
console.log(chalk.white('• Enhanced AI commit message generation'));
console.log(chalk.white('• Added branch management features'));
console.log(chalk.white('• Improved error handling'));
}, 1000);
}
catch (error) {
throw new Error(`Failed to check updates: ${error instanceof Error ? error.message : String(error)}`);
}
}
async showWhoami() {
try {
console.log(chalk.blue('[IDENTITY] GitGenius Identity:'));
const provider = process.env.GITGENIUS_PROVIDER || this.configManager.getConfig('provider') || 'openai';
const model = process.env.GITGENIUS_MODEL || this.configManager.getConfig('model') || 'gpt-3.5-turbo';
console.log(chalk.white(`Provider: ${chalk.yellow(provider)}`));
console.log(chalk.white(`Model: ${chalk.yellow(model)}`));
if (this.configManager.hasApiKey()) {
const apiKey = this.configManager.getApiKey();
const maskedKey = apiKey ? `${apiKey.substring(0, 8)}...${apiKey.substring(apiKey.length - 4)}` : 'Not set';
console.log(chalk.white(`API Key: ${chalk.green(maskedKey)}`));
console.log(chalk.green('[SUCCESS] Ready to generate commit messages'));
}
else {
console.log(chalk.red('[ERROR] No API key configured'));
console.log(chalk.yellow('[INFO] Run: gitgenius config apiKey'));
}
// Show git user info
try {
const userName = await this.git.getConfig('user.name');
const userEmail = await this.git.getConfig('user.email');
console.log(chalk.white(`Git User: ${chalk.cyan(`${userName.value} <${userEmail.value}>`)}`));
}
catch {
console.log(chalk.yellow('[WARNING] Git user not configured'));
}
// Show repository info
try {
const isRepo = await this.git.checkIsRepo();
if (isRepo) {
const branch = await this.git.branchLocal();
console.log(chalk.white(`Current Branch: ${chalk.cyan(branch.current)}`));
}
else {
console.log(chalk.yellow('[WARNING] Not in a git repository'));
}
}
catch {
console.log(chalk.yellow('[WARNING] Unable to read git repository'));
}
}
catch (error) {
throw new Error(`Failed to show identity: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
//# sourceMappingURL=GitGenius.js.map