Skip to content
23 changes: 22 additions & 1 deletion source/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import process from 'node:process';
import {Command} from 'commander';
import packageJSON from '../package.json';
import {showBanner, showWarning} from './utils/ui.js';
import {showBanner, showWarning, showSuccess, showError} from './utils/ui.js';
import {commitCommand} from './commands/commit.js';
import {
authenticateWithCopilot,
Expand All @@ -11,6 +11,8 @@ import {
logout,
} from './commands/auth.js';
import {showConfig, resetConfig} from './commands/config.js';
import {setConvention} from './utils/config-manager.js';
import {CONVENTIONS} from './utils/commit-conventions.js';

const program = new Command();

Expand Down Expand Up @@ -44,6 +46,10 @@ program
.option('--all', 'Process all files at once')
.option('--file <path>', 'Process specific file')
.option('--model <model>', 'Specify AI model (e.g., gpt-4, gpt-3.5-turbo)')
.option(
'--convention <type>',
'Commit convention (clean, conventional, gitmoji, simple)',
)
.action(options => {
commitCommand(options);
});
Expand Down Expand Up @@ -83,6 +89,21 @@ authCmd
// Config command
const configCmd = program.command('config').description('Manage configuration');

configCmd
.command('set-convention <type>')
.description('Set default commit convention')
.action(type => {
const validConventions = Object.keys(CONVENTIONS);
if (!validConventions.includes(type)) {
showError(`Invalid convention: ${type}`);
console.log(`Available: ${validConventions.join(', ')}`);
process.exit(1);
}

setConvention(type);
showSuccess(`Default convention set to: ${type}`);
});

configCmd
.option('--show', 'Show current configuration')
.option('--reset', 'Reset configuration to defaults')
Expand Down
50 changes: 25 additions & 25 deletions source/commands/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import process from 'node:process';
import {execa} from 'execa';
import {
setAuthMode,
setToken,
Expand All @@ -18,46 +19,45 @@ import {
* Handles GitHub Copilot and OpenAI authentication
*/

export async function authenticateWithCopilot(token = null) {
export async function authenticateWithCopilot() {
try {
// Check for token in arguments first
let authToken = token;
console.log('πŸ” Setting up GitHub Copilot authentication...\n');

// If no token provided, check environment variables
if (!authToken) {
authToken =
process.env.COPILOT_GITHUB_TOKEN ||
process.env.GH_TOKEN ||
process.env.GITHUB_TOKEN;
// Check if gh CLI is installed
try {
await execa('gh', ['--version']);
} catch {
showError('GitHub CLI (gh) is not installed.');
console.log('');
console.log('Install it from: https://cli.github.com/');
console.log('Then run: gh auth login');
process.exit(1);
}

if (!authToken) {
showError('No GitHub token found.');
// Check if gh is authenticated
try {
await execa('gh', ['auth', 'status']);
} catch {
showError('GitHub CLI is not authenticated.');
console.log('');
console.log('Please provide a token using one of these methods:');
console.log(' 1. Pass token: magicc auth copilot --token <your-token>');
console.log(' 2. Set environment variable: GITHUB_TOKEN or GH_TOKEN');
console.log(
' 3. Use gh CLI: gh auth login (then use: magicc auth copilot)',
);
console.log('Please authenticate first:');
console.log(' gh auth login');
console.log('');
console.log('To create a token:');
console.log(' Visit: https://github.com/settings/tokens');
console.log(' Scopes needed: repo, read:user');
console.log('Make sure you have GitHub Copilot enabled on your account.');
process.exit(1);
}

// Store token and set auth mode
setToken('github', authToken);
// Store auth mode
setAuthMode('copilot');

showSuccess('GitHub Copilot authentication successful!');
showSuccess('GitHub Copilot ready!');
console.log('');
console.log(`πŸ“ Config stored at: ${getConfigPath()}`);
console.log('βœ… GitHub CLI authenticated');
console.log('βœ… Copilot SDK will use your CLI session');
console.log('');
console.log('You can now use: magicc commit');
} catch (error) {
showError(`Authentication failed: ${error.message}`);
showError(`Setup failed: ${error.message}`);
process.exit(1);
}
}
Expand Down
148 changes: 103 additions & 45 deletions source/commands/commit.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
showInfo,
confirmCommit,
} from '../utils/ui.js';
import {getAuthMode} from '../utils/config-manager.js';
import {getAuthMode, getConvention} from '../utils/config-manager.js';

/**
* Main commit command logic
Expand Down Expand Up @@ -137,35 +137,66 @@ export async function processFilesInteractively(aiProvider, options = {}) {
continue;
}

// Generate commit message
try {
const message = await aiProvider.generateCommitMessage(
diff,
file,
options,
);
let currentConvention = options.convention || getConvention();
let fileProcessed = false;
let attempts = 0;
const maxAttempts = 5;

while (!fileProcessed && attempts < maxAttempts) {
try {
const message = await aiProvider.generateCommitMessage(diff, file, {
...options,
convention: currentConvention,
});

const result = await confirmCommit(message, file, currentConvention);

switch (result.action) {
case 'accept': {
const success = await commit(result.message);
if (success) {
showSuccess(`Committed: ${file}`);
console.log(`πŸ“ ${result.message}`);
committed++;
} else {
showError(`Failed to commit ${file}`);
await unstageFile(file);
skipped++;
}

// Confirm and commit
const result = await confirmCommit(message, file);

if (result.action === 'accept') {
const success = await commit(result.message);
if (success) {
showSuccess(`Committed: ${file}`);
console.log(`πŸ“ ${result.message}`);
committed++;
} else {
showError(`Failed to commit ${file}`);
await unstageFile(file);
skipped++;
fileProcessed = true;
break;
}

case 'regenerate': {
currentConvention = result.convention;
attempts++;
// Loop continues to regenerate
break;
}

case 'skip': {
showInfo(`Skipped: ${file}`);
await unstageFile(file);
skipped++;
fileProcessed = true;
break;
}
// No default
}
} else if (result.action === 'skip') {
showInfo(`Skipped: ${file}`);
} catch (error) {
showError(`Error processing ${file}: ${error.message}`);
await unstageFile(file);
skipped++;
fileProcessed = true;
}
} catch (error) {
showError(`Error processing ${file}: ${error.message}`);
}

// Check if max attempts reached without processing
if (!fileProcessed && attempts >= maxAttempts) {
showWarning(
`Maximum regeneration attempts (${maxAttempts}) reached for ${file}`,
);
await unstageFile(file);
skipped++;
}
Expand All @@ -178,7 +209,6 @@ export async function processFilesInteractively(aiProvider, options = {}) {
console.log(` ⏭️ Skipped: ${skipped} file(s)`);
console.log('═'.repeat(50) + '\n');
}
/* eslint-enable no-await-in-loop */

export async function processFile(filePath, aiProvider, options = {}) {
showInfo(`Processing single file: ${filePath}`);
Expand All @@ -199,27 +229,55 @@ export async function processFile(filePath, aiProvider, options = {}) {
return;
}

// Generate commit message
showInfo('Generating commit message...');
const message = await aiProvider.generateCommitMessage(
diff,
filePath,
options,
);
let currentConvention = options.convention || getConvention();
let attempts = 0;
const maxAttempts = 5;

// Confirm and commit
const result = await confirmCommit(message, filePath);
while (attempts < maxAttempts) {
showInfo('Generating commit message...');

if (result.action === 'accept') {
const success = await commit(result.message);
if (success) {
showSuccess('Changes committed successfully!');
console.log(`πŸ“ ${result.message}`);
} else {
showError('Failed to commit changes.');
try {
const message = await aiProvider.generateCommitMessage(diff, filePath, {
...options,
convention: currentConvention,
});

const result = await confirmCommit(message, filePath, currentConvention);

switch (result.action) {
case 'accept': {
const success = await commit(result.message);
if (success) {
showSuccess('Changes committed successfully!');
console.log(`πŸ“ ${result.message}`);
} else {
showError('Failed to commit changes.');
}

return;
}

case 'regenerate': {
currentConvention = result.convention;
attempts++;
continue;
}

case 'skip': {
showInfo('Commit cancelled.');
await unstageFile(filePath);
return;
}
// No default
}
} catch (error) {
showError(`Error: ${error.message}`);
await unstageFile(filePath);
return;
}
} else if (result.action === 'skip') {
showInfo('Commit cancelled.');
await unstageFile(filePath);
}

showWarning('Maximum regeneration attempts reached.');
await unstageFile(filePath);
}
/* eslint-enable no-await-in-loop */
11 changes: 11 additions & 0 deletions source/commands/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getConfigPath,
} from '../utils/config-manager.js';
import {showSuccess, showInfo} from '../utils/ui.js';
import {getConvention, CONVENTIONS} from '../utils/commit-conventions.js';

/**
* Configuration commands
Expand All @@ -31,6 +32,16 @@ export function showConfig() {
displayValue = value.slice(0, 10) + '...';
}

// Show convention name nicely
if (key === 'convention') {
const conv = getConvention(value);
// Check if the stored value matches a valid convention
const validConventions = Object.keys(CONVENTIONS);
displayValue = validConventions.includes(value)
? `${value} (${conv.name})`
: `${value} (invalid - defaults to ${conv.name})`;
}

console.log(` ${chalk.yellow(key)}: ${chalk.white(displayValue)}`);
}
}
Expand Down
Loading