ccommitpilot-ai 2.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/README.md ADDED
Binary file
package/bin/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { generateCommitMessage, configureSettings, showConfig } from '../src/index.js';
5
+ import { showPersonalityReport, showStreak } from '../src/personality.js';
6
+
7
+ program
8
+ .name('commit-sage')
9
+ .description('🧙 AI-powered git commit generator with personality scoring!')
10
+ .version('2.0.0');
11
+
12
+ // ── Default: generate commit message ──
13
+ program
14
+ .command('generate', { isDefault: true })
15
+ .alias('g')
16
+ .description('Generate a commit message from staged changes')
17
+ .option('-t, --type <type>', 'Commit type (feat, fix, docs, etc.)')
18
+ .option('-n, --count <number>', 'Number of suggestions to generate', '3')
19
+ .option('-m, --mood <mood>', 'Mood: professional | savage | poetic', 'professional')
20
+ .option('-l, --lang <lang>', 'Language: english | hinglish | hindi', 'english')
21
+ .option('--provider <provider>', 'AI provider: openai | ollama', null)
22
+ .action(generateCommitMessage);
23
+
24
+ // ── Score: personality report ──
25
+ program
26
+ .command('score')
27
+ .alias('s')
28
+ .description('Show your git personality score and team report')
29
+ .option('--team', 'Show full team report with awards')
30
+ .option('--single', 'Show only your personal score')
31
+ .action((options) => showPersonalityReport(options));
32
+
33
+ // ── Streak: commit streak tracker ──
34
+ program
35
+ .command('streak')
36
+ .description('Show your current commit streak 🔥')
37
+ .action(showStreak);
38
+
39
+ // ── Config ──
40
+ program
41
+ .command('config')
42
+ .description('Configure commit-sage settings')
43
+ .option('-k, --key <apiKey>', 'Set your OpenAI API key')
44
+ .option('-p, --provider <provider>', 'Default AI provider: openai | ollama')
45
+ .option('-m, --mood <mood>', 'Default mood: professional | savage | poetic')
46
+ .option('-l, --lang <lang>', 'Default language: english | hinglish | hindi')
47
+ .option('-s, --show', 'Show current configuration')
48
+ .action((options) => {
49
+ if (options.show) return showConfig();
50
+ configureSettings(options);
51
+ });
52
+
53
+ program.parse();
Binary file
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ccommitpilot-ai",
3
+ "version": "2.0.0",
4
+ "description": "An AI-powered CLI tool that automatically generates meaningful, well-structured git commit messages from your staged changes. Supports free offline mode via Ollama, mood personalities, git personality scoring, and team leaderboards.",
5
+ "bin": {
6
+ "commit-sage": "./bin/cli.js"
7
+ },
8
+ "main": "./src/index.js",
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/cli.js",
12
+ "test": "echo \"No tests yet\" && exit 0"
13
+ },
14
+ "keywords": [
15
+ "git",
16
+ "commit",
17
+ "ai",
18
+ "cli",
19
+ "ollama",
20
+ "openai",
21
+ "conventional-commits",
22
+ "developer-tools",
23
+ "automation"
24
+ ],
25
+ "author": "Ankur Ojha",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/ankurojha834/commitpilot-ai.git"
30
+ },
31
+ "homepage": "https://github.com/ankurojha834/commitpilot-ai#readme",
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "dependencies": {
36
+ "chalk": "^5.3.0",
37
+ "commander": "^12.0.0",
38
+ "inquirer": "^9.2.0",
39
+ "ora": "^8.0.0"
40
+ }
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.log("🔥 commit-sage is working!");
4
+ import { execSync } from 'child_process';
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import inquirer from 'inquirer';
11
+
12
+ // ─── Config ───────────────────────────────────────────────────────────────────
13
+
14
+ const CONFIG_DIR = join(homedir(), '.commit-sage');
15
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
16
+
17
+ function getConfig() {
18
+ if (!existsSync(CONFIG_FILE)) return {};
19
+ try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); }
20
+ catch { return {}; }
21
+ }
22
+
23
+ function saveConfig(data) {
24
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
25
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
26
+ }
27
+
28
+ export function configureSettings(options) {
29
+ const config = getConfig();
30
+ if (options.key) { config.openaiApiKey = options.key; console.log(chalk.green('✅ API key saved!')); }
31
+ if (options.provider) { config.defaultProvider = options.provider; console.log(chalk.green(`✅ Default provider: ${options.provider}`)); }
32
+ if (options.mood) { config.defaultMood = options.mood; console.log(chalk.green(`✅ Default mood: ${options.mood}`)); }
33
+ if (options.lang) { config.defaultLang = options.lang; console.log(chalk.green(`✅ Default language: ${options.lang}`)); }
34
+ saveConfig(config);
35
+ }
36
+
37
+ export function showConfig() {
38
+ const config = getConfig();
39
+ console.log('');
40
+ console.log(chalk.bold.magenta(' 🧙 commit-sage config'));
41
+ console.log(chalk.gray(' ─────────────────────────────'));
42
+ const key = config.openaiApiKey;
43
+ console.log(chalk.cyan(' API Key: '), key ? chalk.gray(key.slice(0,7) + '...' + key.slice(-4)) : chalk.red('Not set'));
44
+ console.log(chalk.cyan(' Provider: '), chalk.white(config.defaultProvider || 'not set (will ask)'));
45
+ console.log(chalk.cyan(' Mood: '), chalk.white(config.defaultMood || 'professional'));
46
+ console.log(chalk.cyan(' Language: '), chalk.white(config.defaultLang || 'english'));
47
+ console.log('');
48
+ }
49
+
50
+ // ─── Mood Prompts ─────────────────────────────────────────────────────────────
51
+
52
+ const MOOD_PROMPTS = {
53
+ professional: `You are a senior software engineer writing clean, professional git commit messages.
54
+ Follow Conventional Commits strictly. Be precise, clear, and concise.`,
55
+
56
+ savage: `You are a brutally honest, sarcastic developer writing git commit messages.
57
+ Be funny, savage, and real — but still describe the actual change.
58
+ Examples of style:
59
+ - "fix: undid what someone broke at 2am again"
60
+ - "feat: added feature nobody asked for but PM insisted"
61
+ - "chore: deleted 500 lines of spaghetti called architecture"
62
+ Still follow conventional commits format but make it spicy and real!`,
63
+
64
+ poetic: `You are a poetic, dramatic developer who writes commit messages like literature.
65
+ Be creative and metaphorical but the message must still convey the actual change.
66
+ Examples of style:
67
+ - "feat: and thus the user could finally login, as dawn breaks"
68
+ - "fix: slayed the null pointer dragon haunting production"
69
+ - "refactor: untangled the web of chaos into elegant simplicity"
70
+ Still use conventional commits format.`
71
+ };
72
+
73
+ // ─── Language Prompts ─────────────────────────────────────────────────────────
74
+
75
+ const LANG_PROMPTS = {
76
+ english: `Write commit messages in clear English.`,
77
+
78
+ hinglish: `Write commit messages in Hinglish (mix of Hindi + English in Roman script).
79
+ This is how Indian developers actually talk to each other.
80
+ Examples of style:
81
+ - "feat(auth): login ka jugaad laga diya, ab kaam karega"
82
+ - "fix: woh wala bug thik kiya jo pata nahi kahan se aaya tha"
83
+ - "chore: purana code saaf kar diya, bahut gandagi thi yaar"
84
+ - "feat: naya feature add kiya, PM khush ho jayenge"
85
+ Keep it natural, relatable, and fun like real desi dev chat!`,
86
+
87
+ hindi: `Write commit messages in Hindi using Devanagari script.
88
+ Examples:
89
+ - "feat: उपयोगकर्ता लॉगिन सुविधा जोड़ी"
90
+ - "fix: प्रमाणीकरण त्रुटि को ठीक किया"
91
+ Keep it clear and professional.`
92
+ };
93
+
94
+ // ─── Git Helpers ──────────────────────────────────────────────────────────────
95
+
96
+ function getStagedDiff() {
97
+ try { return execSync('git diff --cached', { encoding: 'utf-8' }).trim(); }
98
+ catch { return null; }
99
+ }
100
+
101
+ function isGitRepo() {
102
+ try { execSync('git rev-parse --git-dir', { stdio: 'ignore' }); return true; }
103
+ catch { return false; }
104
+ }
105
+
106
+ function getChangedFiles() {
107
+ try { return execSync('git diff --cached --name-only', { encoding: 'utf-8' }).trim(); }
108
+ catch { return ''; }
109
+ }
110
+
111
+ // ─── AI Providers ─────────────────────────────────────────────────────────────
112
+
113
+ async function callOpenAI(apiKey, prompt) {
114
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ Authorization: `Bearer ${apiKey}`,
119
+ },
120
+ body: JSON.stringify({
121
+ model: 'gpt-4o-mini',
122
+ messages: [{ role: 'user', content: prompt }],
123
+ temperature: 0.8,
124
+ max_tokens: 400,
125
+ }),
126
+ });
127
+ if (!response.ok) {
128
+ const err = await response.json();
129
+ throw new Error(err.error?.message || 'OpenAI API error');
130
+ }
131
+ const data = await response.json();
132
+ return data.choices[0].message.content.trim();
133
+ }
134
+
135
+ async function callOllama(prompt) {
136
+ try {
137
+ const health = await fetch('http://localhost:11434/api/tags');
138
+ if (!health.ok) throw new Error();
139
+ } catch {
140
+ throw new Error(
141
+ 'Ollama is not running!\n' +
142
+ ' Start it with: ollama serve\n' +
143
+ ' Pull a model: ollama pull llama3'
144
+ );
145
+ }
146
+
147
+ const response = await fetch('http://localhost:11434/api/generate', {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ body: JSON.stringify({
151
+ model: 'llama3',
152
+ prompt,
153
+ stream: false,
154
+ options: { temperature: 0.8 }
155
+ }),
156
+ });
157
+
158
+ if (!response.ok) throw new Error('Ollama request failed. Is llama3 installed? Run: ollama pull llama3');
159
+ const data = await response.json();
160
+ return data.response?.trim();
161
+ }
162
+
163
+ // ─── Build Prompt ─────────────────────────────────────────────────────────────
164
+
165
+ function buildPrompt(diff, count, type, mood, lang) {
166
+ const moodInstr = MOOD_PROMPTS[mood] || MOOD_PROMPTS.professional;
167
+ const langInstr = LANG_PROMPTS[lang] || LANG_PROMPTS.english;
168
+ const typeInstr = type
169
+ ? `Use the conventional commit type: "${type}".`
170
+ : 'Choose the best conventional commit type (feat, fix, docs, style, refactor, test, chore).';
171
+
172
+ return `${moodInstr}
173
+
174
+ ${langInstr}
175
+
176
+ ${typeInstr}
177
+
178
+ Analyze this git diff and generate EXACTLY ${count} different commit message suggestions.
179
+
180
+ CRITICAL: Return ONLY a raw JSON array of strings. No explanation, no markdown, no backticks.
181
+ Correct format: ["message one", "message two", "message three"]
182
+
183
+ Git diff:
184
+ ${diff.slice(0, 4000)}`;
185
+ }
186
+
187
+ function parseMessages(raw) {
188
+ const match = raw.match(/\[[\s\S]*?\]/);
189
+ if (!match) throw new Error('Could not parse AI response. Try again!');
190
+ return JSON.parse(match[0]);
191
+ }
192
+
193
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
194
+
195
+ const MOOD_EMOJI = { professional: '💼', savage: '💀', poetic: '🌸' };
196
+ const LANG_EMOJI = { english: '🇬🇧', hinglish: '🇮🇳', hindi: '🇮🇳' };
197
+
198
+ async function askProvider() {
199
+ const { provider } = await inquirer.prompt([{
200
+ type: 'list',
201
+ name: 'provider',
202
+ message: chalk.bold(' Choose AI provider:'),
203
+ choices: [
204
+ { name: ' 💻 Ollama — FREE, runs offline (needs Ollama installed)', value: 'ollama' },
205
+ { name: ' 🤖 OpenAI — Best quality (needs API key)', value: 'openai' },
206
+ ]
207
+ }]);
208
+ return provider;
209
+ }
210
+
211
+ // ─── Main Command ─────────────────────────────────────────────────────────────
212
+
213
+ export async function generateCommitMessage(options) {
214
+ const config = getConfig();
215
+
216
+ const mood = options.mood || config.defaultMood || 'professional';
217
+ const lang = options.lang || config.defaultLang || 'english';
218
+ let provider = options.provider || config.defaultProvider || null;
219
+
220
+ console.log('');
221
+ console.log(chalk.bold.magenta(' 🧙 commit-sage v2.0'));
222
+ console.log(chalk.gray(' ─────────────────────────────'));
223
+ console.log(` ${MOOD_EMOJI[mood] || '💼'} Mood: ${chalk.cyan(mood)} ${LANG_EMOJI[lang] || '🌐'} Lang: ${chalk.cyan(lang)}`);
224
+ console.log(chalk.gray(' ─────────────────────────────'));
225
+
226
+ // Git checks
227
+ if (!isGitRepo()) {
228
+ console.log(chalk.red('\n ❌ Not a git repository.\n'));
229
+ process.exit(1);
230
+ }
231
+
232
+ const diff = getStagedDiff();
233
+ const files = getChangedFiles();
234
+
235
+ if (!diff) {
236
+ console.log(chalk.yellow('\n ⚠️ No staged changes found.'));
237
+ console.log(chalk.gray(' Stage files first: git add <files>\n'));
238
+ process.exit(0);
239
+ }
240
+
241
+ // Show staged files
242
+ console.log(chalk.cyan(' 📂 Staged files:'));
243
+ files.split('\n').filter(Boolean).forEach(f => console.log(chalk.gray(` • ${f}`)));
244
+ console.log('');
245
+
246
+ // Pick provider if not set
247
+ if (!provider) provider = await askProvider();
248
+ console.log('');
249
+
250
+ // Validate OpenAI key
251
+ if (provider === 'openai') {
252
+ const apiKey = process.env.OPENAI_API_KEY || config.openaiApiKey;
253
+ if (!apiKey) {
254
+ console.log(chalk.red(' ❌ No OpenAI API key found!'));
255
+ console.log(chalk.yellow(' Run: commit-sage config --key sk-...'));
256
+ console.log(chalk.gray(' Or use Ollama for free: commit-sage config --provider ollama'));
257
+ process.exit(1);
258
+ }
259
+ }
260
+
261
+ // Generate messages
262
+ const spinner = ora({
263
+ text: chalk.gray(provider === 'ollama'
264
+ ? ' Thinking locally (Ollama)...'
265
+ : ' Asking OpenAI...'),
266
+ prefixText: ' ',
267
+ }).start();
268
+
269
+ let messages;
270
+ try {
271
+ const prompt = buildPrompt(diff, parseInt(options.count), options.type, mood, lang);
272
+ const apiKey = process.env.OPENAI_API_KEY || config.openaiApiKey;
273
+ const raw = provider === 'ollama'
274
+ ? await callOllama(prompt)
275
+ : await callOpenAI(apiKey, prompt);
276
+ messages = parseMessages(raw);
277
+ spinner.succeed(chalk.green(' Done! Here are your suggestions:'));
278
+ } catch (err) {
279
+ spinner.fail(chalk.red(` Failed: ${err.message}`));
280
+ process.exit(1);
281
+ }
282
+
283
+ console.log('');
284
+
285
+ // Interactive picker
286
+ const { chosen } = await inquirer.prompt([{
287
+ type: 'list',
288
+ name: 'chosen',
289
+ message: chalk.bold(' Pick your commit message:'),
290
+ choices: [
291
+ ...messages.map(msg => ({ name: ` ${msg}`, value: msg })),
292
+ new inquirer.Separator(),
293
+ { name: ' ✏️ Write my own', value: '__custom__' },
294
+ { name: ' 🔄 Regenerate suggestions', value: '__regen__' },
295
+ { name: ' ❌ Cancel', value: '__cancel__' },
296
+ ],
297
+ pageSize: 12,
298
+ }]);
299
+
300
+ if (chosen === '__cancel__') {
301
+ console.log(chalk.gray('\n Cancelled. Nothing committed.\n'));
302
+ process.exit(0);
303
+ }
304
+
305
+ if (chosen === '__regen__') {
306
+ console.log(chalk.yellow('\n Regenerating...\n'));
307
+ return generateCommitMessage(options);
308
+ }
309
+
310
+ let finalMessage = chosen;
311
+
312
+ if (chosen === '__custom__') {
313
+ const { custom } = await inquirer.prompt([{
314
+ type: 'input',
315
+ name: 'custom',
316
+ message: ' Your commit message:',
317
+ validate: v => v.trim().length > 0 || 'Please enter a message',
318
+ }]);
319
+ finalMessage = custom.trim();
320
+ }
321
+
322
+ // Confirm
323
+ const { confirm } = await inquirer.prompt([{
324
+ type: 'confirm',
325
+ name: 'confirm',
326
+ message: chalk.bold(` Commit with: "${chalk.cyan(finalMessage)}"?`),
327
+ default: true,
328
+ }]);
329
+
330
+ if (!confirm) {
331
+ console.log(chalk.gray('\n Cancelled.\n'));
332
+ process.exit(0);
333
+ }
334
+
335
+ // Run git commit
336
+ try {
337
+ execSync(`git commit -m "${finalMessage.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
338
+ console.log('');
339
+ console.log(chalk.green.bold(' ✅ Committed successfully!'));
340
+ console.log(chalk.gray(` "${finalMessage}"\n`));
341
+ } catch {
342
+ console.log(chalk.red('\n ❌ Commit failed.\n'));
343
+ process.exit(1);
344
+ }
345
+ }
@@ -0,0 +1,386 @@
1
+ import { execSync } from 'child_process';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+
5
+ // ─── Git History Helpers ──────────────────────────────────────────────────────
6
+
7
+ function getAllCommits() {
8
+ try {
9
+ const raw = execSync(
10
+ 'git log --pretty=format:"%an|%ae|%s|%ai" --no-merges',
11
+ { encoding: 'utf-8' }
12
+ ).trim();
13
+ if (!raw) return [];
14
+ return raw.split('\n').map(line => {
15
+ const [name, email, message, date] = line.split('|');
16
+ return { name: name?.trim(), email: email?.trim(), message: message?.trim(), date: new Date(date) };
17
+ }).filter(c => c.message);
18
+ } catch { return []; }
19
+ }
20
+
21
+ function isGitRepo() {
22
+ try { execSync('git rev-parse --git-dir', { stdio: 'ignore' }); return true; }
23
+ catch { return false; }
24
+ }
25
+
26
+ // ─── Commit Classifiers ───────────────────────────────────────────────────────
27
+
28
+ const LAZY_PATTERNS = [
29
+ /^fix$/i, /^fixes$/i, /^fixed$/i, /^update$/i, /^updated$/i,
30
+ /^changes$/i, /^misc/i, /^wip/i, /^temp/i, /^test$/i,
31
+ /^asdf/i, /^asd/i, /^lol/i, /^idk/i, /^done$/i, /^ok$/i,
32
+ /^working/i, /^blah/i, /^stuff$/i, /^things$/i, /^hm/i,
33
+ /^oops/i, /^whoops/i, /^ugh/i, /^yolo/i, /^please work/i,
34
+ /^god/i, /^wtf/i, /^ffs/i, /^no idea/i, /^\?\?/,
35
+ /^\.+$/, /^\d+$/, /^[a-z]$/i
36
+ ];
37
+
38
+ const FIRE_PATTERNS = [
39
+ /hotfix/i, /urgent/i, /production/i, /prod/i, /critical/i,
40
+ /emergency/i, /revert/i, /rollback/i, /on fire/i, /burning/i,
41
+ /crash/i, /broke/i, /breaking/i, /disaster/i
42
+ ];
43
+
44
+ const SAVAGE_PATTERNS = [
45
+ /finally/i, /idk why/i, /somehow/i, /magic/i, /no idea why/i,
46
+ /hack/i, /please/i, /again/i, /still/i, /why/i, /sigh/i,
47
+ /cursed/i, /should work/i, /maybe/i, /hopefully/i, /pray/i
48
+ ];
49
+
50
+ const CONVENTIONAL_PATTERN = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{5,}/;
51
+
52
+ const LATE_NIGHT_HOURS = [22, 23, 0, 1, 2, 3, 4];
53
+
54
+ function classifyCommit(commit) {
55
+ const msg = commit.message;
56
+ const hour = commit.date.getHours();
57
+ return {
58
+ isLazy: LAZY_PATTERNS.some(p => p.test(msg)) || msg.length < 5,
59
+ isFire: FIRE_PATTERNS.some(p => p.test(msg)),
60
+ isSavage: SAVAGE_PATTERNS.some(p => p.test(msg)),
61
+ isConventional: CONVENTIONAL_PATTERN.test(msg),
62
+ isLateNight: LATE_NIGHT_HOURS.includes(hour),
63
+ isWeekend: [0, 6].includes(commit.date.getDay()),
64
+ hour
65
+ };
66
+ }
67
+
68
+ // ─── Awards ───────────────────────────────────────────────────────────────────
69
+
70
+ function getAwards(stats) {
71
+ const awards = [];
72
+ const authors = Object.values(stats);
73
+
74
+ // Most lazy commits
75
+ const laziestPerson = authors.sort((a, b) => b.lazy - a.lazy)[0];
76
+ if (laziestPerson?.lazy > 0) {
77
+ awards.push({
78
+ emoji: '🏆',
79
+ title: '"What does this even mean?" Award',
80
+ winner: laziestPerson.name,
81
+ reason: `${laziestPerson.lazy} lazy commits like "fix", "update", "misc"`
82
+ });
83
+ }
84
+
85
+ // Most late night commits
86
+ const nightOwl = [...authors].sort((a, b) => b.lateNight - a.lateNight)[0];
87
+ if (nightOwl?.lateNight > 2) {
88
+ awards.push({
89
+ emoji: '🦉',
90
+ title: '"Neend Kisko Chahiye?" Award',
91
+ winner: nightOwl.name,
92
+ reason: `${nightOwl.lateNight} commits after 10pm. Bhai so ja!`
93
+ });
94
+ }
95
+
96
+ // Most fire/production commits
97
+ const fireman = [...authors].sort((a, b) => b.fire - a.fire)[0];
98
+ if (fireman?.fire > 0) {
99
+ awards.push({
100
+ emoji: '🚒',
101
+ title: '"Production Pe Aag" Award',
102
+ winner: fireman.name,
103
+ reason: `${fireman.fire} hotfix/emergency commits. Chaos specialist!`
104
+ });
105
+ }
106
+
107
+ // Most conventional commits (best writer)
108
+ const bestWriter = [...authors].sort((a, b) => b.conventional - a.conventional)[0];
109
+ if (bestWriter?.conventional > 3) {
110
+ awards.push({
111
+ emoji: '🎖️',
112
+ title: '"Seedha Saadha Dev" Award',
113
+ winner: bestWriter.name,
114
+ reason: `${bestWriter.conventional} clean conventional commits. Respect!`
115
+ });
116
+ }
117
+
118
+ // Weekend warrior
119
+ const weekendDev = [...authors].sort((a, b) => b.weekend - a.weekend)[0];
120
+ if (weekendDev?.weekend > 2) {
121
+ awards.push({
122
+ emoji: '😵',
123
+ title: '"Weekend Ka Kya Matlab?" Award',
124
+ winner: weekendDev.name,
125
+ reason: `${weekendDev.weekend} commits on weekends. Bhai chutti le!`
126
+ });
127
+ }
128
+
129
+ return awards;
130
+ }
131
+
132
+ // ─── Personality Label ────────────────────────────────────────────────────────
133
+
134
+ function getPersonality(s) {
135
+ const ratio = s.total > 0 ? s.conventional / s.total : 0;
136
+
137
+ if (s.fire > 5) return { label: '🚒 Firefighter', desc: 'Production ka rakhwala' };
138
+ if (ratio > 0.8) return { label: '💼 The Professional', desc: 'Clean commits, clean life' };
139
+ if (s.lateNight > 10) return { label: '🦉 Night Owl', desc: 'Raat ko jeevan milta hai' };
140
+ if (s.lazy > s.total * 0.5) return { label: '😴 The Lazy Dev', desc: '"fix" aur "update" fan' };
141
+ if (s.savage > 5) return { label: '💀 The Savage', desc: '"idk why this works" energy' };
142
+ if (s.weekend > 5) return { label: '😵 No Life Dev', desc: 'Weekend? Kya hota hai?' };
143
+ if (ratio > 0.5) return { label: '📚 The Learner', desc: 'Improving every day' };
144
+ return { label: '🎲 The Chaotic', desc: 'Unpredictable but creative' };
145
+ }
146
+
147
+ // ─── Score Calculator ─────────────────────────────────────────────────────────
148
+
149
+ function calcScore(s) {
150
+ if (s.total === 0) return 0;
151
+ let score = 50;
152
+ score += Math.min(30, (s.conventional / s.total) * 50);
153
+ score -= Math.min(30, (s.lazy / s.total) * 40);
154
+ score -= Math.min(10, s.fire * 2);
155
+ score += Math.min(10, s.streak * 2);
156
+ return Math.max(0, Math.min(100, Math.round(score)));
157
+ }
158
+
159
+ function scoreBar(score) {
160
+ const filled = Math.round(score / 10);
161
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
162
+ const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
163
+ return color(bar) + chalk.gray(` ${score}/100`);
164
+ }
165
+
166
+ // ─── Streak Calculator ────────────────────────────────────────────────────────
167
+
168
+ function calcStreak(commits) {
169
+ if (!commits.length) return 0;
170
+ const days = [...new Set(commits.map(c =>
171
+ c.date.toISOString().split('T')[0]
172
+ ))].sort().reverse();
173
+
174
+ let streak = 1;
175
+ for (let i = 1; i < days.length; i++) {
176
+ const diff = (new Date(days[i-1]) - new Date(days[i])) / 86400000;
177
+ if (diff <= 1) streak++;
178
+ else break;
179
+ }
180
+ return streak;
181
+ }
182
+
183
+ // ─── Main Report ──────────────────────────────────────────────────────────────
184
+
185
+ export async function showPersonalityReport(options) {
186
+ console.log('');
187
+
188
+ if (!isGitRepo()) {
189
+ console.log(chalk.red(' ❌ Not a git repository.'));
190
+ process.exit(1);
191
+ }
192
+
193
+ const spinner = ora({ text: chalk.gray(' Analyzing commit history...'), prefixText: ' ' }).start();
194
+ const commits = getAllCommits();
195
+
196
+ if (!commits.length) {
197
+ spinner.fail(chalk.yellow(' No commits found in this repo!'));
198
+ process.exit(0);
199
+ }
200
+
201
+ // Build per-author stats
202
+ const stats = {};
203
+ for (const commit of commits) {
204
+ const key = commit.email || commit.name;
205
+ if (!stats[key]) {
206
+ stats[key] = {
207
+ name: commit.name,
208
+ total: 0, lazy: 0, fire: 0, savage: 0,
209
+ conventional: 0, lateNight: 0, weekend: 0,
210
+ commits: [], streak: 0
211
+ };
212
+ }
213
+ const s = stats[key];
214
+ const cls = classifyCommit(commit);
215
+ s.total++;
216
+ s.commits.push(commit);
217
+ if (cls.isLazy) s.lazy++;
218
+ if (cls.isFire) s.fire++;
219
+ if (cls.isSavage) s.savage++;
220
+ if (cls.isConventional) s.conventional++;
221
+ if (cls.isLateNight) s.lateNight++;
222
+ if (cls.isWeekend) s.weekend++;
223
+ }
224
+
225
+ // Calc streaks
226
+ for (const key of Object.keys(stats)) {
227
+ stats[key].streak = calcStreak(stats[key].commits);
228
+ }
229
+
230
+ spinner.succeed(chalk.green(` Analyzed ${commits.length} commits across ${Object.keys(stats).length} author(s)!`));
231
+
232
+ const isSingle = options?.single || Object.keys(stats).length === 1;
233
+
234
+ if (isSingle) {
235
+ // ── Single author mode ──
236
+ const s = Object.values(stats)[0];
237
+ const score = calcScore(s);
238
+ const personality = getPersonality(s);
239
+
240
+ console.log('');
241
+ console.log(chalk.bold.magenta(' 🧙 commit-sage — Personality Report'));
242
+ console.log(chalk.gray(' ══════════════════════════════════════'));
243
+ console.log(` ${chalk.bold(personality.label)}`);
244
+ console.log(` ${chalk.gray(personality.desc)}`);
245
+ console.log('');
246
+ console.log(` ${chalk.cyan('Commit Score:')} ${scoreBar(score)}`);
247
+ console.log('');
248
+ console.log(` ${chalk.cyan('Total commits:')} ${chalk.white(s.total)}`);
249
+ console.log(` ${chalk.cyan('Conventional:')} ${chalk.green(s.conventional)} ${chalk.gray(`(${Math.round(s.conventional/s.total*100)}%)`)}`);
250
+ console.log(` ${chalk.cyan('Lazy commits:')} ${chalk.red(s.lazy)} ${s.lazy > 5 ? chalk.red('← bhai yeh theek karo') : ''}`);
251
+ console.log(` ${chalk.cyan('Hotfixes:')} ${s.fire > 3 ? chalk.red(s.fire + ' 🔥') : chalk.white(s.fire)}`);
252
+ console.log(` ${chalk.cyan('Late night:')} ${s.lateNight > 5 ? chalk.yellow(s.lateNight + ' 🦉') : chalk.white(s.lateNight)}`);
253
+ console.log(` ${chalk.cyan('Weekend commits:')} ${s.weekend > 3 ? chalk.yellow(s.weekend + ' 😵') : chalk.white(s.weekend)}`);
254
+ console.log(` ${chalk.cyan('Current streak:')} ${chalk.bold.green(s.streak + ' days 🔥')}`);
255
+
256
+ // Laziest commit
257
+ const lazyCommits = s.commits
258
+ .filter(c => LAZY_PATTERNS.some(p => p.test(c.message)) || c.message.length < 5)
259
+ .slice(0, 3);
260
+ if (lazyCommits.length) {
261
+ console.log('');
262
+ console.log(chalk.gray(' 😬 Your "best" commits:'));
263
+ lazyCommits.forEach(c => console.log(chalk.red(` • "${c.message}"`)));
264
+ }
265
+
266
+ // Tip
267
+ console.log('');
268
+ if (s.lazy > s.total * 0.4) {
269
+ console.log(chalk.yellow(' 💡 Tip: Bahut lazy commits hain. commit-sage regularly use karo!'));
270
+ } else if (score >= 70) {
271
+ console.log(chalk.green(' 🏆 Bhai clean commits likhta hai tu! Keep it up!'));
272
+ } else {
273
+ console.log(chalk.cyan(' 💡 Tip: More conventional commits = better score!'));
274
+ }
275
+
276
+ } else {
277
+ // ── Team report mode ──
278
+ const authors = Object.values(stats).sort((a, b) => calcScore(b) - calcScore(a));
279
+
280
+ console.log('');
281
+ console.log(chalk.bold.magenta(' 🧙 commit-sage — Team Report'));
282
+ console.log(chalk.gray(' ══════════════════════════════════════════════════'));
283
+
284
+ for (const s of authors) {
285
+ const score = calcScore(s);
286
+ const personality = getPersonality(s);
287
+ console.log('');
288
+ console.log(` ${chalk.bold.white(s.name)} ${chalk.gray(personality.label)}`);
289
+ console.log(` ${scoreBar(score)} ${chalk.gray('streak: ' + s.streak + 'd 🔥')}`);
290
+ console.log(
291
+ ` ${chalk.gray('total:')} ${chalk.white(s.total)} ` +
292
+ `${chalk.gray('clean:')} ${chalk.green(s.conventional)} ` +
293
+ `${chalk.gray('lazy:')} ${chalk.red(s.lazy)} ` +
294
+ `${chalk.gray('3am:')} ${s.lateNight > 3 ? chalk.yellow(s.lateNight + '🦉') : chalk.white(s.lateNight)}`
295
+ );
296
+ }
297
+
298
+ // Awards
299
+ const awards = getAwards(stats);
300
+ if (awards.length) {
301
+ console.log('');
302
+ console.log(chalk.gray(' ══════════════════════════════════════════════════'));
303
+ console.log(chalk.bold.yellow(' 🏅 Weekly Awards'));
304
+ console.log(chalk.gray(' ══════════════════════════════════════════════════'));
305
+ for (const award of awards) {
306
+ console.log('');
307
+ console.log(` ${award.emoji} ${chalk.bold(award.title)}`);
308
+ console.log(` ${chalk.cyan('Winner:')} ${chalk.white(award.winner)}`);
309
+ console.log(` ${chalk.gray(award.reason)}`);
310
+ }
311
+ }
312
+ }
313
+
314
+ console.log('');
315
+ console.log(chalk.gray(' ══════════════════════════════════════════════════'));
316
+ console.log(chalk.gray(' Run: commit-sage score --team for full team view'));
317
+ console.log(chalk.gray(' Run: commit-sage score --single for your own score'));
318
+ console.log('');
319
+ }
320
+
321
+ // ─── Streak Command ───────────────────────────────────────────────────────────
322
+
323
+ export function showStreak() {
324
+ if (!isGitRepo()) {
325
+ console.log(chalk.red('\n ❌ Not a git repository.\n'));
326
+ process.exit(1);
327
+ }
328
+
329
+ const commits = getAllCommits();
330
+ if (!commits.length) {
331
+ console.log(chalk.yellow('\n No commits found!\n'));
332
+ return;
333
+ }
334
+
335
+ // Get current user's commits
336
+ let currentUser;
337
+ try {
338
+ currentUser = execSync('git config user.email', { encoding: 'utf-8' }).trim();
339
+ } catch { currentUser = null; }
340
+
341
+ const myCommits = currentUser
342
+ ? commits.filter(c => c.email === currentUser)
343
+ : commits;
344
+
345
+ const streak = calcStreak(myCommits);
346
+ const days = [...new Set(myCommits.map(c => c.date.toISOString().split('T')[0]))].sort().reverse();
347
+ const todayCommits = myCommits.filter(c =>
348
+ c.date.toISOString().split('T')[0] === new Date().toISOString().split('T')[0]
349
+ );
350
+
351
+ console.log('');
352
+ console.log(chalk.bold.magenta(' 🔥 commit-sage — Streak Report'));
353
+ console.log(chalk.gray(' ─────────────────────────────'));
354
+
355
+ const flames = '🔥'.repeat(Math.min(streak, 7));
356
+ console.log(` ${chalk.bold.yellow(`${streak}-day streak`)} ${flames}`);
357
+ console.log('');
358
+ console.log(` ${chalk.cyan('Today\'s commits:')} ${chalk.white(todayCommits.length)}`);
359
+ console.log(` ${chalk.cyan('Active days:')} ${chalk.white(days.length)}`);
360
+ console.log(` ${chalk.cyan('Total commits:')} ${chalk.white(myCommits.length)}`);
361
+
362
+ if (todayCommits.length === 0) {
363
+ console.log('');
364
+ console.log(chalk.yellow(' ⚠️ Aaj commit nahi kiya! Streak toot jayegi!'));
365
+ console.log(chalk.gray(' Kuch bhi karo, commit karo! 😄'));
366
+ } else {
367
+ console.log('');
368
+ console.log(chalk.green(' ✅ Aaj commit kar diya! Streak safe hai!'));
369
+ }
370
+
371
+ // Last broken streak message
372
+ if (days.length > 1) {
373
+ const lastCommit = myCommits[myCommits.length - 1];
374
+ const worstCommit = myCommits
375
+ .filter(c => LAZY_PATTERNS.some(p => p.test(c.message)))
376
+ .sort(() => Math.random() - 0.5)[0];
377
+
378
+ if (worstCommit) {
379
+ console.log('');
380
+ console.log(chalk.gray(` 😅 Your most "creative" commit ever:`));
381
+ console.log(chalk.red(` "${worstCommit.message}"`));
382
+ }
383
+ }
384
+
385
+ console.log('');
386
+ }