@freetison/git-super 0.2.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/LICENSE +201 -0
- package/README.md +384 -0
- package/bin/git-super.mjs +576 -0
- package/lib/ARCHITECTURE.md +254 -0
- package/lib/auth/auth-strategy.mjs +132 -0
- package/lib/auth/credential-store.mjs +222 -0
- package/lib/auth/oauth-flows.mjs +266 -0
- package/lib/auth/token-manager.mjs +246 -0
- package/lib/cli/auth-commands.mjs +327 -0
- package/lib/config/config-loader.mjs +167 -0
- package/lib/fallback/add-files-strategy.mjs +15 -0
- package/lib/fallback/base-fallback-strategy.mjs +34 -0
- package/lib/fallback/delete-files-strategy.mjs +15 -0
- package/lib/fallback/fallback-resolver.mjs +54 -0
- package/lib/fallback/modify-files-strategy.mjs +15 -0
- package/lib/providers/anthropic-provider.mjs +44 -0
- package/lib/providers/azure-openai-provider.mjs +185 -0
- package/lib/providers/base-oauth-provider.mjs +62 -0
- package/lib/providers/base-provider.mjs +29 -0
- package/lib/providers/generic-oidc-provider.mjs +144 -0
- package/lib/providers/github-copilot-provider.mjs +113 -0
- package/lib/providers/ollama-provider.mjs +109 -0
- package/lib/providers/openai-provider.mjs +44 -0
- package/lib/providers/provider-registry.mjs +99 -0
- package/package.json +59 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @theia-core/git-super - AI-powered git commits with customizable templates
|
|
4
|
+
*
|
|
5
|
+
* Automates: git add . → AI-generated commit → git push
|
|
6
|
+
* Works with ANY git repository (Node.js, Python, Java, C++, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* git super # add + commit + push
|
|
10
|
+
* git super --no-push # add + commit only (no push)
|
|
11
|
+
* git super --dry-run # preview message without committing
|
|
12
|
+
* git super --amend # amend last commit with new AI message
|
|
13
|
+
* git super --no-verify # skip pre-commit hooks (not recommended)
|
|
14
|
+
* git super --init # create config file with defaults
|
|
15
|
+
*
|
|
16
|
+
* Installation:
|
|
17
|
+
* npm install -g @theia-core/git-super
|
|
18
|
+
* git config --global alias.super '!git-super'
|
|
19
|
+
* git super --init # Optional: customize config
|
|
20
|
+
*
|
|
21
|
+
* Configuration:
|
|
22
|
+
* Edit ~/.gitsuperrc to customize:
|
|
23
|
+
* - Message templates (Jira, Linear, GitHub issues)
|
|
24
|
+
* - Commit types and max length
|
|
25
|
+
* - AI provider defaults
|
|
26
|
+
*
|
|
27
|
+
* Environment Variables:
|
|
28
|
+
* AI_PROVIDER - AI provider: 'ollama' (default), 'anthropic', 'openai'
|
|
29
|
+
* AI_MODEL - Model to use (default: 'mistral:latest' for Ollama)
|
|
30
|
+
* OLLAMA_URL - Ollama API URL (default: http://localhost:11434)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { execSync } from 'node:child_process';
|
|
34
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
35
|
+
import { basename, join } from 'node:path';
|
|
36
|
+
import { homedir } from 'node:os';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { dirname } from 'node:path';
|
|
39
|
+
|
|
40
|
+
// Import refactored modules
|
|
41
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
42
|
+
const __dirname = dirname(__filename);
|
|
43
|
+
|
|
44
|
+
import { loadConfig } from '../lib/config/config-loader.mjs';
|
|
45
|
+
import { ProviderRegistry } from '../lib/providers/provider-registry.mjs';
|
|
46
|
+
import { FallbackResolver } from '../lib/fallback/fallback-resolver.mjs';
|
|
47
|
+
import { handleAuthLogin, handleAuthLogout, handleAuthStatus, handleContext } from '../lib/cli/auth-commands.mjs';
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// CONFIGURATION
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const CONFIG = loadConfig();
|
|
54
|
+
const providerRegistry = new ProviderRegistry(CONFIG);
|
|
55
|
+
const fallbackResolver = new FallbackResolver();
|
|
56
|
+
|
|
57
|
+
// Parse command line args
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
|
|
60
|
+
// Check for auth/context commands first (before flags parsing)
|
|
61
|
+
const command = args[0];
|
|
62
|
+
if (command === 'auth') {
|
|
63
|
+
const subcommand = args[1];
|
|
64
|
+
const parsedArgs = parseArgs(args.slice(2));
|
|
65
|
+
parsedArgs._ = [command, subcommand];
|
|
66
|
+
|
|
67
|
+
(async () => {
|
|
68
|
+
switch (subcommand) {
|
|
69
|
+
case 'login':
|
|
70
|
+
await handleAuthLogin(parsedArgs);
|
|
71
|
+
break;
|
|
72
|
+
case 'logout':
|
|
73
|
+
await handleAuthLogout(parsedArgs);
|
|
74
|
+
break;
|
|
75
|
+
case 'status':
|
|
76
|
+
await handleAuthStatus();
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
log('Usage: git super auth <login|logout|status>', 'yellow');
|
|
80
|
+
log('\nCommands:', 'bright');
|
|
81
|
+
log(' login --provider <name> [--org <id>] Authenticate with OAuth provider', 'cyan');
|
|
82
|
+
log(' logout [--provider <name>] [--all] Log out from provider(s)', 'cyan');
|
|
83
|
+
log(' status Show authentication status', 'cyan');
|
|
84
|
+
}
|
|
85
|
+
process.exit(0);
|
|
86
|
+
})();
|
|
87
|
+
} else if (command === 'context' || command === 'ctx') {
|
|
88
|
+
const parsedArgs = parseArgs(args.slice(1));
|
|
89
|
+
parsedArgs._ = [command, ...parsedArgs._];
|
|
90
|
+
|
|
91
|
+
handleContext(parsedArgs);
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Traditional flags for normal git super flow
|
|
96
|
+
const flags = {
|
|
97
|
+
dryRun: args.includes('--dry-run'),
|
|
98
|
+
noPush: args.includes('--no-push'),
|
|
99
|
+
amend: args.includes('--amend'),
|
|
100
|
+
noVerify: args.includes('--no-verify'),
|
|
101
|
+
init: args.includes('--init'),
|
|
102
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Simple args parser for auth commands
|
|
106
|
+
function parseArgs(argv) {
|
|
107
|
+
const parsed = { _: [] };
|
|
108
|
+
for (let i = 0; i < argv.length; i++) {
|
|
109
|
+
const arg = argv[i];
|
|
110
|
+
if (arg.startsWith('--')) {
|
|
111
|
+
const key = arg.slice(2);
|
|
112
|
+
const next = argv[i + 1];
|
|
113
|
+
if (next && !next.startsWith('--')) {
|
|
114
|
+
parsed[key] = next;
|
|
115
|
+
i++;
|
|
116
|
+
} else {
|
|
117
|
+
parsed[key] = true;
|
|
118
|
+
}
|
|
119
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
120
|
+
const key = arg.slice(1);
|
|
121
|
+
const next = argv[i + 1];
|
|
122
|
+
if (next && !next.startsWith('-')) {
|
|
123
|
+
parsed[key] = next;
|
|
124
|
+
i++;
|
|
125
|
+
} else {
|
|
126
|
+
parsed[key] = true;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
parsed._.push(arg);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// UTILITIES
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
const colors = {
|
|
140
|
+
reset: '\x1b[0m',
|
|
141
|
+
bright: '\x1b[1m',
|
|
142
|
+
red: '\x1b[31m',
|
|
143
|
+
green: '\x1b[32m',
|
|
144
|
+
yellow: '\x1b[33m',
|
|
145
|
+
blue: '\x1b[34m',
|
|
146
|
+
magenta: '\x1b[35m',
|
|
147
|
+
cyan: '\x1b[36m',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function log(message, color = 'reset') {
|
|
151
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function exec(command, options = {}) {
|
|
155
|
+
try {
|
|
156
|
+
return execSync(command, {
|
|
157
|
+
encoding: 'utf-8',
|
|
158
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
159
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
160
|
+
...options,
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (options.silent) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getRepoName() {
|
|
171
|
+
try {
|
|
172
|
+
const remote = exec('git config --get remote.origin.url', { silent: true });
|
|
173
|
+
if (remote) {
|
|
174
|
+
return basename(remote.trim().replace(/\.git$/, ''));
|
|
175
|
+
}
|
|
176
|
+
} catch {}
|
|
177
|
+
|
|
178
|
+
return basename(process.cwd());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hasChanges() {
|
|
182
|
+
const status = exec('git status --porcelain', { silent: true });
|
|
183
|
+
return status && status.trim().length > 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function getGitDiff() {
|
|
187
|
+
try {
|
|
188
|
+
// Get staged changes
|
|
189
|
+
const staged = exec('git diff --cached', { silent: true }) || '';
|
|
190
|
+
|
|
191
|
+
// Get unstaged changes
|
|
192
|
+
const unstaged = exec('git diff', { silent: true }) || '';
|
|
193
|
+
|
|
194
|
+
// Get status
|
|
195
|
+
const status = exec('git status --short', { silent: true }) || '';
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
diff: staged || unstaged,
|
|
199
|
+
status,
|
|
200
|
+
hasStaged: staged.length > 0,
|
|
201
|
+
hasUnstaged: unstaged.length > 0,
|
|
202
|
+
};
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return { diff: '', status: '', hasStaged: false, hasUnstaged: false };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// TEMPLATE HELPERS
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
function extractType(msg) {
|
|
213
|
+
const match = msg.match(/^(\w+)(?:\(|:)/);
|
|
214
|
+
return match ? match[1] : 'chore';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractScope(msg) {
|
|
218
|
+
const match = msg.match(/\(([^)]+)\)/);
|
|
219
|
+
return match ? match[1] : '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function applyTemplate(message, template) {
|
|
223
|
+
if (!template) return message;
|
|
224
|
+
|
|
225
|
+
const type = extractType(message);
|
|
226
|
+
const scope = extractScope(message);
|
|
227
|
+
|
|
228
|
+
// Extract just the description part
|
|
229
|
+
let description = message;
|
|
230
|
+
const typeScorMatch = message.match(/^[^:]+:\s*(.+)$/);
|
|
231
|
+
if (typeScorMatch) {
|
|
232
|
+
description = typeScorMatch[1];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return template
|
|
236
|
+
.replace('{message}', description)
|
|
237
|
+
.replace('{type}', type)
|
|
238
|
+
.replace('{scope}', scope)
|
|
239
|
+
.replace('{ticket}', CONFIG.ticketNumber || '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// AI PROVIDERS
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
async function generateCommitMessage(diff, status, repoName) {
|
|
247
|
+
// Extract just filenames for context
|
|
248
|
+
const files = status.split('\n')
|
|
249
|
+
.filter(l => l.trim())
|
|
250
|
+
.map(l => l.substring(3).trim())
|
|
251
|
+
.slice(0, 10);
|
|
252
|
+
|
|
253
|
+
const hasTemplate = CONFIG.messageTemplate && CONFIG.messageTemplate.includes('{message}');
|
|
254
|
+
|
|
255
|
+
const prompt = `Generate a git commit message following Conventional Commits format.
|
|
256
|
+
|
|
257
|
+
Files changed:
|
|
258
|
+
${files.join('\n')}
|
|
259
|
+
|
|
260
|
+
Diff (first 6000 chars):
|
|
261
|
+
${diff.substring(0, 6000)}
|
|
262
|
+
|
|
263
|
+
Rules:
|
|
264
|
+
- Format: ${hasTemplate ? 'type(scope): description (will be inserted in template)' : 'type(scope): description'}
|
|
265
|
+
- Types: ${CONFIG.commitRules.types.join(', ')}
|
|
266
|
+
- Max ${CONFIG.commitRules.maxLength} characters
|
|
267
|
+
- Focus on WHAT changed, not HOW
|
|
268
|
+
- Be specific but concise
|
|
269
|
+
- NO quotes, NO explanations, NO extra text
|
|
270
|
+
|
|
271
|
+
Output ONLY the commit message:`;
|
|
272
|
+
|
|
273
|
+
log('🤖 Generating AI message...', 'cyan');
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Use Strategy Pattern - get provider from registry
|
|
277
|
+
const provider = providerRegistry.get(CONFIG.aiProvider);
|
|
278
|
+
let message = await provider.generate(prompt);
|
|
279
|
+
|
|
280
|
+
// Apply template if configured
|
|
281
|
+
if (CONFIG.messageTemplate) {
|
|
282
|
+
message = applyTemplate(message, CONFIG.messageTemplate);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Validate length
|
|
286
|
+
if (message.length > CONFIG.commitRules.maxLength) {
|
|
287
|
+
log(`⚠️ Message too long (${message.length}>${CONFIG.commitRules.maxLength}), truncating`, 'yellow');
|
|
288
|
+
message = message.substring(0, CONFIG.commitRules.maxLength);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return message;
|
|
292
|
+
|
|
293
|
+
} catch (error) {
|
|
294
|
+
log(`⚠️ Error generating message: ${error.message}`, 'yellow');
|
|
295
|
+
|
|
296
|
+
// Use Strategy Pattern for fallback selection
|
|
297
|
+
const lines = status.split('\n').filter(l => l.trim());
|
|
298
|
+
const stats = {
|
|
299
|
+
added: lines.filter(l => l.startsWith('A ')).length,
|
|
300
|
+
modified: lines.filter(l => l.startsWith('M ')).length,
|
|
301
|
+
deleted: lines.filter(l => l.startsWith('D ')).length,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let fallback = fallbackResolver.resolve(stats);
|
|
305
|
+
|
|
306
|
+
// Apply template to fallback too
|
|
307
|
+
if (CONFIG.messageTemplate) {
|
|
308
|
+
fallback = applyTemplate(fallback, CONFIG.messageTemplate);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
log(`Using fallback message: "${fallback}"`, 'yellow');
|
|
312
|
+
return fallback;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// AI Provider implementations moved to lib/providers/
|
|
318
|
+
// - OllamaProvider: lib/providers/ollama-provider.mjs
|
|
319
|
+
// - AnthropicProvider: lib/providers/anthropic-provider.mjs
|
|
320
|
+
// - OpenAIProvider: lib/providers/openai-provider.mjs
|
|
321
|
+
// ============================================================================
|
|
322
|
+
|
|
323
|
+
// ============================================================================
|
|
324
|
+
// INIT CONFIG
|
|
325
|
+
// ============================================================================
|
|
326
|
+
|
|
327
|
+
function initConfig() {
|
|
328
|
+
const configPath = join(homedir(), '.gitsuperrc');
|
|
329
|
+
|
|
330
|
+
if (existsSync(configPath)) {
|
|
331
|
+
log('\n⚠️ Config already exists:', 'yellow');
|
|
332
|
+
log(` ${configPath}\n`, 'bright');
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const current = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
336
|
+
log(JSON.stringify(current, null, 2), 'cyan');
|
|
337
|
+
} catch (error) {
|
|
338
|
+
log(`Error reading config: ${error.message}`, 'red');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
log('\n💡 Edit manually or delete to recreate\n', 'yellow');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const template = {
|
|
346
|
+
"$comment": "git-super configuration - Edit this file to customize behavior",
|
|
347
|
+
"aiProvider": "ollama",
|
|
348
|
+
"aiModel": "mistral:latest",
|
|
349
|
+
"ollamaUrl": "http://localhost:11434",
|
|
350
|
+
"messageTemplate": null,
|
|
351
|
+
"ticketNumber": "",
|
|
352
|
+
"commitRules": {
|
|
353
|
+
"types": ["feat", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build"],
|
|
354
|
+
"maxLength": 72,
|
|
355
|
+
"allowEmptyScope": true
|
|
356
|
+
},
|
|
357
|
+
"_examples": {
|
|
358
|
+
"$comment": "Copy one of these to 'messageTemplate' above",
|
|
359
|
+
"jira": "VTT-3020: {type}({scope}): {message}",
|
|
360
|
+
"linear": "LIN-{ticket}: {type}({scope}): {message}",
|
|
361
|
+
"github": "#{ticket}: {type}({scope}): {message}",
|
|
362
|
+
"simple": "{type}: {message}",
|
|
363
|
+
"default": null
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
log('\n📝 Creating config at:', 'cyan');
|
|
368
|
+
log(` ${configPath}\n`, 'bright');
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
writeFileSync(configPath, JSON.stringify(template, null, 2));
|
|
372
|
+
|
|
373
|
+
log('✅ Config file created!\n', 'green');
|
|
374
|
+
log('Edit to customize:', 'cyan');
|
|
375
|
+
log(' • messageTemplate - Add Jira/Linear/GitHub prefixes', 'blue');
|
|
376
|
+
log(' • commitRules - Customize types and max length', 'blue');
|
|
377
|
+
log(' • aiProvider - Change AI provider (ollama/anthropic/openai)', 'blue');
|
|
378
|
+
log('\nExample template:', 'bright');
|
|
379
|
+
log(' "messageTemplate": "VTT-3020: {type}({scope}): {message}"', 'cyan');
|
|
380
|
+
log('\nVariables:', 'bright');
|
|
381
|
+
log(' {message} - AI-generated description', 'blue');
|
|
382
|
+
log(' {type} - Commit type (feat, fix, etc.)', 'blue');
|
|
383
|
+
log(' {scope} - Commit scope (if any)', 'blue');
|
|
384
|
+
log(' {ticket} - Value from ticketNumber config', 'blue');
|
|
385
|
+
log(`\n📄 File: ${configPath}\n`, 'magenta');
|
|
386
|
+
} catch (error) {
|
|
387
|
+
log(`\n❌ Error creating config: ${error.message}`, 'red');
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ============================================================================
|
|
393
|
+
// MAIN LOGIC
|
|
394
|
+
// ============================================================================
|
|
395
|
+
|
|
396
|
+
function showHelp() {
|
|
397
|
+
log('\n╔════════════════════════════════════════════════════╗', 'cyan');
|
|
398
|
+
log('║ git-super - AI-Powered Git Commits ║', 'cyan');
|
|
399
|
+
log('╚════════════════════════════════════════════════════╝\n', 'cyan');
|
|
400
|
+
|
|
401
|
+
log('Usage:', 'bright');
|
|
402
|
+
log(' git super # Stage, commit with AI message, push', 'blue');
|
|
403
|
+
log(' git super --init # Create config file with defaults', 'blue');
|
|
404
|
+
log(' git super --dry-run # Preview AI message without committing', 'blue');
|
|
405
|
+
log(' git super --no-push # Commit but don\'t push', 'blue');
|
|
406
|
+
log(' git super --amend # Amend last commit with new AI message', 'blue');
|
|
407
|
+
log(' git super --no-verify # Skip pre-commit hooks', 'blue');
|
|
408
|
+
log(' git super --help # Show this help\n', 'blue');
|
|
409
|
+
|
|
410
|
+
log('Authentication (OAuth/SSO):', 'bright');
|
|
411
|
+
log(' git super auth login --provider <name> # Authenticate with OAuth', 'blue');
|
|
412
|
+
log(' git super auth status # Show auth status', 'blue');
|
|
413
|
+
log(' git super auth logout [--provider name] # Log out', 'blue');
|
|
414
|
+
log('', '');
|
|
415
|
+
log(' OAuth Providers:', 'cyan');
|
|
416
|
+
log(' • github-copilot GitHub Copilot Enterprise', 'blue');
|
|
417
|
+
log(' • azure-openai Azure OpenAI with Azure AD', 'blue');
|
|
418
|
+
log(' • generic-oidc Generic OIDC provider\n', 'blue');
|
|
419
|
+
|
|
420
|
+
log('Multi-Organization Context:', 'bright');
|
|
421
|
+
log(' git super context # Show current context', 'blue');
|
|
422
|
+
log(' git super context list # List all contexts', 'blue');
|
|
423
|
+
log(' git super context switch <id> # Switch to context\n', 'blue');
|
|
424
|
+
|
|
425
|
+
log('Configuration:', 'bright');
|
|
426
|
+
log(' Edit ~/.gitsuperrc to customize:', 'blue');
|
|
427
|
+
log(' • Message templates (Jira, Linear, GitHub issues)', 'blue');
|
|
428
|
+
log(' • Commit types and max length', 'blue');
|
|
429
|
+
log(' • AI provider defaults', 'blue');
|
|
430
|
+
log(' • Multiple organization contexts (enterprise)\n', 'blue');
|
|
431
|
+
|
|
432
|
+
log('Environment Variables:', 'bright');
|
|
433
|
+
log(' API Key Providers:', 'cyan');
|
|
434
|
+
log(' AI_PROVIDER - ollama, anthropic, openai', 'blue');
|
|
435
|
+
log(' AI_MODEL - model name/version', 'blue');
|
|
436
|
+
log(' OLLAMA_URL - http://localhost:11434 (default)', 'blue');
|
|
437
|
+
log(' ANTHROPIC_API_KEY - Anthropic API key', 'blue');
|
|
438
|
+
log(' OPENAI_API_KEY - OpenAI API key', 'blue');
|
|
439
|
+
log('', '');
|
|
440
|
+
log(' OAuth/Enterprise:', 'cyan');
|
|
441
|
+
log(' GITHUB_ORG - GitHub organization', 'blue');
|
|
442
|
+
log(' AZURE_TENANT_ID - Azure AD tenant ID', 'blue');
|
|
443
|
+
log(' AZURE_CLIENT_ID - Azure application client ID', 'blue');
|
|
444
|
+
log(' AZURE_OPENAI_ENDPOINT - Azure OpenAI resource endpoint', 'blue');
|
|
445
|
+
log(' OIDC_ISSUER - OIDC issuer URL', 'blue');
|
|
446
|
+
log(' OIDC_CLIENT_ID - OIDC client ID\n', 'blue');
|
|
447
|
+
|
|
448
|
+
log('Examples:', 'bright');
|
|
449
|
+
log(' # Basic usage (Ollama local)', 'blue');
|
|
450
|
+
log(' git super\n', 'green');
|
|
451
|
+
|
|
452
|
+
log(' # Authenticate with Azure OpenAI (enterprise)', 'blue');
|
|
453
|
+
log(' git super auth login --provider azure-openai', 'green');
|
|
454
|
+
log(' git super # Now uses Azure OpenAI with SSO\n', 'green');
|
|
455
|
+
|
|
456
|
+
log(' # Setup custom templates', 'blue');
|
|
457
|
+
log(' git super --init', 'green');
|
|
458
|
+
log(' # Edit ~/.gitsuperrc: "messageTemplate": "VTT-3020: {type}: {message}"', 'cyan');
|
|
459
|
+
log(' git super # Now all commits have VTT-3020 prefix\n', 'green');
|
|
460
|
+
|
|
461
|
+
log(' # Preview message', 'blue');
|
|
462
|
+
log(' git super --dry-run\n', 'green');
|
|
463
|
+
|
|
464
|
+
log(' # Use Claude', 'blue');
|
|
465
|
+
log(' AI_PROVIDER=anthropic git super\n', 'green');
|
|
466
|
+
|
|
467
|
+
log(' # Use different Ollama model', 'blue');
|
|
468
|
+
log(' AI_MODEL=qwen2.5-coder git super\n', 'green');
|
|
469
|
+
|
|
470
|
+
log('Installation:', 'bright');
|
|
471
|
+
log(' npm install -g @theia-core/git-super', 'blue');
|
|
472
|
+
log(' git config --global alias.super \'!git-super\'', 'blue');
|
|
473
|
+
log(' git super --init # Optional: customize config\n', 'blue');
|
|
474
|
+
|
|
475
|
+
log('Works with ANY git repo (Node.js, Python, Java, C++, etc.)\n', 'cyan');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function main() {
|
|
479
|
+
if (flags.init) {
|
|
480
|
+
initConfig();
|
|
481
|
+
process.exit(0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (flags.help) {
|
|
485
|
+
showHelp();
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
log('\n✨ git-super - AI-powered commits\n', 'cyan');
|
|
490
|
+
|
|
491
|
+
// Check if in git repo
|
|
492
|
+
try {
|
|
493
|
+
exec('git rev-parse --git-dir', { silent: true });
|
|
494
|
+
} catch {
|
|
495
|
+
log('❌ Not a git repository', 'red');
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const repoName = getRepoName();
|
|
500
|
+
log(`📦 Repository: ${repoName}`, 'blue');
|
|
501
|
+
|
|
502
|
+
// Check for changes
|
|
503
|
+
if (!hasChanges()) {
|
|
504
|
+
log('ℹ️ No changes to commit', 'yellow');
|
|
505
|
+
process.exit(0);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
// Stage all changes if not amending
|
|
510
|
+
if (!flags.amend) {
|
|
511
|
+
log('\n→ git add .', 'bright');
|
|
512
|
+
exec('git add .');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Get diff
|
|
516
|
+
const { diff, status, hasStaged, hasUnstaged } = getGitDiff();
|
|
517
|
+
|
|
518
|
+
if (!diff || (!hasStaged && !flags.amend)) {
|
|
519
|
+
log('ℹ️ No changes to commit', 'yellow');
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Generate commit message
|
|
524
|
+
const message = await generateCommitMessage(diff, status, repoName);
|
|
525
|
+
log(`\n📝 Commit message:\n`, 'magenta');
|
|
526
|
+
log(` "${message}"\n`, 'bright');
|
|
527
|
+
|
|
528
|
+
// Dry run - stop here
|
|
529
|
+
if (flags.dryRun) {
|
|
530
|
+
log('🔍 Dry run mode - no commit made', 'yellow');
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Commit
|
|
535
|
+
const commitFlags = [];
|
|
536
|
+
if (flags.amend) commitFlags.push('--amend');
|
|
537
|
+
if (flags.noVerify) commitFlags.push('--no-verify');
|
|
538
|
+
|
|
539
|
+
const commitCmd = `git commit ${commitFlags.join(' ')} -m "${message.replace(/"/g, '\\"')}"`;
|
|
540
|
+
log(`→ git commit${flags.amend ? ' --amend' : ''}${flags.noVerify ? ' --no-verify' : ''}`, 'bright');
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
exec(commitCmd);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
log('\n❌ Commit failed (possibly rejected by pre-commit hooks)', 'red');
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
log('✅ Commit successful', 'green');
|
|
550
|
+
|
|
551
|
+
// Push
|
|
552
|
+
if (!flags.noPush && !flags.amend) {
|
|
553
|
+
log('\n→ git push origin HEAD', 'bright');
|
|
554
|
+
try {
|
|
555
|
+
exec('git push origin HEAD');
|
|
556
|
+
log('✅ Push successful', 'green');
|
|
557
|
+
} catch (error) {
|
|
558
|
+
log('❌ Push failed', 'red');
|
|
559
|
+
log('Commit was successful but push failed. Run git push manually.', 'yellow');
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
} else if (flags.amend) {
|
|
563
|
+
log('\n💡 Tip: Use `git push --force-with-lease` to push amended commit', 'yellow');
|
|
564
|
+
} else {
|
|
565
|
+
log('\n💡 Commit successful (not pushed)', 'yellow');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
log('\n✨ Done!\n', 'cyan');
|
|
569
|
+
|
|
570
|
+
} catch (error) {
|
|
571
|
+
log(`\n❌ Error: ${error.message}`, 'red');
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
main();
|