test: update tests

This commit is contained in:
s41r4j
2026-06-09 02:18:17 +05:30
parent 807d5bcb20
commit a54d529f31
6 changed files with 414 additions and 344 deletions
+197 -110
View File
@@ -128,6 +128,24 @@ async function confirmCommit(message, isLocalHeuristic) {
});
}
function askQuestion(promptText) {
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(resolve => {
rl.question(promptText, answer => {
rl.close();
resolve(answer.trim());
});
});
}
async function confirmPrompt(message, defaultYes = true) {
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
const answer = (await askQuestion(`${message} ${suffix}: `)).toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
}
async function resolveCommit(input) {
if (/^\d+$/.test(input)) {
const { all } = await safeLog();
@@ -145,6 +163,25 @@ async function hasChanges() {
return status.files.length > 0;
}
// Returns staged diff string, auto-staging as needed, or null when nothing to commit.
async function getStagedDiff(opts) {
if (!(await hasChanges()) && !opts.all) return null;
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let diff = await git.diff(['--cached', '--no-ext-diff']);
if (!diff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
diff = await git.diff(['--cached', '--no-ext-diff']);
}
return diff.trim() ? diff : null;
}
program
.name('gims')
.alias('g')
@@ -528,27 +565,12 @@ program.command('local').alias('l')
const opts = getOpts();
try {
if (!(await hasChanges()) && !opts.all) {
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.warning('No changes to commit');
return;
}
}
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
const result = await generateCommitMessage(rawDiff, opts);
if (opts.progressIndicators) Progress.stop('');
@@ -590,27 +612,12 @@ program.command('online').alias('o')
const opts = getOpts();
try {
if (!(await hasChanges()) && !opts.all) {
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) {
Progress.info('Staging all changes...');
await git.add('.');
}
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.info('No staged changes found; staging all changes...');
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
Progress.warning('No changes to commit');
return;
}
}
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
const result = await generateCommitMessage(rawDiff, opts);
if (opts.progressIndicators) Progress.stop('');
@@ -675,22 +682,12 @@ program.command('commit <message...>').alias('m')
const msg = (messageParts || []).join(' ').trim();
if (!msg) { console.log('Provide a commit message.'); return; }
if (!(await hasChanges()) && !opts.all) {
console.log('No changes to commit.');
const rawDiff = await getStagedDiff(opts);
if (!rawDiff) {
Progress.warning('No changes to commit');
return;
}
if (opts.all) await git.add('.');
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
// Auto-stage all changes by default when nothing is staged
console.log(color.yellow('No staged changes found; staging all changes (git add .).'));
await git.add('.');
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) { console.log('No changes to commit.'); return; }
}
if (opts.dryRun) {
console.log(color.yellow('[dry-run] Would commit with custom message:'));
console.log(msg);
@@ -722,16 +719,114 @@ program.command('pull')
program.command('push')
.description('Push commits to remote')
.action(async () => {
.option('--tags', 'Push all tags to remote')
.action(async (cmdOptions) => {
await ensureRepo();
try {
Progress.info('Pushing to remote...');
await git.push();
Progress.success('Pushed to remote');
if (cmdOptions.tags) {
Progress.info('Pushing tags to remote...');
await git.push(['--tags']);
Progress.success('Tags pushed to remote');
} else {
Progress.info('Pushing to remote...');
await git.push();
Progress.success('Pushed to remote');
}
}
catch (e) { handleError('Push error', e); }
});
program.command('mirror [url]')
.alias('mr')
.description('Manage extra push destinations for origin (g push fans out to all of them)')
.action(async (url) => {
await ensureRepo();
try {
const remotes = await git.getRemotes(true);
const origin = remotes.find(r => r.name === 'origin');
if (!origin) {
Progress.warning("No 'origin' remote configured");
return;
}
const primary = origin.refs.fetch;
const pushUrls = (await git.raw(['config', '--get-all', 'remote.origin.pushurl']).catch(() => ''))
.split('\n').map(s => s.trim()).filter(Boolean);
const mirrors = pushUrls.filter(u => u !== primary);
if (url) {
if (primary === url || pushUrls.includes(url)) {
Progress.warning('That URL is already registered for origin');
return;
}
Progress.info(`Verifying ${url}...`);
try {
await git.listRemote([url]);
} catch {
const proceed = await confirmPrompt(`Could not reach "${url}". Add it anyway?`, false);
if (!proceed) { Progress.info('Cancelled'); return; }
}
// Make the primary an explicit pushurl first so it isn't lost once we add others
if (pushUrls.length === 0) {
await git.raw(['remote', 'set-url', '--push', 'origin', primary]);
}
await git.raw(['remote', 'set-url', '--add', '--push', 'origin', url]);
Progress.success(`Added mirror: ${url}`);
Progress.info(`'g push' now pushes to ${mirrors.length + 2} destination(s)`);
return;
}
console.log(color.bold('\nPush destinations for origin:'));
console.log(` ${color.green('●')} ${primary} ${color.dim('(primary)')}`);
if (mirrors.length === 0) {
console.log(color.dim('\nNo mirrors configured.'));
console.log(color.dim(`Add one with: ${color.cyan('g mirror <url>')}`));
return;
}
mirrors.forEach(m => console.log(` ${color.cyan('○')} ${m} ${color.dim('(mirror)')}`));
// Single readline session for the whole remove flow — separate
// interfaces on the same stdin can drop input on later prompts.
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise(resolve => rl.question(q, a => resolve(a.trim())));
const confirm = async (message, defaultYes) => {
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
const answer = (await ask(`${message} ${suffix}: `)).toLowerCase();
if (answer === '') return defaultYes;
return answer === 'y' || answer === 'yes';
};
try {
const wantsRemove = await confirm('\nRemove a mirror?', false);
if (!wantsRemove) return;
let target = mirrors[0];
if (mirrors.length > 1) {
const answer = await ask(`Which mirror? [1-${mirrors.length}]: `);
const idx = parseInt(answer) - 1;
if (isNaN(idx) || idx < 0 || idx >= mirrors.length) {
Progress.warning('Invalid selection, cancelled');
return;
}
target = mirrors[idx];
}
const confirmDelete = await confirm(`Remove "${target}"?`, false);
if (!confirmDelete) { Progress.info('Cancelled'); return; }
await git.raw(['remote', 'set-url', '--delete', '--push', 'origin', target]);
Progress.success(`Removed mirror: ${target}`);
} finally {
rl.close();
}
} catch (e) {
handleError('Mirror error', e);
}
});
program.command('sync')
.description('Smart sync: pull + rebase/merge')
.option('--rebase', 'Use rebase instead of merge')
@@ -826,11 +921,12 @@ program.command('stash')
Progress.start('🤖 Generating stash description');
const diff = await git.diff();
const description = await aiProvider.generateCommitMessage(diff, {
const descResult = await aiProvider.generateCommitMessage(diff, {
conventional: false,
body: false
});
Progress.stop('');
const description = descResult.message || descResult;
await git.stash(['push', '-m', `WIP: ${description}`]);
Progress.success(`Stashed changes: "${description}"`);
@@ -923,28 +1019,28 @@ program.command('amend').alias('a')
}
});
async function printCommitLog(limit) {
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
}
program.command('list').alias('ls')
.description('Short numbered git log (oldest → newest)')
.option('--limit <n>', 'Limit number of commits', '20')
.action(async (cmdOptions) => {
await ensureRepo();
try {
const limit = parseInt(cmdOptions.limit) || 20;
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
await printCommitLog(parseInt(cmdOptions.limit) || 20);
} catch (e) {
handleError('List error', e);
}
@@ -984,22 +1080,7 @@ program.command('history').alias('h')
.action(async (cmdOptions) => {
await ensureRepo();
try {
const limit = parseInt(cmdOptions.limit) || 20;
const log = await git.log({ maxCount: limit });
const commits = [...log.all].reverse();
if (commits.length === 0) {
Progress.info('No commits found');
return;
}
commits.forEach((c, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
});
if (log.all.length >= limit) {
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
}
await printCommitLog(parseInt(cmdOptions.limit) || 20);
} catch (e) {
handleError('History error', e);
}
@@ -1743,14 +1824,16 @@ program.command('fix').alias('f')
if (behind > 0) {
try {
const behindLog = await git.log({ from: branch, to: remoteBranch, maxCount: 5 });
// symmetric: false → "branch..remoteBranch" (commits on remote only, not "branch...remoteBranch" which mixes both directions)
const behindLog = await git.log({ from: branch, to: remoteBranch, symmetric: false, maxCount: 5 });
aiContext += `Incoming commits (latest 5):\n${behindLog.all.map(c => `- ${c.message}`).join('\n')}\n\n`;
} catch (e) { }
}
if (ahead > 0) {
try {
const aheadLog = await git.log({ from: remoteBranch, to: branch, maxCount: 5 });
// symmetric: false → "remoteBranch..branch" (commits on local only)
const aheadLog = await git.log({ from: remoteBranch, to: branch, symmetric: false, maxCount: 5 });
aiContext += `My outgoing commits (latest 5):\n${aheadLog.all.map(c => `- ${c.message}`).join('\n')}\n`;
} catch (e) { }
}
@@ -1766,36 +1849,40 @@ program.command('fix').alias('f')
Recommended Command: <command>
`;
// Use preferred provider or auto-resolve
const provider = aiProvider.resolveProvider(opts.provider);
if (provider === 'none') {
const heuristicRec = () => {
if (ahead > 0 && behind === 0) return 'Push (g push)';
if (behind > 0 && ahead === 0) return 'Pull (g pull)';
return 'Rebase (g fix --rebase)';
};
// Try each available provider in priority order, with full fallback chain
const providerChain = aiProvider.buildProviderChain(opts.provider || 'auto')
.filter(p => p !== 'local');
if (providerChain.length === 0) {
Progress.stop('');
console.log(color.yellow('No AI provider configured. Falling back to simple heuristics.'));
// ... heuristic fallback ...
let rec = '';
if (ahead > 0 && behind === 0) rec = 'Push (g push)';
else if (behind > 0 && ahead === 0) rec = 'Pull (g pull)';
else rec = 'Rebase (g fix --rebase)';
console.log(`Recommendation: ${rec}`);
console.log(`Recommendation: ${heuristicRec()}`);
return;
}
try {
const response = await aiProvider.generateWithProvider(provider, prompt, { temperature: 0.3 }); // Use generic provider method
Progress.stop('');
console.log(`\n${color.bold('🤖 AI Analysis:')}`);
console.log(response.trim());
} catch (e) {
Progress.stop('');
console.log(color.yellow('AI Analysis failed, falling back to heuristics.'));
// ... heuristic fallback code ...
let rec = '';
if (ahead > 0 && behind === 0) rec = 'Push (g push)';
else if (behind > 0 && ahead === 0) rec = 'Pull (g pull)';
else rec = 'Rebase (g fix --rebase)';
console.log(`Recommendation: ${rec}`);
let response = null;
for (const p of providerChain) {
try {
response = await aiProvider.generateWithProvider(p, prompt, { temperature: 0.3 });
break;
} catch { continue; }
}
Progress.stop('');
if (!response) {
console.log(color.yellow('AI providers unavailable. Falling back to heuristics.'));
console.log(`Recommendation: ${heuristicRec()}`);
return;
}
console.log(`\n${color.bold('🤖 AI Analysis:')}`);
console.log(response.trim());
return;
}
+49 -38
View File
@@ -1,3 +1,5 @@
const fs = require('fs');
const path = require('path');
const { OpenAI } = require('openai');
const { GoogleGenAI } = require('@google/genai');
const { Progress } = require('../utils/progress');
@@ -9,31 +11,17 @@ const { color } = require('../utils/colors');
class AIProviderManager {
constructor(config = {}) {
this.config = config;
this.cache = new Map();
this.maxCacheSize = 100;
}
resolveProvider(preference = 'auto') {
if (preference === 'none') return 'none';
// Check if preferred provider's key is available
if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai';
if (preference === 'gemini' && process.env.GEMINI_API_KEY) return 'gemini';
if (preference === 'groq' && process.env.GROQ_API_KEY) return 'groq';
// Fallback: try any available provider (priority: Gemini → OpenAI → Groq)
if (process.env.GEMINI_API_KEY) return 'gemini';
if (process.env.OPENAI_API_KEY) return 'openai';
if (process.env.GROQ_API_KEY) return 'groq';
return 'none';
this.cacheFile = path.join(
process.env.HOME || process.env.USERPROFILE || process.cwd(),
'.gims', 'cache.json'
);
}
getDefaultModel(provider) {
const defaults = {
'gemini': 'gemini-3-flash-preview', // Latest Gemini model
'openai': 'gpt-5.2-2025-12-11', // Latest GPT model
'groq': 'groq/compound' // Latest Groq model
'gemini': 'gemini-2.0-flash',
'openai': 'gpt-4o-mini',
'groq': 'llama-3.3-70b-versatile'
};
return defaults[provider] || '';
}
@@ -46,26 +34,36 @@ class AIProviderManager {
getFromCache(cacheKey) {
if (!this.config.cacheEnabled) return null;
return this.cache.get(cacheKey);
try {
if (!fs.existsSync(this.cacheFile)) return null;
const data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
return data[cacheKey] || null;
} catch {
return null;
}
}
setCache(cacheKey, result, usedLocal = false) {
if (!this.config.cacheEnabled) return;
if (this.cache.size >= this.maxCacheSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(cacheKey, {
result,
usedLocal,
timestamp: Date.now()
});
try {
const dir = path.dirname(this.cacheFile);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
let data = {};
if (fs.existsSync(this.cacheFile)) {
try { data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8')); } catch {}
}
const keys = Object.keys(data);
if (keys.length >= 100) {
const oldest = keys.sort((a, b) => (data[a].timestamp || 0) - (data[b].timestamp || 0));
oldest.slice(0, keys.length - 99).forEach(k => delete data[k]);
}
data[cacheKey] = { result, usedLocal, timestamp: Date.now() };
fs.writeFileSync(this.cacheFile, JSON.stringify(data));
} catch {}
}
async generateWithProvider(provider, prompt, options = {}) {
const { model = '', temperature = 0.3, maxTokens = 200 } = options;
const { model = '' } = options;
try {
switch (provider) {
@@ -79,7 +77,14 @@ class AIProviderManager {
throw new Error(`Unknown provider: ${provider}`);
}
} catch (error) {
throw new Error(`${provider} generation failed: ${error.message}`);
const msg = error.message || '';
if (msg.includes('429') || /rate.?limit/i.test(msg)) {
throw new Error(`${provider} rate limit hit — try again in a moment`);
}
if (msg.includes('401') || /invalid.api.key|api_key/i.test(msg)) {
throw new Error(`${provider} API key invalid — run: g setup --api-key ${provider}`);
}
throw new Error(`${provider} generation failed: ${msg}`);
}
}
@@ -135,8 +140,14 @@ class AIProviderManager {
verbose = false
} = options;
// Truncate oversized diffs before sending to AI
const maxDiffSize = this.config.maxDiffSize || 100000;
const truncatedDiff = diff.length > maxDiffSize
? diff.substring(0, maxDiffSize) + '\n... (diff truncated for AI)'
: diff;
// Check cache first
const cacheKey = this.getCacheKey(diff, { conventional, body });
const cacheKey = this.getCacheKey(truncatedDiff, { conventional, body });
const cached = this.getFromCache(cacheKey);
if (cached && Date.now() - cached.timestamp < 3600000) { // 1 hour cache
if (verbose) Progress.info('Using cached result');
@@ -148,12 +159,12 @@ class AIProviderManager {
for (const provider of providerChain) {
try {
if (provider === 'local') {
const result = await this.generateLocalHeuristic(diff, options);
const result = await this.generateLocalHeuristic(truncatedDiff, options);
this.setCache(cacheKey, result, true);
return { message: result, usedLocal: true };
}
const prompt = this.buildPrompt(diff, { conventional, body });
const prompt = this.buildPrompt(truncatedDiff, { conventional, body });
const result = await this.generateWithProvider(provider, prompt, options);
const cleaned = this.cleanCommitMessage(result, { body });
+6 -6
View File
@@ -164,14 +164,14 @@ class ConfigManager {
console.log(` ${color.cyan('g setup --api-key openai')} # High quality`);
console.log(` ${color.cyan('g setup --api-key groq')} # Ultra fast`);
console.log('\nOr set environment variables manually:');
console.log(' - OPENAI_API_KEY (gpt-5)');
console.log(' - GEMINI_API_KEY (gemini-2.5-flash)');
console.log(' - GROQ_API_KEY (groq/compound)');
console.log(' - OPENAI_API_KEY (gpt-4o-mini)');
console.log(' - GEMINI_API_KEY (gemini-2.0-flash)');
console.log(' - GROQ_API_KEY (llama-3.3-70b-versatile)');
} else {
console.log('Available providers with default models:');
if (hasGemini) console.log(` ${color.green('✓')} Google Gemini (gemini-2.5-flash)`);
if (hasOpenAI) console.log(` ${color.green('✓')} OpenAI (gpt-5)`);
if (hasGroq) console.log(` ${color.green('✓')} Groq (groq/compound)`);
if (hasGemini) console.log(` ${color.green('✓')} Google Gemini (gemini-2.0-flash)`);
if (hasOpenAI) console.log(` ${color.green('✓')} OpenAI (gpt-4o-mini)`);
if (hasGroq) console.log(` ${color.green('✓')} Groq (llama-3.3-70b-versatile)`);
}
const provider = await question(`\nPreferred provider (auto/openai/gemini/groq/none) [auto]: `) || 'auto';