@snapcommit/cli 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/README.md +162 -0
- package/dist/ai/anthropic-client.js +92 -0
- package/dist/ai/commit-generator.js +200 -0
- package/dist/ai/gemini-client.js +201 -0
- package/dist/ai/git-interpreter.js +209 -0
- package/dist/ai/smart-solver.js +260 -0
- package/dist/auth/supabase-client.js +288 -0
- package/dist/commands/activate.js +108 -0
- package/dist/commands/commit.js +255 -0
- package/dist/commands/conflict.js +233 -0
- package/dist/commands/doctor.js +113 -0
- package/dist/commands/git-advanced.js +311 -0
- package/dist/commands/github-auth.js +193 -0
- package/dist/commands/login.js +11 -0
- package/dist/commands/natural.js +305 -0
- package/dist/commands/onboard.js +111 -0
- package/dist/commands/quick.js +173 -0
- package/dist/commands/setup.js +163 -0
- package/dist/commands/stats.js +128 -0
- package/dist/commands/uninstall.js +131 -0
- package/dist/db/database.js +99 -0
- package/dist/index.js +144 -0
- package/dist/lib/auth.js +171 -0
- package/dist/lib/github.js +280 -0
- package/dist/lib/multi-repo.js +276 -0
- package/dist/lib/supabase.js +153 -0
- package/dist/license/manager.js +203 -0
- package/dist/repl/index.js +185 -0
- package/dist/repl/interpreter.js +524 -0
- package/dist/utils/analytics.js +36 -0
- package/dist/utils/auth-storage.js +65 -0
- package/dist/utils/dopamine.js +211 -0
- package/dist/utils/errors.js +56 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/heatmap.js +265 -0
- package/dist/utils/rate-limit.js +68 -0
- package/dist/utils/retry.js +46 -0
- package/dist/utils/ui.js +189 -0
- package/dist/utils/version.js +81 -0
- package/package.json +69 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dopamine-inducing features
|
|
4
|
+
* Streaks, milestones, achievements, motivational messages
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.calculateStreak = calculateStreak;
|
|
11
|
+
exports.getMotivationalMessage = getMotivationalMessage;
|
|
12
|
+
exports.getMilestone = getMilestone;
|
|
13
|
+
exports.getFireEmoji = getFireEmoji;
|
|
14
|
+
exports.getDopamineStats = getDopamineStats;
|
|
15
|
+
exports.displayDopamineStats = displayDopamineStats;
|
|
16
|
+
exports.displayQuickDopamine = displayQuickDopamine;
|
|
17
|
+
const database_1 = require("../db/database");
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
/**
|
|
20
|
+
* Calculate streak (consecutive days with commits)
|
|
21
|
+
*/
|
|
22
|
+
function calculateStreak() {
|
|
23
|
+
const stats = (0, database_1.getStats)(365); // Get last year
|
|
24
|
+
if (stats.recentCommits.length === 0) {
|
|
25
|
+
return { current: 0, longest: 0 };
|
|
26
|
+
}
|
|
27
|
+
// Sort by timestamp (newest first)
|
|
28
|
+
const commits = [...stats.recentCommits].sort((a, b) => b.timestamp - a.timestamp);
|
|
29
|
+
// Group by day
|
|
30
|
+
const dayBuckets = new Map();
|
|
31
|
+
commits.forEach((commit) => {
|
|
32
|
+
const date = new Date(commit.timestamp);
|
|
33
|
+
const dayKey = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
|
34
|
+
dayBuckets.set(dayKey, (dayBuckets.get(dayKey) || 0) + 1);
|
|
35
|
+
});
|
|
36
|
+
const today = new Date();
|
|
37
|
+
const todayKey = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
|
|
38
|
+
// Calculate current streak
|
|
39
|
+
let currentStreak = 0;
|
|
40
|
+
let checkDate = new Date(today);
|
|
41
|
+
while (true) {
|
|
42
|
+
const dayKey = `${checkDate.getFullYear()}-${checkDate.getMonth() + 1}-${checkDate.getDate()}`;
|
|
43
|
+
if (dayBuckets.has(dayKey)) {
|
|
44
|
+
currentStreak++;
|
|
45
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Allow 1-day gap if we're not checking today (give grace period)
|
|
49
|
+
if (dayKey === todayKey) {
|
|
50
|
+
checkDate.setDate(checkDate.getDate() - 1);
|
|
51
|
+
const yesterdayKey = `${checkDate.getFullYear()}-${checkDate.getMonth() + 1}-${checkDate.getDate()}`;
|
|
52
|
+
if (!dayBuckets.has(yesterdayKey)) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Calculate longest streak (simple version)
|
|
62
|
+
let longestStreak = currentStreak;
|
|
63
|
+
let tempStreak = 0;
|
|
64
|
+
let lastDate = null;
|
|
65
|
+
const sortedDays = Array.from(dayBuckets.keys()).sort().reverse();
|
|
66
|
+
sortedDays.forEach(dayKey => {
|
|
67
|
+
const [year, month, day] = dayKey.split('-').map(Number);
|
|
68
|
+
const date = new Date(year, month - 1, day);
|
|
69
|
+
if (lastDate) {
|
|
70
|
+
const diffDays = Math.floor((lastDate.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
71
|
+
if (diffDays === 1) {
|
|
72
|
+
tempStreak++;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
76
|
+
tempStreak = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
tempStreak = 1;
|
|
81
|
+
}
|
|
82
|
+
lastDate = date;
|
|
83
|
+
});
|
|
84
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
85
|
+
return { current: currentStreak, longest: longestStreak };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get motivational message based on stats
|
|
89
|
+
*/
|
|
90
|
+
function getMotivationalMessage(stats) {
|
|
91
|
+
const { currentStreak, weeklyCommits, totalCommits } = stats;
|
|
92
|
+
// Streak-based messages
|
|
93
|
+
if (currentStreak >= 30) {
|
|
94
|
+
return "🔥 LEGENDARY! 30+ day streak!";
|
|
95
|
+
}
|
|
96
|
+
if (currentStreak >= 14) {
|
|
97
|
+
return "💪 Beast mode! 2 weeks straight!";
|
|
98
|
+
}
|
|
99
|
+
if (currentStreak >= 7) {
|
|
100
|
+
return "⚡ On fire! 1 week streak!";
|
|
101
|
+
}
|
|
102
|
+
if (currentStreak >= 3) {
|
|
103
|
+
return "🚀 Momentum building!";
|
|
104
|
+
}
|
|
105
|
+
// Commit-based messages
|
|
106
|
+
if (weeklyCommits >= 50) {
|
|
107
|
+
return "🤯 Insane productivity this week!";
|
|
108
|
+
}
|
|
109
|
+
if (weeklyCommits >= 25) {
|
|
110
|
+
return "💯 Crushing it this week!";
|
|
111
|
+
}
|
|
112
|
+
if (weeklyCommits >= 10) {
|
|
113
|
+
return "👏 Great week of coding!";
|
|
114
|
+
}
|
|
115
|
+
// Milestone messages
|
|
116
|
+
if (totalCommits >= 1000) {
|
|
117
|
+
return "🏆 1000+ commits! You're a legend!";
|
|
118
|
+
}
|
|
119
|
+
if (totalCommits >= 500) {
|
|
120
|
+
return "🎉 500+ commits milestone!";
|
|
121
|
+
}
|
|
122
|
+
if (totalCommits >= 100) {
|
|
123
|
+
return "🌟 100+ commits! Keep going!";
|
|
124
|
+
}
|
|
125
|
+
// Default encouragement
|
|
126
|
+
if (totalCommits === 0) {
|
|
127
|
+
return "🌱 Let's start your coding journey!";
|
|
128
|
+
}
|
|
129
|
+
if (totalCommits < 10) {
|
|
130
|
+
return "🎯 Getting started! Keep it up!";
|
|
131
|
+
}
|
|
132
|
+
return "💻 Keep building amazing things!";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get milestone (if any)
|
|
136
|
+
*/
|
|
137
|
+
function getMilestone(totalCommits) {
|
|
138
|
+
const milestones = [10, 25, 50, 100, 250, 500, 1000];
|
|
139
|
+
for (const milestone of milestones) {
|
|
140
|
+
if (totalCommits === milestone) {
|
|
141
|
+
return `🎉 ${milestone} COMMITS MILESTONE!`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get fire emoji intensity based on streak
|
|
148
|
+
*/
|
|
149
|
+
function getFireEmoji(streak) {
|
|
150
|
+
if (streak >= 30)
|
|
151
|
+
return '🔥🔥🔥';
|
|
152
|
+
if (streak >= 14)
|
|
153
|
+
return '🔥🔥';
|
|
154
|
+
if (streak >= 7)
|
|
155
|
+
return '🔥';
|
|
156
|
+
if (streak >= 3)
|
|
157
|
+
return '⚡';
|
|
158
|
+
return '🌱';
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get complete dopamine stats
|
|
162
|
+
*/
|
|
163
|
+
function getDopamineStats() {
|
|
164
|
+
const stats7d = (0, database_1.getStats)(7);
|
|
165
|
+
const statsAll = (0, database_1.getStats)(365);
|
|
166
|
+
const streak = calculateStreak();
|
|
167
|
+
const dopamineStats = {
|
|
168
|
+
currentStreak: streak.current,
|
|
169
|
+
longestStreak: streak.longest,
|
|
170
|
+
totalCommits: statsAll.totalCommits,
|
|
171
|
+
weeklyCommits: stats7d.totalCommits,
|
|
172
|
+
linesThisWeek: stats7d.totalInsertions + stats7d.totalDeletions,
|
|
173
|
+
milestone: getMilestone(statsAll.totalCommits),
|
|
174
|
+
motivationalMessage: '',
|
|
175
|
+
fireEmoji: getFireEmoji(streak.current),
|
|
176
|
+
};
|
|
177
|
+
dopamineStats.motivationalMessage = getMotivationalMessage(dopamineStats);
|
|
178
|
+
return dopamineStats;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Display dopamine stats in a beautiful way
|
|
182
|
+
*/
|
|
183
|
+
function displayDopamineStats() {
|
|
184
|
+
const stats = getDopamineStats();
|
|
185
|
+
console.log();
|
|
186
|
+
console.log(chalk_1.default.yellow.bold(`${stats.fireEmoji} ${stats.motivationalMessage}`));
|
|
187
|
+
if (stats.currentStreak > 0) {
|
|
188
|
+
console.log(chalk_1.default.gray(` ${stats.currentStreak}-day streak${stats.longestStreak > stats.currentStreak ? ` (best: ${stats.longestStreak})` : ' 🏆'}`));
|
|
189
|
+
}
|
|
190
|
+
console.log(chalk_1.default.gray(` ${stats.weeklyCommits} commits this week`));
|
|
191
|
+
if (stats.linesThisWeek > 0) {
|
|
192
|
+
console.log(chalk_1.default.gray(` ${stats.linesThisWeek.toLocaleString()} lines changed`));
|
|
193
|
+
}
|
|
194
|
+
if (stats.milestone) {
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(chalk_1.default.green.bold(` ${stats.milestone}`));
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Display quick dopamine hit (for after commits)
|
|
202
|
+
*/
|
|
203
|
+
function displayQuickDopamine() {
|
|
204
|
+
const stats = getDopamineStats();
|
|
205
|
+
if (stats.currentStreak >= 3) {
|
|
206
|
+
console.log(chalk_1.default.yellow(`${stats.fireEmoji} ${stats.currentStreak}-day streak!`));
|
|
207
|
+
}
|
|
208
|
+
if (stats.milestone) {
|
|
209
|
+
console.log(chalk_1.default.green.bold(stats.milestone));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Production-ready error handling
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GitError = exports.LicenseError = exports.APIError = exports.SnapCommitError = void 0;
|
|
7
|
+
exports.handleError = handleError;
|
|
8
|
+
class SnapCommitError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
userMessage;
|
|
11
|
+
helpUrl;
|
|
12
|
+
constructor(message, code, userMessage, helpUrl) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.userMessage = userMessage;
|
|
16
|
+
this.helpUrl = helpUrl;
|
|
17
|
+
this.name = 'SnapCommitError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.SnapCommitError = SnapCommitError;
|
|
21
|
+
class APIError extends SnapCommitError {
|
|
22
|
+
provider;
|
|
23
|
+
constructor(message, provider, userMessage) {
|
|
24
|
+
super(message, 'API_ERROR', userMessage || 'AI service temporarily unavailable. Please try again.', 'https://builderos.dev/docs/errors#api-error');
|
|
25
|
+
this.provider = provider;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.APIError = APIError;
|
|
29
|
+
class LicenseError extends SnapCommitError {
|
|
30
|
+
constructor(message, userMessage) {
|
|
31
|
+
super(message, 'LICENSE_ERROR', userMessage || 'License validation failed.', 'https://builderos.dev/docs/errors#license-error');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.LicenseError = LicenseError;
|
|
35
|
+
class GitError extends SnapCommitError {
|
|
36
|
+
constructor(message, userMessage) {
|
|
37
|
+
super(message, 'GIT_ERROR', userMessage || 'Git operation failed.', 'https://builderos.dev/docs/errors#git-error');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.GitError = GitError;
|
|
41
|
+
function handleError(error) {
|
|
42
|
+
if (error instanceof SnapCommitError) {
|
|
43
|
+
console.error(`\n❌ ${error.userMessage}`);
|
|
44
|
+
if (error.helpUrl) {
|
|
45
|
+
console.error(` Help: ${error.helpUrl}`);
|
|
46
|
+
}
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
console.error(`\n❌ Unexpected error: ${error.message}`);
|
|
51
|
+
console.error(' Please report this at: https://github.com/Arjun0606/builderOS/issues');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
console.error('\n❌ An unknown error occurred');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getGitDiff = getGitDiff;
|
|
4
|
+
exports.getGitStatus = getGitStatus;
|
|
5
|
+
exports.isGitRepo = isGitRepo;
|
|
6
|
+
exports.stageAllChanges = stageAllChanges;
|
|
7
|
+
exports.commitWithMessage = commitWithMessage;
|
|
8
|
+
exports.getCommitStats = getCommitStats;
|
|
9
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
function getGitDiff(staged = true) {
|
|
12
|
+
try {
|
|
13
|
+
if (staged) {
|
|
14
|
+
// Get staged changes
|
|
15
|
+
const diff = (0, child_process_1.execSync)('git diff --cached', {
|
|
16
|
+
encoding: 'utf-8',
|
|
17
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large diffs
|
|
18
|
+
});
|
|
19
|
+
return diff;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Get unstaged changes
|
|
23
|
+
const diff = (0, child_process_1.execSync)('git diff', {
|
|
24
|
+
encoding: 'utf-8',
|
|
25
|
+
maxBuffer: 10 * 1024 * 1024
|
|
26
|
+
});
|
|
27
|
+
return diff;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`Git error: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getGitStatus() {
|
|
35
|
+
try {
|
|
36
|
+
const status = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' });
|
|
37
|
+
const lines = status.trim().split('\n').filter(Boolean);
|
|
38
|
+
let staged = 0;
|
|
39
|
+
let unstaged = 0;
|
|
40
|
+
let untracked = 0;
|
|
41
|
+
lines.forEach((line) => {
|
|
42
|
+
const statusCode = line.substring(0, 2);
|
|
43
|
+
if (statusCode[0] !== ' ' && statusCode[0] !== '?')
|
|
44
|
+
staged++;
|
|
45
|
+
if (statusCode[1] !== ' ')
|
|
46
|
+
unstaged++;
|
|
47
|
+
if (statusCode[0] === '?' && statusCode[1] === '?')
|
|
48
|
+
untracked++;
|
|
49
|
+
});
|
|
50
|
+
return { staged, unstaged, untracked };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Git error: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isGitRepo() {
|
|
57
|
+
try {
|
|
58
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function stageAllChanges() {
|
|
66
|
+
try {
|
|
67
|
+
(0, child_process_1.execSync)('git add -A', { stdio: 'inherit' });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new Error(`Git add failed: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function commitWithMessage(message) {
|
|
74
|
+
try {
|
|
75
|
+
(0, child_process_1.execSync)(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
|
76
|
+
// Get the commit hash
|
|
77
|
+
const hash = (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
78
|
+
return hash;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`Git commit failed: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function getCommitStats(hash) {
|
|
85
|
+
try {
|
|
86
|
+
const stats = (0, child_process_1.execSync)(`git show --stat --format="" ${hash}`, { encoding: 'utf-8' });
|
|
87
|
+
const match = stats.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
88
|
+
return {
|
|
89
|
+
files: match?.[1] ? parseInt(match[1]) : 0,
|
|
90
|
+
insertions: match?.[2] ? parseInt(match[2]) : 0,
|
|
91
|
+
deletions: match?.[3] ? parseInt(match[3]) : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return { files: 0, insertions: 0, deletions: 0 };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function getCurrentBranch() {
|
|
99
|
+
try {
|
|
100
|
+
return (0, child_process_1.execSync)('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return 'unknown';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub-style contribution heatmap for terminal
|
|
4
|
+
* Shows coding activity over last 12 weeks
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.generateHeatmap = generateHeatmap;
|
|
11
|
+
exports.getActivitySummary = getActivitySummary;
|
|
12
|
+
exports.displayHeatmap = displayHeatmap;
|
|
13
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
14
|
+
const database_1 = require("../db/database");
|
|
15
|
+
/**
|
|
16
|
+
* Generate terminal heatmap (GitHub-style)
|
|
17
|
+
*/
|
|
18
|
+
function generateHeatmap(weeks = 12) {
|
|
19
|
+
const today = new Date();
|
|
20
|
+
today.setHours(0, 0, 0, 0);
|
|
21
|
+
// Get all commits from the timeframe
|
|
22
|
+
const stats = (0, database_1.getStats)(weeks * 7);
|
|
23
|
+
// Group commits by date
|
|
24
|
+
const commitsByDate = new Map();
|
|
25
|
+
stats.recentCommits.forEach((commit) => {
|
|
26
|
+
const date = new Date(commit.timestamp);
|
|
27
|
+
date.setHours(0, 0, 0, 0);
|
|
28
|
+
const dateKey = date.toISOString().split('T')[0];
|
|
29
|
+
commitsByDate.set(dateKey, (commitsByDate.get(dateKey) || 0) + 1);
|
|
30
|
+
});
|
|
31
|
+
// Generate grid (7 days x N weeks)
|
|
32
|
+
const grid = [];
|
|
33
|
+
// Start from the most recent Sunday
|
|
34
|
+
const startDate = new Date(today);
|
|
35
|
+
const dayOfWeek = startDate.getDay(); // 0 = Sunday
|
|
36
|
+
startDate.setDate(startDate.getDate() - dayOfWeek - (weeks * 7) + 7);
|
|
37
|
+
for (let week = 0; week < weeks; week++) {
|
|
38
|
+
const weekData = [];
|
|
39
|
+
for (let day = 0; day < 7; day++) {
|
|
40
|
+
const date = new Date(startDate);
|
|
41
|
+
date.setDate(date.getDate() + (week * 7) + day);
|
|
42
|
+
const dateKey = date.toISOString().split('T')[0];
|
|
43
|
+
const commits = commitsByDate.get(dateKey) || 0;
|
|
44
|
+
weekData.push({ date, commits });
|
|
45
|
+
}
|
|
46
|
+
grid.push(weekData);
|
|
47
|
+
}
|
|
48
|
+
// Render heatmap
|
|
49
|
+
return renderHeatmap(grid, commitsByDate);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Render the heatmap with colors
|
|
53
|
+
*/
|
|
54
|
+
function renderHeatmap(grid, commitsByDate) {
|
|
55
|
+
const maxCommits = Math.max(...Array.from(commitsByDate.values()), 1);
|
|
56
|
+
let output = '';
|
|
57
|
+
// Header
|
|
58
|
+
output += chalk_1.default.white.bold('\n📈 Contribution Graph (Last 12 Weeks)\n\n');
|
|
59
|
+
// Month labels (top row)
|
|
60
|
+
output += ' ';
|
|
61
|
+
const monthLabels = [];
|
|
62
|
+
let lastMonth = -1;
|
|
63
|
+
grid.forEach((week, weekIndex) => {
|
|
64
|
+
const monthNum = week[0].date.getMonth();
|
|
65
|
+
if (monthNum !== lastMonth && weekIndex % 4 === 0) {
|
|
66
|
+
const monthName = week[0].date.toLocaleString('default', { month: 'short' });
|
|
67
|
+
monthLabels.push(monthName);
|
|
68
|
+
lastMonth = monthNum;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
monthLabels.push('');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Print month labels (every 4 weeks roughly)
|
|
75
|
+
for (let i = 0; i < grid.length; i++) {
|
|
76
|
+
if (i % 4 === 0 && i < grid.length - 3) {
|
|
77
|
+
const month = grid[i][0].date.toLocaleString('default', { month: 'short' });
|
|
78
|
+
output += chalk_1.default.gray(month.padEnd(4));
|
|
79
|
+
i += 3; // Skip next 3 weeks
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
output += ' ';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
output += '\n';
|
|
86
|
+
// Day labels + grid
|
|
87
|
+
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
88
|
+
for (let day = 0; day < 7; day++) {
|
|
89
|
+
// Only show labels for Mon, Wed, Fri
|
|
90
|
+
if (day === 1 || day === 3 || day === 5) {
|
|
91
|
+
output += chalk_1.default.gray(dayLabels[day].substring(0, 3).padEnd(5));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
output += ' ';
|
|
95
|
+
}
|
|
96
|
+
// Render each week for this day
|
|
97
|
+
for (let week = 0; week < grid.length; week++) {
|
|
98
|
+
const dayData = grid[week][day];
|
|
99
|
+
const intensity = getIntensity(dayData.commits, maxCommits);
|
|
100
|
+
const cell = getColoredCell(intensity, dayData.date);
|
|
101
|
+
output += cell + ' ';
|
|
102
|
+
}
|
|
103
|
+
output += '\n';
|
|
104
|
+
}
|
|
105
|
+
// Legend
|
|
106
|
+
output += '\n ';
|
|
107
|
+
output += chalk_1.default.gray('Less ');
|
|
108
|
+
output += getColoredCell(0) + ' ';
|
|
109
|
+
output += getColoredCell(1) + ' ';
|
|
110
|
+
output += getColoredCell(2) + ' ';
|
|
111
|
+
output += getColoredCell(3) + ' ';
|
|
112
|
+
output += getColoredCell(4) + ' ';
|
|
113
|
+
output += chalk_1.default.gray(' More');
|
|
114
|
+
output += '\n';
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get intensity level (0-4) based on commit count
|
|
119
|
+
*/
|
|
120
|
+
function getIntensity(commits, maxCommits) {
|
|
121
|
+
if (commits === 0)
|
|
122
|
+
return 0;
|
|
123
|
+
if (maxCommits === 1)
|
|
124
|
+
return 4; // If max is 1, show as full
|
|
125
|
+
const percentage = commits / maxCommits;
|
|
126
|
+
if (percentage >= 0.75)
|
|
127
|
+
return 4; // Darkest
|
|
128
|
+
if (percentage >= 0.50)
|
|
129
|
+
return 3;
|
|
130
|
+
if (percentage >= 0.25)
|
|
131
|
+
return 2;
|
|
132
|
+
if (percentage > 0)
|
|
133
|
+
return 1; // Lightest
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get colored cell based on intensity
|
|
138
|
+
*/
|
|
139
|
+
function getColoredCell(intensity, date) {
|
|
140
|
+
const today = new Date();
|
|
141
|
+
today.setHours(0, 0, 0, 0);
|
|
142
|
+
const isToday = date && date.getTime() === today.getTime();
|
|
143
|
+
const isFuture = date && date > today;
|
|
144
|
+
// Future dates (gray, empty)
|
|
145
|
+
if (isFuture) {
|
|
146
|
+
return chalk_1.default.gray('·');
|
|
147
|
+
}
|
|
148
|
+
// Today gets a special highlight
|
|
149
|
+
if (isToday) {
|
|
150
|
+
switch (intensity) {
|
|
151
|
+
case 0: return chalk_1.default.yellow('◯'); // No commits today yet
|
|
152
|
+
case 1: return chalk_1.default.cyan('◉');
|
|
153
|
+
case 2: return chalk_1.default.cyan('◉');
|
|
154
|
+
case 3: return chalk_1.default.green('◉');
|
|
155
|
+
case 4: return chalk_1.default.green('◉');
|
|
156
|
+
default: return chalk_1.default.gray('·');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Regular cells (GitHub-style green gradient)
|
|
160
|
+
switch (intensity) {
|
|
161
|
+
case 0: return chalk_1.default.gray('·'); // No activity
|
|
162
|
+
case 1: return chalk_1.default.rgb(14, 68, 41)('■'); // Light green
|
|
163
|
+
case 2: return chalk_1.default.rgb(0, 109, 50)('■'); // Medium green
|
|
164
|
+
case 3: return chalk_1.default.rgb(38, 166, 65)('■'); // Green
|
|
165
|
+
case 4: return chalk_1.default.rgb(57, 211, 83)('■'); // Bright green
|
|
166
|
+
default: return chalk_1.default.gray('·');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get activity summary for heatmap
|
|
171
|
+
*/
|
|
172
|
+
function getActivitySummary(weeks = 12) {
|
|
173
|
+
const stats = (0, database_1.getStats)(weeks * 7);
|
|
174
|
+
const commitsByDate = new Map();
|
|
175
|
+
stats.recentCommits.forEach((commit) => {
|
|
176
|
+
const date = new Date(commit.timestamp);
|
|
177
|
+
date.setHours(0, 0, 0, 0);
|
|
178
|
+
const dateKey = date.toISOString().split('T')[0];
|
|
179
|
+
commitsByDate.set(dateKey, (commitsByDate.get(dateKey) || 0) + 1);
|
|
180
|
+
});
|
|
181
|
+
const activeDays = commitsByDate.size;
|
|
182
|
+
const totalDays = weeks * 7;
|
|
183
|
+
const averagePerDay = totalDays > 0 ? stats.recentCommits.length / totalDays : 0;
|
|
184
|
+
// Find most productive day
|
|
185
|
+
let mostProductiveDay = { date: '', commits: 0 };
|
|
186
|
+
commitsByDate.forEach((commits, date) => {
|
|
187
|
+
if (commits > mostProductiveDay.commits) {
|
|
188
|
+
mostProductiveDay = { date, commits };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// Calculate streaks (simple version)
|
|
192
|
+
let currentStreak = 0;
|
|
193
|
+
let longestStreak = 0;
|
|
194
|
+
let tempStreak = 0;
|
|
195
|
+
const sortedDates = Array.from(commitsByDate.keys()).sort().reverse();
|
|
196
|
+
let lastDate = null;
|
|
197
|
+
sortedDates.forEach(dateStr => {
|
|
198
|
+
const date = new Date(dateStr);
|
|
199
|
+
if (lastDate) {
|
|
200
|
+
const diffDays = Math.floor((lastDate.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
201
|
+
if (diffDays === 1) {
|
|
202
|
+
tempStreak++;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
206
|
+
tempStreak = 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Check if most recent date is today or yesterday
|
|
211
|
+
const today = new Date();
|
|
212
|
+
today.setHours(0, 0, 0, 0);
|
|
213
|
+
const diffFromToday = Math.floor((today.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
|
|
214
|
+
if (diffFromToday <= 1) {
|
|
215
|
+
currentStreak = 1;
|
|
216
|
+
tempStreak = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
lastDate = date;
|
|
220
|
+
});
|
|
221
|
+
longestStreak = Math.max(longestStreak, tempStreak);
|
|
222
|
+
if (currentStreak > 0) {
|
|
223
|
+
currentStreak = tempStreak;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
totalCommits: stats.recentCommits.length,
|
|
227
|
+
activeDays,
|
|
228
|
+
longestStreak,
|
|
229
|
+
currentStreak,
|
|
230
|
+
averagePerDay: Math.round(averagePerDay * 10) / 10,
|
|
231
|
+
mostProductiveDay,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Display heatmap with summary stats
|
|
236
|
+
*/
|
|
237
|
+
function displayHeatmap(weeks = 12) {
|
|
238
|
+
const heatmap = generateHeatmap(weeks);
|
|
239
|
+
const summary = getActivitySummary(weeks);
|
|
240
|
+
console.log(heatmap);
|
|
241
|
+
// Summary stats below heatmap
|
|
242
|
+
console.log(chalk_1.default.white.bold('\n📊 Activity Summary:\n'));
|
|
243
|
+
const stats = [
|
|
244
|
+
{ label: 'Total commits', value: summary.totalCommits, emoji: '💻' },
|
|
245
|
+
{ label: 'Active days', value: `${summary.activeDays}/${weeks * 7}`, emoji: '📅' },
|
|
246
|
+
{ label: 'Current streak', value: `${summary.currentStreak} days`, emoji: '🔥' },
|
|
247
|
+
{ label: 'Longest streak', value: `${summary.longestStreak} days`, emoji: '🏆' },
|
|
248
|
+
{ label: 'Avg per day', value: summary.averagePerDay.toFixed(1), emoji: '📈' },
|
|
249
|
+
];
|
|
250
|
+
stats.forEach(stat => {
|
|
251
|
+
console.log(chalk_1.default.gray(` ${stat.emoji} ${stat.label}:`.padEnd(25)) +
|
|
252
|
+
chalk_1.default.cyan(stat.value.toString()));
|
|
253
|
+
});
|
|
254
|
+
if (summary.mostProductiveDay.commits > 0) {
|
|
255
|
+
const date = new Date(summary.mostProductiveDay.date);
|
|
256
|
+
const formattedDate = date.toLocaleDateString('en-US', {
|
|
257
|
+
month: 'short',
|
|
258
|
+
day: 'numeric',
|
|
259
|
+
year: 'numeric'
|
|
260
|
+
});
|
|
261
|
+
console.log(chalk_1.default.gray(` 🌟 Most productive:`.padEnd(25)) +
|
|
262
|
+
chalk_1.default.cyan(`${summary.mostProductiveDay.commits} commits on ${formattedDate}`));
|
|
263
|
+
}
|
|
264
|
+
console.log();
|
|
265
|
+
}
|