From 6595812be9d8968fcbeca173179885133b6a0cbc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:50:22 +0000 Subject: [PATCH 1/9] Initial plan From 3f9f61ac638ddc408f48aa841190bc8756e54f69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:54:43 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=A6=20new:=20implement=20multi-con?= =?UTF-8?q?vention=20support=20and=20Copilot=20SDK=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/cli.js | 21 ++++ source/commands/auth.js | 48 +++++----- source/commands/commit.js | 121 ++++++++++++++--------- source/commands/config.js | 7 ++ source/providers/ai-provider.js | 122 +++++++++++------------- source/utils/commit-conventions.js | 148 +++++++++++++++++++++++++++++ source/utils/config-manager.js | 9 ++ source/utils/ui.js | 39 +++++++- 8 files changed, 379 insertions(+), 136 deletions(-) create mode 100644 source/utils/commit-conventions.js diff --git a/source/cli.js b/source/cli.js index c9d8495..4ece87a 100755 --- a/source/cli.js +++ b/source/cli.js @@ -11,6 +11,8 @@ import { logout, } from './commands/auth.js'; import {showConfig, resetConfig} from './commands/config.js'; +import {setConvention, getConvention} from './utils/config-manager.js'; +import {showSuccess, showError} from './utils/ui.js'; const program = new Command(); @@ -44,6 +46,10 @@ program .option('--all', 'Process all files at once') .option('--file ', 'Process specific file') .option('--model ', 'Specify AI model (e.g., gpt-4, gpt-3.5-turbo)') + .option( + '--convention ', + 'Commit convention (clean, conventional, gitmoji, simple)', + ) .action(options => { commitCommand(options); }); @@ -83,6 +89,21 @@ authCmd // Config command const configCmd = program.command('config').description('Manage configuration'); +configCmd + .command('set-convention ') + .description('Set default commit convention') + .action(type => { + const validConventions = ['clean', 'conventional', 'gitmoji', 'simple']; + 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') diff --git a/source/commands/auth.js b/source/commands/auth.js index 15df4f0..11ca3b2 100644 --- a/source/commands/auth.js +++ b/source/commands/auth.js @@ -1,3 +1,4 @@ +import {execa} from 'execa'; import process from 'node:process'; import { setAuthMode, @@ -20,44 +21,43 @@ import { export async function authenticateWithCopilot(token = null) { 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 '); - 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); } } diff --git a/source/commands/commit.js b/source/commands/commit.js index a9cef47..1010d85 100644 --- a/source/commands/commit.js +++ b/source/commands/commit.js @@ -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 @@ -137,37 +137,47 @@ 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; - // Confirm and commit - const result = await confirmCommit(message, file); + while (!fileProcessed) { + try { + const message = await aiProvider.generateCommitMessage( + diff, + file, + {...options, convention: currentConvention}, + ); - 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}`); + const result = await confirmCommit(message, file, currentConvention); + + 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; + } else if (result.action === 'regenerate') { + currentConvention = result.convention; + // Loop continues to regenerate + } else if (result.action === 'skip') { + showInfo(`Skipped: ${file}`); await unstageFile(file); skipped++; + fileProcessed = true; } - } 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}`); - await unstageFile(file); - skipped++; } } @@ -199,27 +209,52 @@ 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); + + 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.'); + } + + return; + } + + if (result.action === 'regenerate') { + currentConvention = result.convention; + attempts++; + continue; + } + + if (result.action === 'skip') { + showInfo('Commit cancelled.'); + await unstageFile(filePath); + return; + } + } 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); } diff --git a/source/commands/config.js b/source/commands/config.js index 314e00a..c833d78 100644 --- a/source/commands/config.js +++ b/source/commands/config.js @@ -5,6 +5,7 @@ import { getConfigPath, } from '../utils/config-manager.js'; import {showSuccess, showInfo} from '../utils/ui.js'; +import {getConvention} from '../utils/commit-conventions.js'; /** * Configuration commands @@ -31,6 +32,12 @@ export function showConfig() { displayValue = value.slice(0, 10) + '...'; } + // Show convention name nicely + if (key === 'convention') { + const conv = getConvention(value); + displayValue = `${value} (${conv.name})`; + } + console.log(` ${chalk.yellow(key)}: ${chalk.white(displayValue)}`); } } diff --git a/source/providers/ai-provider.js b/source/providers/ai-provider.js index 4d38c37..f8f66ae 100644 --- a/source/providers/ai-provider.js +++ b/source/providers/ai-provider.js @@ -1,6 +1,11 @@ -import process from 'node:process'; +import {CopilotClient} from '@github/copilot-sdk'; import OpenAI from 'openai'; -import {getAuthMode, getToken} from '../utils/config-manager.js'; +import { + getAuthMode, + getToken, + getConvention as getConventionName, +} from '../utils/config-manager.js'; +import {getConvention} from '../utils/commit-conventions.js'; /** * AI Provider abstraction layer @@ -29,7 +34,12 @@ export class AIProvider { return this.generateFallbackMessage(filePath); } - const prompt = this.buildCleanCommitPrompt(diff, filePath); + // Get the convention (from options, config, or default to 'clean') + const conventionName = options.convention || getConventionName() || 'clean'; + const convention = getConvention(conventionName); + + // Build prompt using the selected convention + const prompt = convention.buildPrompt(diff, filePath); try { if (this.authMode === 'copilot') { @@ -51,42 +61,59 @@ export class AIProvider { /** * Generate commit message using GitHub Copilot - * NOTE: This is a simplified implementation. A GitHub token alone cannot authenticate - * with the OpenAI API. In production, this would either: - * 1. Use the GitHub Copilot API endpoint (requires different authentication) - * 2. Require users to have an OpenAI API key separately - * 3. Use a proxy service that bridges GitHub auth to OpenAI - * For now, this serves as a placeholder for the intended Copilot integration. */ async generateWithCopilot(prompt, options = {}) { + let client; try { - // Check for GitHub token in environment variables or config - const token = - getToken('github') || - process.env.COPILOT_GITHUB_TOKEN || - process.env.GH_TOKEN || - process.env.GITHUB_TOKEN; - - if (!token) { - throw new Error( - 'GitHub token not found. Please authenticate with "magicc auth copilot"', - ); + // Create and start the Copilot client + client = new CopilotClient(); + await client.start(); + + // Create session with model + const session = await client.createSession({ + model: this.model || options.model || 'gpt-4.1', + }); + + // Send the prompt and wait for response + const response = await session.sendAndWait({ + prompt, + }); + + // Clean up + await client.stop(); + + // Extract the content from response + if (response?.data?.content) { + return response.data.content.trim(); } - // In a real implementation, this would use the Copilot API endpoint - // For now, if a GitHub token is provided, we fall back to OpenAI - // This allows the structure to be in place for future Copilot integration - throw new Error( - 'GitHub Copilot integration pending - using OpenAI fallback', - ); + throw new Error('No response from Copilot'); } catch (error) { + // Clean up client if it was created + if (client) { + try { + await client.stop(); + } catch { + // Ignore cleanup errors + } + } + + console.error('Copilot error:', error.message); + // Fallback to OpenAI if available if (getToken('openai')) { - console.warn('โš ๏ธ Copilot not fully implemented, using OpenAI...'); + console.warn('โš ๏ธ Copilot failed, using OpenAI fallback...'); return this.generateWithOpenAI(prompt, options); } - throw error; + throw new Error( + `GitHub Copilot failed: ${error.message}\n\n` + + 'Make sure you have:\n' + + '1. GitHub Copilot subscription\n' + + '2. Authenticated via: gh auth login\n' + + '3. GitHub CLI installed and in PATH\n\n' + + 'Or set OpenAI key as fallback: magicc auth openai ', + ); } } @@ -120,45 +147,6 @@ export class AIProvider { return response.choices[0].message.content.trim(); } - /** - * Build Clean Commit format prompt - */ - buildCleanCommitPrompt(diff, filePath = null) { - return `You are an expert at writing git commit messages following the "Clean Commit" format. - -**Clean Commit Format:** - : - (): - -**The 9 Types:** -| Emoji | Type | What it covers | -|-------|-----------|----------------| -| ๐Ÿ“ฆ | new | Adding new features, files, or capabilities | -| ๐Ÿ”ง | update | Changing existing code, refactoring, improvements | -| ๐Ÿ—‘๏ธ | remove | Removing code, files, features, or dependencies | -| ๐Ÿ”’ | security | Security fixes, patches, vulnerability resolutions | -| โš™๏ธ | setup | Project configs, CI/CD, tooling, build systems | -| โ˜• | chore | Maintenance tasks, dependency updates, housekeeping | -| ๐Ÿงช | test | Adding, updating, or fixing tests | -| ๐Ÿ“– | docs | Documentation changes and updates | -| ๐Ÿš€ | release | Version releases and release preparation | - -**Rules:** -- Use lowercase for type -- Use present tense ("add" not "added") -- No period at the end -- Keep description under 72 characters -- Include scope (filename) if appropriate - -**Git Diff:** -\`\`\`diff -${diff} -\`\`\` - -${filePath ? `**File:** ${filePath}\n` : ''} -Generate a single commit message following Clean Commit format. Return ONLY the commit message, nothing else.`; - } - /** * Fallback message for large diffs */ diff --git a/source/utils/commit-conventions.js b/source/utils/commit-conventions.js new file mode 100644 index 0000000..ecea56f --- /dev/null +++ b/source/utils/commit-conventions.js @@ -0,0 +1,148 @@ +export const CONVENTIONS = { + clean: { + name: 'Clean Commit', + description: 'wgtechlabs Clean Commit format with emojis', + buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages following the "Clean Commit" format. + +**Clean Commit Format:** + : + (): + +**The 9 Types:** +| Emoji | Type | What it covers | +|-------|-----------|----------------| +| ๐Ÿ“ฆ | new | Adding new features, files, or capabilities | +| ๐Ÿ”ง | update | Changing existing code, refactoring, improvements | +| ๐Ÿ—‘๏ธ | remove | Removing code, files, features, or dependencies | +| ๐Ÿ”’ | security | Security fixes, patches, vulnerability resolutions | +| โš™๏ธ | setup | Project configs, CI/CD, tooling, build systems | +| โ˜• | chore | Maintenance tasks, dependency updates, housekeeping | +| ๐Ÿงช | test | Adding, updating, or fixing tests | +| ๐Ÿ“– | docs | Documentation changes and updates | +| ๐Ÿš€ | release | Version releases and release preparation | + +**Rules:** +- Use lowercase for type +- Use present tense ("add" not "added") +- No period at the end +- Keep description under 72 characters +- Include scope (filename) if appropriate + +**Git Diff:** +\`\`\`diff +${diff} +\`\`\` + +${filePath ? `**File:** ${filePath}\n` : ''} +Generate a single commit message following Clean Commit format. Return ONLY the commit message, nothing else.`, + }, + + conventional: { + name: 'Conventional Commits', + description: 'Standard Conventional Commits specification', + buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages following the "Conventional Commits" specification. + +**Format:** +(): + +**Types:** +- feat: A new feature +- fix: A bug fix +- docs: Documentation only changes +- style: Changes that don't affect code meaning (whitespace, formatting) +- refactor: Code change that neither fixes a bug nor adds a feature +- perf: Code change that improves performance +- test: Adding missing tests or correcting existing tests +- build: Changes that affect the build system or external dependencies +- ci: Changes to CI configuration files and scripts +- chore: Other changes that don't modify src or test files + +**Rules:** +- Use lowercase for type +- Use present tense ("add" not "added") +- No period at the end +- Keep description under 72 characters +- Scope is optional but recommended + +**Git Diff:** +\`\`\`diff +${diff} +\`\`\` + +${filePath ? `**File:** ${filePath}\n` : ''} +Generate a single commit message following Conventional Commits format. Return ONLY the commit message, nothing else.`, + }, + + gitmoji: { + name: 'Gitmoji', + description: 'Gitmoji commit convention with emojis', + buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages using the "Gitmoji" convention. + +**Format:** +:: + +**Common Emoji Codes:** +- :sparkles: Introduce new features +- :bug: Fix a bug +- :recycle: Refactor code +- :lipstick: Update UI and style files +- :memo: Add or update documentation +- :rocket: Deploy stuff +- :white_check_mark: Add, update, or pass tests +- :lock: Fix security issues +- :arrow_up: Upgrade dependencies +- :arrow_down: Downgrade dependencies +- :fire: Remove code or files +- :construction: Work in progress + +**Rules:** +- Start with emoji code (e.g., :sparkles:) +- Use present tense +- Keep concise and clear +- No period at the end + +**Git Diff:** +\`\`\`diff +${diff} +\`\`\` + +${filePath ? `**File:** ${filePath}\n` : ''} +Generate a single commit message using Gitmoji format. Return ONLY the commit message, nothing else.`, + }, + + simple: { + name: 'Simple', + description: 'Plain descriptive commit messages', + buildPrompt: (diff, filePath) => `You are an expert at writing clear, concise git commit messages. + +**Format:** +Simple descriptive message in imperative mood + +**Rules:** +- Use imperative mood ("Add feature" not "Added feature") +- Start with capital letter +- No period at the end +- Keep under 72 characters +- Be specific and clear +- No prefixes or emojis + +**Git Diff:** +\`\`\`diff +${diff} +\`\`\` + +${filePath ? `**File:** ${filePath}\n` : ''} +Generate a single, clear commit message. Return ONLY the commit message, nothing else.`, + }, +}; + +export function getConvention(name = 'clean') { + return CONVENTIONS[name] || CONVENTIONS.clean; +} + +export function listConventions() { + return Object.entries(CONVENTIONS).map(([key, conv]) => ({ + value: key, + name: `${conv.name} - ${conv.description}`, + })); +} diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index 2708a15..aa6d8b5 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -73,3 +73,12 @@ export function setUseGhCli(value) { export function getUseGhCli() { return config.get('useGhCli', false); } + +// Convention management +export function setConvention(convention = 'clean') { + config.set('convention', convention); +} + +export function getConvention() { + return config.get('convention', 'clean'); // Default to 'clean' +} diff --git a/source/utils/ui.js b/source/utils/ui.js index 199d326..655a84f 100644 --- a/source/utils/ui.js +++ b/source/utils/ui.js @@ -1,6 +1,10 @@ import chalk from 'chalk'; import figlet from 'figlet'; import inquirer from 'inquirer'; +import { + listConventions, + getConvention, +} from '../utils/commit-conventions.js'; /** * UI utilities for magic-commit terminal interface @@ -32,8 +36,33 @@ export function showCommitPreview(message, filePath = null) { console.log(); } -export async function confirmCommit(message, filePath = null) { - showCommitPreview(message, filePath); +export async function promptConventionSelection() { + const answer = await inquirer.prompt([ + { + type: 'list', + name: 'convention', + message: 'Select commit message convention:', + choices: listConventions(), + }, + ]); + return answer.convention; +} + +export async function confirmCommit( + message, + filePath = null, + conventionName = 'clean', +) { + const conv = getConvention(conventionName); + + console.log(chalk.gray(`\n๐Ÿ“‹ Convention: ${conv.name}`)); + console.log(chalk.yellow('๐Ÿ“ Suggested Commit Message:')); + console.log(chalk.white(message)); + if (filePath) { + console.log(chalk.gray(` File: ${filePath}`)); + } + + console.log(); const answer = await inquirer.prompt([ { @@ -43,6 +72,7 @@ export async function confirmCommit(message, filePath = null) { choices: [ {name: 'โœ… Accept and commit', value: 'accept'}, {name: 'โœ๏ธ Edit message', value: 'edit'}, + {name: '๐Ÿ”„ Regenerate with different convention', value: 'change-convention'}, {name: 'โญ๏ธ Skip this file', value: 'skip'}, ], }, @@ -60,6 +90,11 @@ export async function confirmCommit(message, filePath = null) { return {action: 'accept', message: edited.message}; } + if (answer.action === 'change-convention') { + const newConvention = await promptConventionSelection(); + return {action: 'regenerate', convention: newConvention}; + } + return {action: answer.action, message}; } From 1aceb605c507b53c8b3dd3a3469f164fb1dc06a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:58:10 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=A7=AA=20test:=20update=20tests=20for?= =?UTF-8?q?=20convention=20system=20and=20fix=20linting=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/cli.js | 5 +- source/commands/auth.js | 4 +- source/commands/commit.js | 21 ++++---- source/utils/commit-conventions.js | 20 +++++-- source/utils/ui.js | 10 ++-- test.js | 83 +++++++++++++++++++++++++++--- 6 files changed, 111 insertions(+), 32 deletions(-) diff --git a/source/cli.js b/source/cli.js index 4ece87a..f738c3a 100755 --- a/source/cli.js +++ b/source/cli.js @@ -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, @@ -11,8 +11,7 @@ import { logout, } from './commands/auth.js'; import {showConfig, resetConfig} from './commands/config.js'; -import {setConvention, getConvention} from './utils/config-manager.js'; -import {showSuccess, showError} from './utils/ui.js'; +import {setConvention} from './utils/config-manager.js'; const program = new Command(); diff --git a/source/commands/auth.js b/source/commands/auth.js index 11ca3b2..d5c3eee 100644 --- a/source/commands/auth.js +++ b/source/commands/auth.js @@ -1,5 +1,5 @@ -import {execa} from 'execa'; import process from 'node:process'; +import {execa} from 'execa'; import { setAuthMode, setToken, @@ -19,7 +19,7 @@ import { * Handles GitHub Copilot and OpenAI authentication */ -export async function authenticateWithCopilot(token = null) { +export async function authenticateWithCopilot() { try { console.log('๐Ÿ” Setting up GitHub Copilot authentication...\n'); diff --git a/source/commands/commit.js b/source/commands/commit.js index 1010d85..9dcf60c 100644 --- a/source/commands/commit.js +++ b/source/commands/commit.js @@ -142,14 +142,14 @@ export async function processFilesInteractively(aiProvider, options = {}) { while (!fileProcessed) { try { - const message = await aiProvider.generateCommitMessage( - diff, - file, - {...options, convention: currentConvention}, - ); + const message = await aiProvider.generateCommitMessage(diff, file, { + ...options, + convention: currentConvention, + }); const result = await confirmCommit(message, file, currentConvention); + // eslint-disable-next-line unicorn/prefer-switch if (result.action === 'accept') { const success = await commit(result.message); if (success) { @@ -190,6 +190,7 @@ export async function processFilesInteractively(aiProvider, options = {}) { } /* eslint-enable no-await-in-loop */ +/* eslint-disable no-await-in-loop */ export async function processFile(filePath, aiProvider, options = {}) { showInfo(`Processing single file: ${filePath}`); console.log(''); @@ -217,11 +218,10 @@ export async function processFile(filePath, aiProvider, options = {}) { showInfo('Generating commit message...'); try { - const message = await aiProvider.generateCommitMessage( - diff, - filePath, - {...options, convention: currentConvention}, - ); + const message = await aiProvider.generateCommitMessage(diff, filePath, { + ...options, + convention: currentConvention, + }); const result = await confirmCommit(message, filePath, currentConvention); @@ -258,3 +258,4 @@ export async function processFile(filePath, aiProvider, options = {}) { showWarning('Maximum regeneration attempts reached.'); await unstageFile(filePath); } +/* eslint-enable no-await-in-loop */ diff --git a/source/utils/commit-conventions.js b/source/utils/commit-conventions.js index ecea56f..43ee30e 100644 --- a/source/utils/commit-conventions.js +++ b/source/utils/commit-conventions.js @@ -2,7 +2,10 @@ export const CONVENTIONS = { clean: { name: 'Clean Commit', description: 'wgtechlabs Clean Commit format with emojis', - buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages following the "Clean Commit" format. + buildPrompt: ( + diff, + filePath, + ) => `You are an expert at writing git commit messages following the "Clean Commit" format. **Clean Commit Format:** : @@ -40,7 +43,10 @@ Generate a single commit message following Clean Commit format. Return ONLY the conventional: { name: 'Conventional Commits', description: 'Standard Conventional Commits specification', - buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages following the "Conventional Commits" specification. + buildPrompt: ( + diff, + filePath, + ) => `You are an expert at writing git commit messages following the "Conventional Commits" specification. **Format:** (): @@ -76,7 +82,10 @@ Generate a single commit message following Conventional Commits format. Return O gitmoji: { name: 'Gitmoji', description: 'Gitmoji commit convention with emojis', - buildPrompt: (diff, filePath) => `You are an expert at writing git commit messages using the "Gitmoji" convention. + buildPrompt: ( + diff, + filePath, + ) => `You are an expert at writing git commit messages using the "Gitmoji" convention. **Format:** :: @@ -113,7 +122,10 @@ Generate a single commit message using Gitmoji format. Return ONLY the commit me simple: { name: 'Simple', description: 'Plain descriptive commit messages', - buildPrompt: (diff, filePath) => `You are an expert at writing clear, concise git commit messages. + buildPrompt: ( + diff, + filePath, + ) => `You are an expert at writing clear, concise git commit messages. **Format:** Simple descriptive message in imperative mood diff --git a/source/utils/ui.js b/source/utils/ui.js index 655a84f..a49fc6c 100644 --- a/source/utils/ui.js +++ b/source/utils/ui.js @@ -1,10 +1,7 @@ import chalk from 'chalk'; import figlet from 'figlet'; import inquirer from 'inquirer'; -import { - listConventions, - getConvention, -} from '../utils/commit-conventions.js'; +import {listConventions, getConvention} from '../utils/commit-conventions.js'; /** * UI utilities for magic-commit terminal interface @@ -72,7 +69,10 @@ export async function confirmCommit( choices: [ {name: 'โœ… Accept and commit', value: 'accept'}, {name: 'โœ๏ธ Edit message', value: 'edit'}, - {name: '๐Ÿ”„ Regenerate with different convention', value: 'change-convention'}, + { + name: '๐Ÿ”„ Regenerate with different convention', + value: 'change-convention', + }, {name: 'โญ๏ธ Skip this file', value: 'skip'}, ], }, diff --git a/test.js b/test.js index ffb484c..c08df5d 100644 --- a/test.js +++ b/test.js @@ -5,8 +5,14 @@ import { setToken, getAuthMode, getToken, + setConvention, + getConvention, clearAll, } from './source/utils/config-manager.js'; +import { + getConvention as getConventionDetails, + listConventions, +} from './source/utils/commit-conventions.js'; test('config manager stores and retrieves auth mode', t => { clearAll(); @@ -33,18 +39,79 @@ test('config manager stores and retrieves tokens', t => { t.is(getToken('github'), undefined); }); -test('AIProvider builds Clean Commit prompt correctly', t => { - const provider = new AIProvider({authMode: 'openai'}); +test('config manager stores and retrieves convention', t => { + clearAll(); + t.is(getConvention(), 'clean'); // Default + + setConvention('conventional'); + t.is(getConvention(), 'conventional'); + + setConvention('gitmoji'); + t.is(getConvention(), 'gitmoji'); + + clearAll(); + t.is(getConvention(), 'clean'); // Back to default +}); + +test('convention system returns correct convention details', t => { + const cleanConv = getConventionDetails('clean'); + t.is(cleanConv.name, 'Clean Commit'); + t.true(cleanConv.description.includes('emoji')); + + const conventionalConv = getConventionDetails('conventional'); + t.is(conventionalConv.name, 'Conventional Commits'); + + const gitmojiConv = getConventionDetails('gitmoji'); + t.is(gitmojiConv.name, 'Gitmoji'); + + const simpleConv = getConventionDetails('simple'); + t.is(simpleConv.name, 'Simple'); +}); + +test('convention system lists all conventions', t => { + const conventions = listConventions(); + t.is(conventions.length, 4); + t.true(conventions.some(c => c.value === 'clean')); + t.true(conventions.some(c => c.value === 'conventional')); + t.true(conventions.some(c => c.value === 'gitmoji')); + t.true(conventions.some(c => c.value === 'simple')); +}); + +test('convention system builds prompts correctly', t => { const diff = 'test diff content'; const filePath = 'test.js'; - const prompt = provider.buildCleanCommitPrompt(diff, filePath); + // Test Clean Commit + const cleanConv = getConventionDetails('clean'); + const cleanPrompt = cleanConv.buildPrompt(diff, filePath); + t.true(cleanPrompt.includes('Clean Commit')); + t.true(cleanPrompt.includes('test diff content')); + t.true(cleanPrompt.includes('test.js')); + t.true(cleanPrompt.includes('๐Ÿ“ฆ')); + + // Test Conventional Commits + const conventionalConv = getConventionDetails('conventional'); + const conventionalPrompt = conventionalConv.buildPrompt(diff, filePath); + t.true(conventionalPrompt.includes('Conventional Commits')); + t.true(conventionalPrompt.includes('feat:')); + t.true(conventionalPrompt.includes('fix:')); + + // Test Gitmoji + const gitmojiConv = getConventionDetails('gitmoji'); + const gitmojiPrompt = gitmojiConv.buildPrompt(diff, filePath); + t.true(gitmojiPrompt.includes('Gitmoji')); + t.true(gitmojiPrompt.includes(':sparkles:')); + + // Test Simple + const simpleConv = getConventionDetails('simple'); + const simplePrompt = simpleConv.buildPrompt(diff, filePath); + t.true(simplePrompt.includes('imperative mood')); + t.true(simplePrompt.includes('test diff content')); +}); - t.true(prompt.includes('Clean Commit')); - t.true(prompt.includes('test diff content')); - t.true(prompt.includes('test.js')); - t.true(prompt.includes('๐Ÿ“ฆ')); - t.true(prompt.includes('๐Ÿ”ง')); +test('convention system defaults to clean for unknown convention', t => { + const unknownConv = getConventionDetails('unknown-convention'); + t.is(unknownConv.name, 'Clean Commit'); // Should fallback to clean }); test('AIProvider generates fallback message for large diffs', t => { From 5eae5a914c2b1a72434ab4a820eb86335821b8ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:01:35 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=94=A7=20update:=20refactor=20to=20us?= =?UTF-8?q?e=20switch=20statements=20per=20linter=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/commands/commit.js | 90 ++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/source/commands/commit.js b/source/commands/commit.js index 9dcf60c..bff8b9b 100644 --- a/source/commands/commit.js +++ b/source/commands/commit.js @@ -149,28 +149,37 @@ export async function processFilesInteractively(aiProvider, options = {}) { const result = await confirmCommit(message, file, currentConvention); - // eslint-disable-next-line unicorn/prefer-switch - 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}`); + 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++; + } + + fileProcessed = true; + break; + } + + case 'regenerate': { + currentConvention = result.convention; + // Loop continues to regenerate + break; + } + + case 'skip': { + showInfo(`Skipped: ${file}`); await unstageFile(file); skipped++; + fileProcessed = true; + break; } - - fileProcessed = true; - } else if (result.action === 'regenerate') { - currentConvention = result.convention; - // Loop continues to regenerate - } else if (result.action === 'skip') { - showInfo(`Skipped: ${file}`); - await unstageFile(file); - skipped++; - fileProcessed = true; + // No default } } catch (error) { showError(`Error processing ${file}: ${error.message}`); @@ -188,9 +197,7 @@ export async function processFilesInteractively(aiProvider, options = {}) { console.log(` โญ๏ธ Skipped: ${skipped} file(s)`); console.log('โ•'.repeat(50) + '\n'); } -/* eslint-enable no-await-in-loop */ -/* eslint-disable no-await-in-loop */ export async function processFile(filePath, aiProvider, options = {}) { showInfo(`Processing single file: ${filePath}`); console.log(''); @@ -225,28 +232,31 @@ export async function processFile(filePath, aiProvider, options = {}) { const result = await confirmCommit(message, filePath, currentConvention); - 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.'); - } + 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; - } + return; + } - if (result.action === 'regenerate') { - currentConvention = result.convention; - attempts++; - continue; - } + case 'regenerate': { + currentConvention = result.convention; + attempts++; + continue; + } - if (result.action === 'skip') { - showInfo('Commit cancelled.'); - await unstageFile(filePath); - return; + case 'skip': { + showInfo('Commit cancelled.'); + await unstageFile(filePath); + return; + } + // No default } } catch (error) { showError(`Error: ${error.message}`); From feee241a348cac3e93b7730132963e5ac02499df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:49:05 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=94=A7=20update:=20address=20PR=20rev?= =?UTF-8?q?iew=20feedback=20on=20code=20quality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/cli.js | 3 ++- source/commands/commit.js | 14 +++++++++- source/providers/ai-provider.js | 45 +++++++++++++++++++++++---------- source/utils/config-manager.js | 11 ++------ test.js | 4 --- 5 files changed, 48 insertions(+), 29 deletions(-) diff --git a/source/cli.js b/source/cli.js index f738c3a..9a42d50 100755 --- a/source/cli.js +++ b/source/cli.js @@ -12,6 +12,7 @@ import { } 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(); @@ -92,7 +93,7 @@ configCmd .command('set-convention ') .description('Set default commit convention') .action(type => { - const validConventions = ['clean', 'conventional', 'gitmoji', 'simple']; + const validConventions = Object.keys(CONVENTIONS); if (!validConventions.includes(type)) { showError(`Invalid convention: ${type}`); console.log(`Available: ${validConventions.join(', ')}`); diff --git a/source/commands/commit.js b/source/commands/commit.js index bff8b9b..25237f8 100644 --- a/source/commands/commit.js +++ b/source/commands/commit.js @@ -139,8 +139,11 @@ export async function processFilesInteractively(aiProvider, options = {}) { let currentConvention = options.convention || getConvention(); let fileProcessed = false; + let attempts = 0; + const maxAttempts = 5; - while (!fileProcessed) { + while (!fileProcessed && attempts < maxAttempts) { + attempts++; try { const message = await aiProvider.generateCommitMessage(diff, file, { ...options, @@ -188,6 +191,15 @@ export async function processFilesInteractively(aiProvider, options = {}) { fileProcessed = true; } } + + // Check if max attempts reached without processing + if (!fileProcessed && attempts >= maxAttempts) { + showWarning( + `Maximum regeneration attempts (${maxAttempts}) reached for ${file}`, + ); + await unstageFile(file); + skipped++; + } } // Show summary diff --git a/source/providers/ai-provider.js b/source/providers/ai-provider.js index f8f66ae..b58fae4 100644 --- a/source/providers/ai-provider.js +++ b/source/providers/ai-provider.js @@ -64,10 +64,13 @@ export class AIProvider { */ async generateWithCopilot(prompt, options = {}) { let client; + let clientStarted = false; + try { // Create and start the Copilot client client = new CopilotClient(); await client.start(); + clientStarted = true; // Create session with model const session = await client.createSession({ @@ -79,25 +82,30 @@ export class AIProvider { prompt, }); - // Clean up - await client.stop(); + // Extract the content from response, handling multiple possible shapes + let content = null; + + if (typeof response === 'string') { + content = response; + } else { + content = + // Preserve original expected shape first + response?.data?.content ?? + // Common OpenAI / chat-like shapes under data + response?.data?.choices?.[0]?.message?.content ?? + response?.data?.choices?.[0]?.content ?? + // Or directly on the response object + response?.choices?.[0]?.message?.content ?? + response?.choices?.[0]?.content ?? + response?.content; + } - // Extract the content from response - if (response?.data?.content) { - return response.data.content.trim(); + if (typeof content === 'string' && content.trim()) { + return content.trim(); } throw new Error('No response from Copilot'); } catch (error) { - // Clean up client if it was created - if (client) { - try { - await client.stop(); - } catch { - // Ignore cleanup errors - } - } - console.error('Copilot error:', error.message); // Fallback to OpenAI if available @@ -114,6 +122,15 @@ export class AIProvider { '3. GitHub CLI installed and in PATH\n\n' + 'Or set OpenAI key as fallback: magicc auth openai ', ); + } finally { + // Clean up client if it was started + if (client && clientStarted) { + try { + await client.stop(); + } catch { + // Ignore cleanup errors + } + } } } diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index aa6d8b5..734d0be 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -18,20 +18,13 @@ export function getAuthMode() { // Token management export function setToken(provider, token) { - if (provider === 'copilot' || provider === 'github') { - config.set('githubToken', token); - } else if (provider === 'openai') { + if (provider === 'openai') { config.set('openai', token); + config.set('authenticatedAt', new Date().toISOString()); } - - config.set('authenticatedAt', new Date().toISOString()); } export function getToken(provider) { - if (provider === 'copilot' || provider === 'github') { - return config.get('githubToken'); - } - if (provider === 'openai') { return config.get('openai'); } diff --git a/test.js b/test.js index c08df5d..0fe75f3 100644 --- a/test.js +++ b/test.js @@ -31,12 +31,8 @@ test('config manager stores and retrieves tokens', t => { setToken('openai', 'sk-test-key'); t.is(getToken('openai'), 'sk-test-key'); - setToken('github', 'ghp-test-token'); - t.is(getToken('github'), 'ghp-test-token'); - clearAll(); t.is(getToken('openai'), undefined); - t.is(getToken('github'), undefined); }); test('config manager stores and retrieves convention', t => { From 5f3d2a63108edf7bac27679b8cd117f44b108e6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:39:15 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=94=A7=20update:=20address=20second?= =?UTF-8?q?=20round=20of=20PR=20review=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/commands/commit.js | 2 +- source/commands/config.js | 8 ++++++-- source/providers/ai-provider.js | 25 +++++++++++++++++-------- source/utils/config-manager.js | 4 ++++ 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/source/commands/commit.js b/source/commands/commit.js index 25237f8..3952725 100644 --- a/source/commands/commit.js +++ b/source/commands/commit.js @@ -143,7 +143,6 @@ export async function processFilesInteractively(aiProvider, options = {}) { const maxAttempts = 5; while (!fileProcessed && attempts < maxAttempts) { - attempts++; try { const message = await aiProvider.generateCommitMessage(diff, file, { ...options, @@ -171,6 +170,7 @@ export async function processFilesInteractively(aiProvider, options = {}) { case 'regenerate': { currentConvention = result.convention; + attempts++; // Loop continues to regenerate break; } diff --git a/source/commands/config.js b/source/commands/config.js index c833d78..b8611d6 100644 --- a/source/commands/config.js +++ b/source/commands/config.js @@ -5,7 +5,7 @@ import { getConfigPath, } from '../utils/config-manager.js'; import {showSuccess, showInfo} from '../utils/ui.js'; -import {getConvention} from '../utils/commit-conventions.js'; +import {getConvention, CONVENTIONS} from '../utils/commit-conventions.js'; /** * Configuration commands @@ -35,7 +35,11 @@ export function showConfig() { // Show convention name nicely if (key === 'convention') { const conv = getConvention(value); - displayValue = `${value} (${conv.name})`; + // 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)}`); diff --git a/source/providers/ai-provider.js b/source/providers/ai-provider.js index b58fae4..d42ebe2 100644 --- a/source/providers/ai-provider.js +++ b/source/providers/ai-provider.js @@ -74,7 +74,7 @@ export class AIProvider { // Create session with model const session = await client.createSession({ - model: this.model || options.model || 'gpt-4.1', + model: this.model || options.model || 'gpt-4o', }); // Send the prompt and wait for response @@ -83,13 +83,19 @@ export class AIProvider { }); // Extract the content from response, handling multiple possible shapes + // The Copilot SDK response format can vary based on the model and session type. + // We check multiple common response structures to ensure compatibility: + // - Direct string responses + // - Nested under data.content (some SDK versions) + // - OpenAI-style choices array (fallback compatibility) + // - Direct content property on response object let content = null; if (typeof response === 'string') { content = response; } else { content = - // Preserve original expected shape first + // Check data.content first (common in some SDK versions) response?.data?.content ?? // Common OpenAI / chat-like shapes under data response?.data?.choices?.[0]?.message?.content ?? @@ -115,12 +121,15 @@ export class AIProvider { } throw new Error( - `GitHub Copilot failed: ${error.message}\n\n` + - 'Make sure you have:\n' + - '1. GitHub Copilot subscription\n' + - '2. Authenticated via: gh auth login\n' + - '3. GitHub CLI installed and in PATH\n\n' + - 'Or set OpenAI key as fallback: magicc auth openai ', + `GitHub Copilot failed` + + (error.name ? ` (${error.name})` : '') + + `: ${error.message}\n\n` + + 'This may be caused by:\n' + + '1. Missing GitHub Copilot subscription\n' + + '2. Not authenticated in GitHub CLI (try: gh auth login)\n' + + '3. Network issues or GitHub API availability problems\n' + + '4. Incompatible or outdated GitHub Copilot SDK or CLI version\n\n' + + 'If the issue persists, set an OpenAI key as fallback: magicc auth openai ', ); } finally { // Clean up client if it was started diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index 734d0be..2989615 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -10,6 +10,7 @@ const config = new Conf({projectName: 'magicc'}); // Auth mode management export function setAuthMode(mode) { config.set('authMode', mode); + config.set('authenticatedAt', new Date().toISOString()); } export function getAuthMode() { @@ -69,6 +70,9 @@ export function getUseGhCli() { // Convention management export function setConvention(convention = 'clean') { + // Validate convention name if CONVENTIONS is available + // This is a soft validation - we allow setting even if validation fails + // to avoid breaking config updates, but warn in getConvention config.set('convention', convention); } From 670890a10cf3297b0a1de46815a2a1da751f092f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 04:40:21 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=93=96=20docs:=20clarify=20setConvent?= =?UTF-8?q?ion=20validation=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/utils/config-manager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index 2989615..c690190 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -70,9 +70,8 @@ export function getUseGhCli() { // Convention management export function setConvention(convention = 'clean') { - // Validate convention name if CONVENTIONS is available - // This is a soft validation - we allow setting even if validation fails - // to avoid breaking config updates, but warn in getConvention + // Store convention without validation here to avoid circular dependencies + // Validation happens at read time in config display and CLI commands config.set('convention', convention); } From 8e01b498d3bc741f2d124d200750436f78be3af2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:48:50 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=94=A7=20update:=20add=20validation?= =?UTF-8?q?=20to=20setConvention=20for=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/utils/config-manager.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index c690190..8cc0b30 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -1,4 +1,5 @@ import Conf from 'conf'; +import {CONVENTIONS} from './commit-conventions.js'; const config = new Conf({projectName: 'magicc'}); @@ -70,8 +71,17 @@ export function getUseGhCli() { // Convention management export function setConvention(convention = 'clean') { - // Store convention without validation here to avoid circular dependencies - // Validation happens at read time in config display and CLI commands + // Validate convention name + const validConventions = Object.keys(CONVENTIONS); + if (!validConventions.includes(convention)) { + console.warn( + `Warning: Invalid convention '${convention}'. Valid options: ${validConventions.join( + ', ', + )}. Defaulting to 'clean'.`, + ); + convention = 'clean'; + } + config.set('convention', convention); } From 9304fa8ffab01db0d58cbcfbf56b9193377d285e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:49:54 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=94=A7=20update:=20improve=20warning?= =?UTF-8?q?=20message=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: warengonzaga <15052701+warengonzaga@users.noreply.github.com> --- source/utils/config-manager.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/utils/config-manager.js b/source/utils/config-manager.js index 8cc0b30..6f7579e 100644 --- a/source/utils/config-manager.js +++ b/source/utils/config-manager.js @@ -74,10 +74,9 @@ export function setConvention(convention = 'clean') { // Validate convention name const validConventions = Object.keys(CONVENTIONS); if (!validConventions.includes(convention)) { + const validOptions = validConventions.join(', '); console.warn( - `Warning: Invalid convention '${convention}'. Valid options: ${validConventions.join( - ', ', - )}. Defaulting to 'clean'.`, + `Warning: Invalid convention '${convention}'. Valid options: ${validOptions}. Defaulting to 'clean'.`, ); convention = 'clean'; }