UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

240 lines (230 loc) 8.35 kB
import { getProvider } from "../providers/index.js"; import { loadConfig } from "../state/config.js"; import { getDiffSummary, getDiffContent, getAllChanges, commitWithMessage, addAll } from "../tools/git.js"; import { log } from "../util/logging.js"; import { trackTaskUsage } from "../util/costs.js"; export async function generateCommitMessage(repo, context) { try { // Get changes const changesResult = getAllChanges(repo); if (!changesResult.ok || !changesResult.data.trim()) { log.warn("No changes to commit"); return null; } const files = changesResult.data.trim().split('\n').filter(Boolean); // Get diff summary for overview const diffSummary = getDiffSummary(repo); const summaryText = diffSummary.ok ? diffSummary.data : ""; // Get actual diff content (limited to avoid token explosion) const diffContent = getDiffContent(repo); const diffText = diffContent.ok ? diffContent.data.split('\n').slice(0, 200).join('\n') : // Limit to first 200 lines ""; // Generate commit message using AI const config = await loadConfig(); if (!config) { throw new Error("No configuration found"); } const provider = getProvider(config.defaultProvider); const model = config.models[config.defaultProvider]?.chat; if (!model) { throw new Error("No chat model configured"); } const prompt = buildCommitPrompt(files, summaryText, diffText, context); const response = await provider.chat([ { role: "system", content: COMMIT_SYSTEM_PROMPT }, { role: "user", content: prompt } ], { model, temperature: 0.3, maxTokens: 200 }); // Track usage await trackTaskUsage(config.defaultProvider, model, "Generate commit message", prompt, response, repo); // Parse response const commitInfo = parseCommitResponse(response, files); if (!commitInfo) { log.warn("Failed to parse commit message from AI response"); return generateFallbackCommit(files, summaryText); } return commitInfo; } catch (error) { log.warn("Failed to generate AI commit message:", error); // Fallback to simple conventional commit const changesResult = getAllChanges(repo); const files = changesResult.ok ? changesResult.data.trim().split('\n').filter(Boolean) : []; const diffSummary = getDiffSummary(repo); const summaryText = diffSummary.ok ? diffSummary.data : ""; return generateFallbackCommit(files, summaryText); } } export async function autoCommit(repo, taskDescription) { try { log.step("Auto-commit", "generating commit message..."); const commitInfo = await generateCommitMessage(repo, taskDescription); if (!commitInfo) { log.warn("No changes to commit"); return false; } // Stage all changes const addResult = addAll(repo); if (!addResult.ok) { log.error("Failed to stage changes:", addResult.error); return false; } // Commit with generated message const commitResult = commitWithMessage(repo, commitInfo.message); if (commitResult.ok) { log.success(`Committed: ${commitInfo.message}`); return true; } else { log.error("Failed to commit:", commitResult.error); return false; } } catch (error) { log.error("Auto-commit failed:", error); return false; } } const COMMIT_SYSTEM_PROMPT = `You are an expert at writing concise, conventional commit messages. Follow the Conventional Commits specification: - Format: type(scope): description - Types: feat, fix, docs, style, refactor, test, chore, build - Keep under 72 characters - Use present tense, imperative mood - Don't capitalize the first letter of description - Don't end with period Examples: - feat(auth): add JWT token validation - fix(api): handle null response in user endpoint - docs(readme): update installation instructions - refactor(utils): extract common validation logic - test(auth): add login integration tests - chore(deps): upgrade typescript to v5.0 - build(docker): optimize production image size Respond with JSON in this format: { "message": "feat(scope): description", "type": "feat", "scope": "auth", "breaking": false } If the change introduces breaking changes, set "breaking": true and add "BREAKING CHANGE:" to description or use ! after scope.`; function buildCommitPrompt(files, diffSummary, diffContent, context) { let prompt = `Generate a conventional commit message for these changes: Files changed (${files.length}): ${files.map(f => `- ${f}`).join('\n')} Diff summary: ${diffSummary} `; if (diffContent.trim()) { prompt += `Code changes (sample): \`\`\` ${diffContent.substring(0, 1000)}${diffContent.length > 1000 ? '...' : ''} \`\`\` `; } if (context) { prompt += `Context: ${context} `; } prompt += `Analyze the changes and generate an appropriate conventional commit message. Focus on the most significant change if there are multiple types.`; return prompt; } function parseCommitResponse(response, files) { try { // Try to parse JSON response const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { const data = JSON.parse(jsonMatch[0]); if (data.message && data.type) { return { message: data.message, files, type: data.type, scope: data.scope, breaking: data.breaking || false }; } } // Fallback: try to extract message from text const messageMatch = response.match(/(?:message["']?\s*:\s*["']?)([^"'\n]+)/i); if (messageMatch) { const message = messageMatch[1].trim(); const type = detectCommitType(message); return { message, files, type, breaking: message.includes('BREAKING CHANGE') || message.includes('!') }; } return null; } catch (error) { return null; } } function detectCommitType(message) { const msg = message.toLowerCase(); if (msg.startsWith('feat')) return 'feat'; if (msg.startsWith('fix')) return 'fix'; if (msg.startsWith('docs')) return 'docs'; if (msg.startsWith('style')) return 'style'; if (msg.startsWith('refactor')) return 'refactor'; if (msg.startsWith('test')) return 'test'; if (msg.startsWith('build')) return 'build'; if (msg.startsWith('chore')) return 'chore'; return 'chore'; } function generateFallbackCommit(files, diffSummary) { // Analyze file types and changes to determine commit type let type = "chore"; let scope; // Determine type based on files const hasTests = files.some(f => f.includes('test') || f.includes('spec')); const hasDocs = files.some(f => f.includes('readme') || f.includes('doc') || f.endsWith('.md')); const hasPackageJson = files.some(f => f.includes('package.json') || f.includes('package-lock.json')); const hasSourceCode = files.some(f => /\.(js|ts|jsx|tsx|py|go|java|c|cpp|rs)$/.test(f)); if (hasTests && !hasSourceCode) { type = "test"; } else if (hasDocs && !hasSourceCode) { type = "docs"; } else if (hasPackageJson) { type = "chore"; scope = "deps"; } else if (hasSourceCode) { type = "feat"; // Default to feat for code changes } // Generate simple message const fileCount = files.length; let message = `${type}`; if (scope) { message += `(${scope})`; } if (fileCount === 1) { const fileName = files[0].split('/').pop() || files[0]; message += `: update ${fileName}`; } else { message += `: update ${fileCount} files`; } return { message, files, type, scope, breaking: false }; }