commitect 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +39 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/commands/analyze.d.ts +2 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +40 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/clear-cache.d.ts +2 -0
- package/dist/commands/clear-cache.d.ts.map +1 -0
- package/dist/commands/clear-cache.js +23 -0
- package/dist/commands/clear-cache.js.map +1 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.d.ts.map +1 -0
- package/dist/commands/commit.js +42 -0
- package/dist/commands/commit.js.map +1 -0
- package/dist/commands/copy.d.ts +2 -0
- package/dist/commands/copy.d.ts.map +1 -0
- package/dist/commands/copy.js +42 -0
- package/dist/commands/copy.js.map +1 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +129 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +58 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/services/llm.d.ts +6 -0
- package/dist/services/llm.d.ts.map +1 -0
- package/dist/services/llm.js +94 -0
- package/dist/services/llm.js.map +1 -0
- package/dist/utils/cache.d.ts +61 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +141 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +69 -0
- package/dist/utils/git.js.map +1 -0
- package/package.json +38 -0
- package/src/commands/analyze.ts +44 -0
- package/src/commands/clear-cache.ts +23 -0
- package/src/commands/commit.ts +48 -0
- package/src/commands/copy.ts +48 -0
- package/src/commands/help.ts +143 -0
- package/src/commands/history.ts +62 -0
- package/src/index.ts +53 -0
- package/src/services/llm.ts +123 -0
- package/src/utils/cache.ts +170 -0
- package/src/utils/git.ts +74 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isGitRepository, getGitDiff, hasChanges, executeCommit } from '../utils/git.js';
|
|
2
|
+
import { generateCommitMessage } from '../services/llm.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function commitCommand(): Promise<void> {
|
|
6
|
+
try {
|
|
7
|
+
// Validate git repository
|
|
8
|
+
if (!isGitRepository()) {
|
|
9
|
+
console.error(chalk.red('❌ Not a git repository'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check for changes
|
|
14
|
+
if (!hasChanges()) {
|
|
15
|
+
console.log(chalk.yellow('⚠ No changes to commit'));
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Get diff
|
|
20
|
+
const diff = getGitDiff();
|
|
21
|
+
|
|
22
|
+
if (!diff.trim()) {
|
|
23
|
+
console.log(chalk.yellow('⚠ No changes to commit'));
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Generate commit message
|
|
28
|
+
console.log(chalk.blue('🔍 Generating commit message...'));
|
|
29
|
+
const suggestion = await generateCommitMessage(diff);
|
|
30
|
+
|
|
31
|
+
// Build commit message as "intent: message"
|
|
32
|
+
const commitMessage = `${suggestion.intent}: ${suggestion.message}`;
|
|
33
|
+
|
|
34
|
+
// Execute git commit
|
|
35
|
+
console.log(chalk.blue('💾 Committing changes...'));
|
|
36
|
+
executeCommit(commitMessage);
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green('✔ Committed: ') + commitMessage);
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
console.error(chalk.red('❌ ' + error.message));
|
|
43
|
+
} else {
|
|
44
|
+
console.error(chalk.red('❌ An unexpected error occurred'));
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isGitRepository, getGitDiff, hasChanges } from '../utils/git.js';
|
|
2
|
+
import { generateCommitMessage } from '../services/llm.js';
|
|
3
|
+
import clipboardy from 'clipboardy';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export async function copyCommand(): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
// Validate git repository
|
|
9
|
+
if (!isGitRepository()) {
|
|
10
|
+
console.error(chalk.red('❌ Not a git repository'));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check for changes
|
|
15
|
+
if (!hasChanges()) {
|
|
16
|
+
console.log(chalk.yellow('⚠ No changes detected'));
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get diff
|
|
21
|
+
const diff = getGitDiff();
|
|
22
|
+
|
|
23
|
+
if (!diff.trim()) {
|
|
24
|
+
console.log(chalk.yellow('⚠ No changes to analyze'));
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Generate commit message
|
|
29
|
+
console.log(chalk.blue('🔍 Generating commit message...'));
|
|
30
|
+
const suggestion = await generateCommitMessage(diff);
|
|
31
|
+
|
|
32
|
+
// Build commit message as "intent: message"
|
|
33
|
+
const commitMessage = `${suggestion.intent}: ${suggestion.message}`;
|
|
34
|
+
|
|
35
|
+
// Copy to clipboard
|
|
36
|
+
await clipboardy.write(commitMessage);
|
|
37
|
+
|
|
38
|
+
console.log(chalk.green('✔ Commit message copied to clipboard'));
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
console.error(chalk.red('❌ ' + error.message));
|
|
43
|
+
} else {
|
|
44
|
+
console.error(chalk.red('❌ An unexpected error occurred'));
|
|
45
|
+
}
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const VERSION = '1.0.0';
|
|
4
|
+
const GITHUB_REPO = 'https://github.com/Mohammed_3tef/CommiTect_CLI';
|
|
5
|
+
const ISSUES_URL = GITHUB_REPO + '/issues';
|
|
6
|
+
|
|
7
|
+
export function helpCommand(): void {
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log(chalk.bold.cyan('╔══════════════════════════════════════════════════════════╗'));
|
|
10
|
+
console.log(chalk.bold.cyan('║ ') + chalk.bold.white('CommiTect') + chalk.bold.cyan(' ║'));
|
|
11
|
+
console.log(chalk.bold.cyan('║ ') + chalk.gray('Zero-config Git Commit Assistant') + chalk.bold.cyan(' ║'));
|
|
12
|
+
console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════╝'));
|
|
13
|
+
console.log('');
|
|
14
|
+
|
|
15
|
+
// COMMANDS SECTION
|
|
16
|
+
console.log(chalk.bold.yellow('📋 AVAILABLE COMMANDS'));
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
// ANALYZE
|
|
20
|
+
console.log(chalk.bold.green(' commitect analyze'));
|
|
21
|
+
console.log(chalk.gray(' │'));
|
|
22
|
+
console.log(chalk.gray(' ├─ ') + 'Analyzes your git changes and suggests a commit message');
|
|
23
|
+
console.log(chalk.gray(' ├─ ') + 'Displays both intent and message on the terminal');
|
|
24
|
+
console.log(chalk.gray(' └─ ') + 'Does NOT modify your git repository');
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(chalk.dim(' Example output:'));
|
|
27
|
+
console.log(chalk.dim(' Feature: Add user authentication with JWT'));
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
// COPY
|
|
31
|
+
console.log(chalk.bold.green(' commitect copy'));
|
|
32
|
+
console.log(chalk.gray(' │'));
|
|
33
|
+
console.log(chalk.gray(' ├─ ') + 'Generates a commit message from your changes');
|
|
34
|
+
console.log(chalk.gray(' ├─ ') + 'Copies ONLY the message (not intent) to clipboard');
|
|
35
|
+
console.log(chalk.gray(' └─ ') + 'Perfect for manual commits with custom flags');
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.dim(' Usage:'));
|
|
38
|
+
console.log(chalk.dim(' $ commitect copy'));
|
|
39
|
+
console.log(chalk.dim(' $ git commit -m "<paste>" --no-verify'));
|
|
40
|
+
console.log('');
|
|
41
|
+
|
|
42
|
+
// COMMIT
|
|
43
|
+
console.log(chalk.bold.green(' commitect commit'));
|
|
44
|
+
console.log(chalk.gray(' │'));
|
|
45
|
+
console.log(chalk.gray(' ├─ ') + 'Generates a commit message from your changes');
|
|
46
|
+
console.log(chalk.gray(' ├─ ') + 'Automatically executes: git commit -m "<message>"');
|
|
47
|
+
console.log(chalk.gray(' └─ ') + 'Fastest way to commit with AI-generated messages');
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.dim(' Warning: Make sure you have staged your changes first!'));
|
|
50
|
+
console.log(chalk.dim(' $ git add .'));
|
|
51
|
+
console.log(chalk.dim(' $ commitect commit'));
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
54
|
+
// HISTORY
|
|
55
|
+
console.log(chalk.bold.green(' commitect history'));
|
|
56
|
+
console.log(chalk.gray(' │'));
|
|
57
|
+
console.log(chalk.gray(' ├─ ') + 'Shows all cached commit messages');
|
|
58
|
+
console.log(chalk.gray(' ├─ ') + 'Displays timestamp and time ago for each entry');
|
|
59
|
+
console.log(chalk.gray(' └─ ') + 'Useful for reviewing past suggestions');
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
// CLEAR-CACHE
|
|
63
|
+
console.log(chalk.bold.green(' commitect clear-cache'));
|
|
64
|
+
console.log(chalk.gray(' │'));
|
|
65
|
+
console.log(chalk.gray(' ├─ ') + 'Clears all cached commit messages');
|
|
66
|
+
console.log(chalk.gray(' ├─ ') + 'Cache location: ~/.commitect/cache.json');
|
|
67
|
+
console.log(chalk.gray(' └─ ') + 'Use when you want fresh suggestions');
|
|
68
|
+
console.log('');
|
|
69
|
+
|
|
70
|
+
// HELP
|
|
71
|
+
console.log(chalk.bold.green(' commitect help'));
|
|
72
|
+
console.log(chalk.gray(' │'));
|
|
73
|
+
console.log(chalk.gray(' └─ ') + 'Shows this help message');
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
// HOW IT WORKS
|
|
77
|
+
console.log(chalk.bold.yellow('⚙️ HOW IT WORKS'));
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(chalk.gray(' 1. ') + '📖 Reads your git diff (staged + unstaged changes)');
|
|
80
|
+
console.log(chalk.gray(' 2. ') + '🔍 Checks cache for previously analyzed diffs');
|
|
81
|
+
console.log(chalk.gray(' 3. ') + '🤖 Sends to AI API if not cached (with auto-retry)');
|
|
82
|
+
console.log(chalk.gray(' 4. ') + '💾 Caches result for 30 days');
|
|
83
|
+
console.log(chalk.gray(' 5. ') + '✨ Returns professional commit message');
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
// WORKFLOW
|
|
87
|
+
console.log(chalk.bold.yellow('🔄 TYPICAL WORKFLOW'));
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(chalk.gray(' # Make your changes'));
|
|
90
|
+
console.log(chalk.white(' $ vim src/auth.ts'));
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(chalk.gray(' # Stage files'));
|
|
93
|
+
console.log(chalk.white(' $ git add .'));
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(chalk.gray(' # Option 1: Preview message'));
|
|
96
|
+
console.log(chalk.white(' $ commitect analyze'));
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(chalk.gray(' # Option 2: Copy to clipboard'));
|
|
99
|
+
console.log(chalk.white(' $ commitect copy'));
|
|
100
|
+
console.log(chalk.white(' $ git commit -m "<paste>"'));
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.gray(' # Option 3: Auto-commit (fastest)'));
|
|
103
|
+
console.log(chalk.white(' $ commitect commit'));
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
// FEATURES
|
|
107
|
+
console.log(chalk.bold.yellow('✨ KEY FEATURES'));
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(chalk.green(' ✓ ') + 'Zero configuration required');
|
|
110
|
+
console.log(chalk.green(' ✓ ') + 'Smart caching (instant responses for same diffs)');
|
|
111
|
+
console.log(chalk.green(' ✓ ') + 'Auto-retry on API failures (up to 3 attempts)');
|
|
112
|
+
console.log(chalk.green(' ✓ ') + 'Ignores: node_modules/, dist/, build/, .git/');
|
|
113
|
+
console.log(chalk.green(' ✓ ') + 'Professional messages (imperative, <70 chars)');
|
|
114
|
+
console.log(chalk.green(' ✓ ') + 'Works with any git repository');
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
// TIPS
|
|
118
|
+
console.log(chalk.bold.yellow('💡 PRO TIPS'));
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('analyze') + ' when you want to review before committing');
|
|
121
|
+
console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('copy') + ' when you need custom git flags');
|
|
122
|
+
console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('commit') + ' for quick, everyday commits');
|
|
123
|
+
console.log(chalk.cyan(' • ') + 'Use ' + chalk.bold('history') + ' to review all your cached messages');
|
|
124
|
+
console.log(chalk.cyan(' • ') + 'Run ' + chalk.bold('clear-cache') + ' if suggestions seem outdated');
|
|
125
|
+
console.log(chalk.cyan(' • ') + 'Cache saves time and reduces API costs significantly');
|
|
126
|
+
console.log('');
|
|
127
|
+
|
|
128
|
+
// REQUIREMENTS
|
|
129
|
+
console.log(chalk.bold.yellow('📦 REQUIREMENTS'));
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(chalk.gray(' • Node.js >= 16'));
|
|
132
|
+
console.log(chalk.gray(' • Git repository (initialized)'));
|
|
133
|
+
console.log(chalk.gray(' • Internet connection (unless result is cached)'));
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
// FOOTER
|
|
137
|
+
console.log(chalk.bold.cyan('─'.repeat(63)));
|
|
138
|
+
console.log(chalk.gray(' Version: ') + chalk.white(VERSION));
|
|
139
|
+
console.log(chalk.gray(' Docs: ') + chalk.white(GITHUB_REPO));
|
|
140
|
+
console.log(chalk.gray(' Issues: ') + chalk.white(ISSUES_URL));
|
|
141
|
+
console.log(chalk.bold.cyan('─'.repeat(63)));
|
|
142
|
+
console.log('');
|
|
143
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { commitCache } from '../utils/cache.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
export function historyCommand(): void {
|
|
5
|
+
try {
|
|
6
|
+
const history = commitCache.getHistory();
|
|
7
|
+
|
|
8
|
+
if (history.length === 0) {
|
|
9
|
+
console.log(chalk.yellow('ℹ No commit history found'));
|
|
10
|
+
console.log(chalk.gray(' Generate some commits first using commitect analyze/copy/commit'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log(chalk.bold.cyan('📜 COMMIT HISTORY'));
|
|
16
|
+
console.log(chalk.gray('─'.repeat(70)));
|
|
17
|
+
console.log('');
|
|
18
|
+
|
|
19
|
+
history.forEach((entry, index) => {
|
|
20
|
+
const date = new Date(entry.timestamp);
|
|
21
|
+
const timeAgo = getTimeAgo(entry.timestamp);
|
|
22
|
+
|
|
23
|
+
// Format: [1] Feature: Add user authentication
|
|
24
|
+
console.log(chalk.bold.white(`[${index + 1}]`) + ' ' + chalk.green(`${entry.intent}: ${entry.message}`));
|
|
25
|
+
console.log(chalk.gray(` 📁 ${entry.folder}`));
|
|
26
|
+
console.log(chalk.gray(` 🕒 ${date.toLocaleString()} (${timeAgo})`));
|
|
27
|
+
console.log('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log(chalk.gray('─'.repeat(70)));
|
|
31
|
+
console.log(chalk.gray(`Total: ${history.length} cached commit message${history.length !== 1 ? 's' : ''}`));
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error instanceof Error) {
|
|
36
|
+
console.error(chalk.red('❌ ' + error.message));
|
|
37
|
+
} else {
|
|
38
|
+
console.error(chalk.red('❌ Failed to load history'));
|
|
39
|
+
}
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getTimeAgo(timestamp: number): string {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const diff = now - timestamp;
|
|
47
|
+
|
|
48
|
+
const seconds = Math.floor(diff / 1000);
|
|
49
|
+
const minutes = Math.floor(seconds / 60);
|
|
50
|
+
const hours = Math.floor(minutes / 60);
|
|
51
|
+
const days = Math.floor(hours / 24);
|
|
52
|
+
|
|
53
|
+
if (days > 0) {
|
|
54
|
+
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
|
55
|
+
} else if (hours > 0) {
|
|
56
|
+
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
57
|
+
} else if (minutes > 0) {
|
|
58
|
+
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
|
59
|
+
} else {
|
|
60
|
+
return 'just now';
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { analyzeCommand } from './commands/analyze.js';
|
|
4
|
+
import { copyCommand } from './commands/copy.js';
|
|
5
|
+
import { commitCommand } from './commands/commit.js';
|
|
6
|
+
import { clearCacheCommand } from './commands/clear-cache.js';
|
|
7
|
+
import { historyCommand } from './commands/history.js';
|
|
8
|
+
import { helpCommand } from './commands/help.js';
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name('commitect')
|
|
14
|
+
.description('Zero-config Git Commit Assistant')
|
|
15
|
+
.version('1.0.0');
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command('analyze')
|
|
19
|
+
.description('Analyze changes and suggest a commit message')
|
|
20
|
+
.action(analyzeCommand);
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('copy')
|
|
24
|
+
.description('Generate commit message and copy to clipboard')
|
|
25
|
+
.action(copyCommand);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('commit')
|
|
29
|
+
.description('Generate and execute git commit')
|
|
30
|
+
.action(commitCommand);
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('history')
|
|
34
|
+
.description('Show cached commit message history')
|
|
35
|
+
.action(historyCommand);
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('clear-cache')
|
|
39
|
+
.description('Clear the commit message cache')
|
|
40
|
+
.action(clearCacheCommand);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('help')
|
|
44
|
+
.description('Show detailed help and examples')
|
|
45
|
+
.action(helpCommand);
|
|
46
|
+
|
|
47
|
+
// Show help by default if no command is provided
|
|
48
|
+
if (process.argv.length === 2) {
|
|
49
|
+
helpCommand();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
program.parse();
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
import { commitCache } from '../utils/cache.js';
|
|
3
|
+
|
|
4
|
+
export interface CommitSuggestion {
|
|
5
|
+
intent: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const API_ENDPOINT = 'http://commitintentdetector.runasp.net/api/Commit/analyze';
|
|
10
|
+
|
|
11
|
+
export async function generateCommitMessage(diff: string): Promise<CommitSuggestion> {
|
|
12
|
+
// Check cache first
|
|
13
|
+
const cached = commitCache.get(diff);
|
|
14
|
+
if (cached) {
|
|
15
|
+
return cached;
|
|
16
|
+
}
|
|
17
|
+
const maxRetries = 3;
|
|
18
|
+
let lastError: Error | null = null;
|
|
19
|
+
|
|
20
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await axios.post(
|
|
23
|
+
API_ENDPOINT,
|
|
24
|
+
{ diff },
|
|
25
|
+
{
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json'
|
|
28
|
+
},
|
|
29
|
+
timeout: 30000 // 30 second timeout
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// API returns: { intent: "Intent: Feature\nMessage: Add subtraction support function" }
|
|
34
|
+
const data = response.data;
|
|
35
|
+
|
|
36
|
+
if (!data || !data.intent) {
|
|
37
|
+
throw new Error('Invalid response format from API');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = parseResponse(data.intent);
|
|
41
|
+
|
|
42
|
+
// Cache the result
|
|
43
|
+
commitCache.set(diff, result.intent, result.message);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
|
|
47
|
+
} catch (error) {
|
|
48
|
+
lastError = error as Error;
|
|
49
|
+
|
|
50
|
+
if (axios.isAxiosError(error)) {
|
|
51
|
+
const axiosError = error as AxiosError;
|
|
52
|
+
|
|
53
|
+
// Handle rate limiting (429)
|
|
54
|
+
if (axiosError.response?.status === 429) {
|
|
55
|
+
if (attempt < maxRetries) {
|
|
56
|
+
await sleep(1000 * attempt); // Exponential backoff
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new Error('API rate limit reached. Please try again later.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle server errors (5xx) - retry
|
|
63
|
+
if (axiosError.response?.status && axiosError.response.status >= 500) {
|
|
64
|
+
if (attempt < maxRetries) {
|
|
65
|
+
await sleep(1000 * attempt);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Handle network errors - retry
|
|
71
|
+
if (axiosError.code === 'ECONNREFUSED' || axiosError.code === 'ETIMEDOUT') {
|
|
72
|
+
if (attempt < maxRetries) {
|
|
73
|
+
await sleep(1000 * attempt);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw new Error('Unable to connect to API. Please check your network connection.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Don't retry on client errors (4xx) except 429
|
|
81
|
+
if (axios.isAxiosError(error) && error.response?.status && error.response.status < 500 && error.response.status !== 429) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Retry on other errors
|
|
86
|
+
if (attempt < maxRetries) {
|
|
87
|
+
await sleep(1000 * attempt);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error('Failed to generate commit message. Please check your API configuration.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseResponse(response: string): CommitSuggestion {
|
|
97
|
+
const lines = response.trim().split('\n');
|
|
98
|
+
|
|
99
|
+
let intent = '';
|
|
100
|
+
let message = '';
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (line.startsWith('Intent:')) {
|
|
104
|
+
intent = line.replace('Intent:', '').trim();
|
|
105
|
+
} else if (line.startsWith('Message:')) {
|
|
106
|
+
message = line.replace('Message:', '').trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!intent || !message) {
|
|
111
|
+
throw new Error('Invalid response format from LLM');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (message.length > 70) {
|
|
115
|
+
message = message.substring(0, 67) + '...';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { intent, message };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sleep(ms: number): Promise<void> {
|
|
122
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
123
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join, basename } from 'path';
|
|
5
|
+
|
|
6
|
+
interface CacheEntry {
|
|
7
|
+
hash: string;
|
|
8
|
+
intent: string;
|
|
9
|
+
message: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
folder: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CACHE_DIR = join(homedir(), '.commitect');
|
|
15
|
+
const CACHE_FILE = join(CACHE_DIR, 'cache.json');
|
|
16
|
+
const CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
|
|
17
|
+
|
|
18
|
+
class CommitCache {
|
|
19
|
+
private cache: Map<string, CacheEntry>;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.cache = new Map();
|
|
23
|
+
this.loadCache();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a hash from the git diff
|
|
28
|
+
*/
|
|
29
|
+
private hashDiff(diff: string): string {
|
|
30
|
+
return createHash('sha256').update(diff.trim()).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get current folder name
|
|
35
|
+
*/
|
|
36
|
+
private getCurrentFolder(): string {
|
|
37
|
+
return basename(process.cwd());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load cache from disk
|
|
42
|
+
*/
|
|
43
|
+
private loadCache(): void {
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(CACHE_DIR)) {
|
|
46
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (existsSync(CACHE_FILE)) {
|
|
50
|
+
const data = readFileSync(CACHE_FILE, 'utf-8');
|
|
51
|
+
const entries: CacheEntry[] = JSON.parse(data);
|
|
52
|
+
|
|
53
|
+
// Load valid entries (not expired)
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
entries.forEach(entry => {
|
|
56
|
+
if (now - entry.timestamp < CACHE_MAX_AGE) {
|
|
57
|
+
this.cache.set(entry.hash, entry);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// If cache is corrupted, start fresh
|
|
63
|
+
this.cache.clear();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Save cache to disk
|
|
69
|
+
*/
|
|
70
|
+
private saveCache(): void {
|
|
71
|
+
try {
|
|
72
|
+
if (!existsSync(CACHE_DIR)) {
|
|
73
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const entries = Array.from(this.cache.values());
|
|
77
|
+
writeFileSync(CACHE_FILE, JSON.stringify(entries, null, 2), 'utf-8');
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Silently fail - caching is optional
|
|
80
|
+
console.warn('Warning: Failed to save cache');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get cached commit message for a diff
|
|
86
|
+
*/
|
|
87
|
+
get(diff: string): { intent: string; message: string; folder: string } | null {
|
|
88
|
+
const hash = this.hashDiff(diff);
|
|
89
|
+
const entry = this.cache.get(hash);
|
|
90
|
+
|
|
91
|
+
if (!entry) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if entry is still valid
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
if (now - entry.timestamp > CACHE_MAX_AGE) {
|
|
98
|
+
this.cache.delete(hash);
|
|
99
|
+
this.saveCache();
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
intent: entry.intent,
|
|
105
|
+
message: entry.message,
|
|
106
|
+
folder: entry.folder
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Store a commit message in cache
|
|
112
|
+
*/
|
|
113
|
+
set(diff: string, intent: string, message: string): void {
|
|
114
|
+
const hash = this.hashDiff(diff);
|
|
115
|
+
|
|
116
|
+
this.cache.set(hash, {
|
|
117
|
+
hash,
|
|
118
|
+
intent,
|
|
119
|
+
message,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
folder: this.getCurrentFolder()
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.saveCache();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear all cache
|
|
129
|
+
*/
|
|
130
|
+
clear(): void {
|
|
131
|
+
this.cache.clear();
|
|
132
|
+
this.saveCache();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get cache statistics
|
|
137
|
+
*/
|
|
138
|
+
getStats(): { size: number; oldestEntry: number | null } {
|
|
139
|
+
const entries = Array.from(this.cache.values());
|
|
140
|
+
const oldestEntry = entries.length > 0
|
|
141
|
+
? Math.min(...entries.map(e => e.timestamp))
|
|
142
|
+
: null;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
size: this.cache.size,
|
|
146
|
+
oldestEntry
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get all cache entries sorted by timestamp (newest first)
|
|
152
|
+
*/
|
|
153
|
+
getHistory(): CacheEntry[] {
|
|
154
|
+
const entries = Array.from(this.cache.values());
|
|
155
|
+
return entries.sort((a, b) => b.timestamp - a.timestamp);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get cache entries for a specific folder
|
|
160
|
+
*/
|
|
161
|
+
getHistoryByFolder(folder: string): CacheEntry[] {
|
|
162
|
+
const entries = Array.from(this.cache.values());
|
|
163
|
+
return entries
|
|
164
|
+
.filter(entry => entry.folder === folder)
|
|
165
|
+
.sort((a, b) => b.timestamp - a.timestamp);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Singleton instance
|
|
170
|
+
export const commitCache = new CommitCache();
|