repo-wrapped 0.0.3 โ†’ 0.0.5

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.
Files changed (38) hide show
  1. package/dist/commands/generate.js +104 -95
  2. package/dist/constants/chronotypes.js +23 -23
  3. package/dist/constants/colors.js +18 -18
  4. package/dist/constants/index.js +18 -18
  5. package/dist/formatters/index.js +17 -17
  6. package/dist/formatters/timeFormatter.js +28 -29
  7. package/dist/generators/html/templates/achievementsSection.js +42 -43
  8. package/dist/generators/html/templates/commitQualitySection.js +25 -26
  9. package/dist/generators/html/templates/contributionGraph.js +64 -48
  10. package/dist/generators/html/templates/impactSection.js +19 -20
  11. package/dist/generators/html/templates/knowledgeSection.js +86 -87
  12. package/dist/generators/html/templates/streakSection.js +8 -9
  13. package/dist/generators/html/templates/timePatternsSection.js +45 -46
  14. package/dist/generators/html/utils/colorUtils.js +61 -21
  15. package/dist/generators/html/utils/commitMapBuilder.js +23 -24
  16. package/dist/generators/html/utils/dateRangeCalculator.js +56 -57
  17. package/dist/generators/html/utils/developerStatsCalculator.js +28 -29
  18. package/dist/generators/html/utils/scriptLoader.js +15 -16
  19. package/dist/generators/html/utils/styleLoader.js +17 -18
  20. package/dist/generators/html/utils/weekGrouper.js +27 -28
  21. package/dist/index.js +78 -78
  22. package/dist/types/index.js +2 -2
  23. package/dist/utils/achievementDefinitions.js +433 -433
  24. package/dist/utils/achievementEngine.js +169 -170
  25. package/dist/utils/commitQualityAnalyzer.js +367 -368
  26. package/dist/utils/fileHotspotAnalyzer.js +269 -270
  27. package/dist/utils/gitParser.js +136 -126
  28. package/dist/utils/htmlGenerator.js +245 -233
  29. package/dist/utils/impactAnalyzer.js +247 -248
  30. package/dist/utils/knowledgeDistributionAnalyzer.js +380 -374
  31. package/dist/utils/matrixGenerator.js +369 -350
  32. package/dist/utils/slideGenerator.js +170 -171
  33. package/dist/utils/streakCalculator.js +134 -135
  34. package/dist/utils/timePatternAnalyzer.js +304 -305
  35. package/dist/utils/wrappedDisplay.js +124 -115
  36. package/dist/utils/wrappedGenerator.js +376 -377
  37. package/dist/utils/wrappedHtmlGenerator.js +105 -106
  38. package/package.json +10 -10
@@ -1,350 +1,369 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateCommitMatrix = void 0;
7
- const chalk_1 = __importDefault(require("chalk"));
8
- const date_fns_1 = require("date-fns");
9
- const constants_1 = require("../constants");
10
- const achievementEngine_1 = require("./achievementEngine");
11
- const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
12
- const fileHotspotAnalyzer_1 = require("./fileHotspotAnalyzer");
13
- const streakCalculator_1 = require("./streakCalculator");
14
- const timePatternAnalyzer_1 = require("./timePatternAnalyzer");
15
- function generateCommitMatrix(commits, year, monthsToShow, repoPath, repoName, repoUrl) {
16
- // Get today or end of year if it's a past year
17
- const now = new Date();
18
- const isCurrentYear = year === now.getFullYear();
19
- // Calculate the end date (today if current year, or end of December for past years)
20
- const endDate = isCurrentYear
21
- ? now
22
- : new Date(year, 11, 31);
23
- // Calculate start date (N months back from end date)
24
- const startDate = (0, date_fns_1.startOfMonth)((0, date_fns_1.subMonths)(endDate, monthsToShow - 1));
25
- // Get the full week range (start on Sunday, end on Saturday)
26
- const weekStartDate = (0, date_fns_1.startOfWeek)(startDate, { weekStartsOn: 0 });
27
- const weekEndDate = (0, date_fns_1.endOfWeek)(endDate, { weekStartsOn: 0 });
28
- // Create a map of date string -> commit count
29
- const commitMap = new Map();
30
- commits.forEach(commit => {
31
- const commitDate = new Date(commit.date);
32
- if (commitDate >= startDate && commitDate <= endDate) {
33
- const dateKey = (0, date_fns_1.format)(commitDate, 'yyyy-MM-dd');
34
- commitMap.set(dateKey, (commitMap.get(dateKey) || 0) + 1);
35
- }
36
- });
37
- const totalCommits = Array.from(commitMap.values()).reduce((sum, count) => sum + count, 0);
38
- // Calculate streaks
39
- const streakData = (0, streakCalculator_1.calculateStreaks)(commits, startDate, endDate);
40
- // Analyze time patterns
41
- const timePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(commits, startDate, endDate);
42
- // Analyze commit quality
43
- const commitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(commits);
44
- // Analyze file hotspots
45
- const fileHotspots = (0, fileHotspotAnalyzer_1.analyzeFileHotspots)(repoPath, startDate, endDate);
46
- // Prepare analysis data for achievements
47
- const analysisData = {
48
- commits,
49
- totalCommits,
50
- streakData,
51
- timePattern,
52
- commitQuality,
53
- fileHotspots,
54
- dateRange: { start: startDate, end: endDate }
55
- };
56
- // Check achievements
57
- const achievementProgress = (0, achievementEngine_1.checkAchievements)(analysisData);
58
- return formatGitHubStyle(commitMap, weekStartDate, weekEndDate, startDate, endDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl);
59
- }
60
- exports.generateCommitMatrix = generateCommitMatrix;
61
- function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate, dataEndDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl) {
62
- const days = (0, date_fns_1.eachDayOfInterval)({ start: weekStartDate, end: weekEndDate });
63
- const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
64
- // Group days into weeks
65
- const weeks = [];
66
- let currentWeek = [];
67
- days.forEach(day => {
68
- currentWeek.push(day);
69
- if (currentWeek.length === 7) {
70
- weeks.push(currentWeek);
71
- currentWeek = [];
72
- }
73
- });
74
- // Build the output with header
75
- let output = '\n';
76
- // Add repository info if available
77
- if (repoName) {
78
- output += chalk_1.default.bold.cyan(` ${repoName}\n`);
79
- if (repoUrl) {
80
- output += chalk_1.default.dim(` ${repoUrl}\n`);
81
- }
82
- output += '\n';
83
- }
84
- // Add stats
85
- output += chalk_1.default.dim(' ') + chalk_1.default.white(`${totalCommits} contributions in the last ${(0, date_fns_1.format)(dataStartDate, 'MMM yyyy')} - ${(0, date_fns_1.format)(dataEndDate, 'MMM yyyy')}\n\n`);
86
- // Build month headers
87
- let monthHeader = ' ';
88
- let currentMonth = '';
89
- weeks.forEach((week, weekIndex) => {
90
- const firstDayOfWeek = week[0];
91
- const monthName = (0, date_fns_1.format)(firstDayOfWeek, 'MMM');
92
- if (monthName !== currentMonth) {
93
- monthHeader += chalk_1.default.bold(monthName.padEnd(4));
94
- currentMonth = monthName;
95
- }
96
- else {
97
- monthHeader += ' ';
98
- }
99
- });
100
- output += monthHeader + '\n';
101
- // Day rows with better formatting
102
- for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
103
- // Only show Mon, Wed, Fri labels
104
- const label = [1, 3, 5].includes(dayOfWeek)
105
- ? chalk_1.default.dim(dayLabels[dayOfWeek].padEnd(5))
106
- : ' ';
107
- let row = label + ' ';
108
- weeks.forEach(week => {
109
- const day = week[dayOfWeek];
110
- if (day) {
111
- const dateKey = (0, date_fns_1.format)(day, 'yyyy-MM-dd');
112
- const count = commitMap.get(dateKey) || 0;
113
- const isInRange = day >= dataStartDate && day <= dataEndDate;
114
- if (isInRange) {
115
- row += formatCell(count) + ' ';
116
- }
117
- else {
118
- row += chalk_1.default.dim('โ– ') + ' ';
119
- }
120
- }
121
- else {
122
- row += ' ';
123
- }
124
- });
125
- output += row + '\n';
126
- }
127
- // Add legend with better spacing
128
- output += '\n';
129
- output += chalk_1.default.dim(' Learn how we count contributions\n');
130
- output += chalk_1.default.dim(' Less ') +
131
- formatCell(0) + ' ' +
132
- formatCell(1) + ' ' +
133
- formatCell(4) + ' ' +
134
- formatCell(7) + ' ' +
135
- formatCell(10) + ' ' +
136
- chalk_1.default.dim('More\n');
137
- // Streak & Momentum Section
138
- output += '\n';
139
- output += chalk_1.default.bold.cyan('๐Ÿ“Š Streak & Momentum\n');
140
- output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
141
- const currentStreakEmoji = streakData.currentStreak.isActive ? '๐Ÿ”ฅ' : '๐Ÿ’ค';
142
- const currentStreakText = streakData.currentStreak.days > 0
143
- ? `${streakData.currentStreak.days} day${streakData.currentStreak.days > 1 ? 's' : ''}`
144
- : 'No active streak';
145
- output += `${currentStreakEmoji} Current Streak: ${chalk_1.default.bold.green(currentStreakText)}`;
146
- if (streakData.currentStreak.isActive && streakData.currentStreak.days > 0) {
147
- output += chalk_1.default.dim(` - ${(0, streakCalculator_1.getStreakMotivation)(streakData.currentStreak.days, true)}`);
148
- }
149
- output += '\n';
150
- const longestStreakText = streakData.longestStreak.days > 0
151
- ? `${streakData.longestStreak.days} day${streakData.longestStreak.days > 1 ? 's' : ''} (${(0, date_fns_1.format)(streakData.longestStreak.startDate, 'MMM d')} - ${(0, date_fns_1.format)(streakData.longestStreak.endDate, 'MMM d, yyyy')})`
152
- : 'No streaks yet';
153
- output += `๐Ÿ† Longest Streak: ${chalk_1.default.bold.yellow(longestStreakText)}\n`;
154
- const activePercentage = streakData.activeDayPercentage.toFixed(1);
155
- output += `๐Ÿ“ˆ Active Days: ${chalk_1.default.bold.cyan(streakData.totalActiveDays)}/${streakData.totalDays} (${activePercentage}%)\n`;
156
- // Time & Productivity Patterns Section
157
- output += '\n';
158
- output += chalk_1.default.bold.cyan('โฐ Productivity Patterns\n');
159
- output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
160
- // Peak hour
161
- const peakHourLabel = timePattern.peakHour.hour === 0 ? '12:00 AM' :
162
- timePattern.peakHour.hour < 12 ? `${timePattern.peakHour.hour}:00 AM` :
163
- timePattern.peakHour.hour === 12 ? '12:00 PM' :
164
- `${timePattern.peakHour.hour - 12}:00 PM`;
165
- output += `โฑ๏ธ Peak Hour: ${chalk_1.default.bold.green(peakHourLabel)} (${timePattern.peakHour.commitCount} commits)\n`;
166
- // Chronotype
167
- const chronotypeLabel = (0, timePatternAnalyzer_1.getChronotypeLabel)(timePattern.chronotype);
168
- const chronotypeDesc = (0, timePatternAnalyzer_1.getChronotypeDescription)(timePattern.chronotype);
169
- output += `${chronotypeLabel} ${chalk_1.default.dim(`(${timePattern.chronotypeConfidence.toFixed(0)}% confidence)`)}\n`;
170
- if (chronotypeDesc) {
171
- output += chalk_1.default.dim(` ${chronotypeDesc}\n`);
172
- }
173
- // Work-life balance
174
- const stars = 'โญ'.repeat(timePattern.workLifeBalance.score);
175
- output += `โš–๏ธ Work-Life Balance: ${stars} ${chalk_1.default.dim(`(${timePattern.workLifeBalance.score}/5)`)}\n`;
176
- output += chalk_1.default.dim(` Weekday: ${timePattern.workLifeBalance.weekdayCommits} | Weekend: ${timePattern.workLifeBalance.weekendCommits}\n`);
177
- // Consistency
178
- output += `๐Ÿ“Š Consistency: ${chalk_1.default.bold.yellow(timePattern.consistency.score.toFixed(1))}/10 ${chalk_1.default.dim(`(${timePattern.consistency.regularity})`)}\n`;
179
- // Burnout warnings
180
- if (timePattern.burnoutRisk.level !== 'low') {
181
- output += `\n${chalk_1.default.yellow('โš ๏ธ Burnout Risk: ' + timePattern.burnoutRisk.level.toUpperCase())}\n`;
182
- timePattern.burnoutRisk.indicators.forEach((indicator) => {
183
- output += chalk_1.default.dim(` โ€ข ${indicator}\n`);
184
- });
185
- }
186
- // Commit Quality Section
187
- output += '\n';
188
- output += chalk_1.default.bold.cyan('๐Ÿ“ Commit Quality\n');
189
- output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
190
- const rating = (0, commitQualityAnalyzer_1.getQualityRating)(commitQuality.overallScore);
191
- const level = (0, commitQualityAnalyzer_1.getQualityLevel)(commitQuality.overallScore);
192
- output += `๐Ÿ“Š Overall Score: ${chalk_1.default.bold.green(commitQuality.overallScore.toFixed(1))}/10 ${rating} ${chalk_1.default.dim(`(${level})`)}\n`;
193
- output += `โœ… Convention: ${chalk_1.default.bold.cyan(commitQuality.conventionalCommits.adherence.toFixed(1))}%`;
194
- if (commitQuality.conventionalCommits.adherence < 70) {
195
- output += chalk_1.default.yellow(' โš ๏ธ');
196
- }
197
- else {
198
- output += chalk_1.default.green(' โœจ');
199
- }
200
- output += '\n';
201
- // Subject quality
202
- output += `๐Ÿ“ Subject Quality: ${chalk_1.default.bold.yellow(commitQuality.subjectQuality.score.toFixed(1))}/10 `;
203
- output += chalk_1.default.dim(`(avg ${commitQuality.subjectQuality.avgLength} chars)\n`);
204
- // Body quality
205
- output += `๐Ÿ“„ Body Quality: ${chalk_1.default.bold.yellow(commitQuality.bodyQuality.score.toFixed(1))}/10 `;
206
- output += chalk_1.default.dim(`(${commitQuality.bodyQuality.withBody}/${commitQuality.totalCommits} with body)\n`);
207
- // Top commit types
208
- if (Object.keys(commitQuality.conventionalCommits.types).length > 0) {
209
- output += `\n${chalk_1.default.dim('Top Commit Types:')}\n`;
210
- const sortedTypes = Object.entries(commitQuality.conventionalCommits.types)
211
- .sort(([, a], [, b]) => b - a)
212
- .slice(0, 3);
213
- sortedTypes.forEach(([type, count]) => {
214
- output += chalk_1.default.dim(` ${type}: ${count}\n`);
215
- });
216
- }
217
- // Improvements
218
- if (commitQuality.improvements && commitQuality.improvements.length > 0) {
219
- output += `\n${chalk_1.default.yellow('๐Ÿ’ก Tips for Improvement:')}\n`;
220
- commitQuality.improvements.slice(0, 2).forEach((tip) => {
221
- output += chalk_1.default.dim(` โ€ข ${tip}\n`);
222
- });
223
- }
224
- // Code hygiene metrics (if any)
225
- const totalHygiene = commitQuality.codeHygiene.quickFixes +
226
- commitQuality.codeHygiene.workInProgress +
227
- commitQuality.codeHygiene.oops +
228
- commitQuality.codeHygiene.vague;
229
- if (totalHygiene > 0) {
230
- output += `\n${chalk_1.default.dim('๐Ÿงน Code Hygiene Indicators:\n')}`;
231
- if (commitQuality.codeHygiene.quickFixes > 0) {
232
- output += chalk_1.default.dim(` Quick fixes (typos/spelling): ${commitQuality.codeHygiene.quickFixes}\n`);
233
- }
234
- if (commitQuality.codeHygiene.workInProgress > 0) {
235
- output += chalk_1.default.dim(` Work in progress: ${commitQuality.codeHygiene.workInProgress}\n`);
236
- }
237
- if (commitQuality.codeHygiene.oops > 0) {
238
- output += chalk_1.default.dim(` Correction commits: ${commitQuality.codeHygiene.oops}\n`);
239
- }
240
- if (commitQuality.codeHygiene.vague > 0) {
241
- output += chalk_1.default.dim(` Vague messages: ${commitQuality.codeHygiene.vague}\n`);
242
- }
243
- }
244
- // File & Code Hotspots Section
245
- output += '\n' + chalk_1.default.bold.hex('#58a6ff')('๐Ÿ”ฅ TOP FILE HOTSPOTS') + '\n';
246
- if (!fileHotspots || fileHotspots.topFiles.length === 0) {
247
- output += chalk_1.default.dim(' No file changes detected in this period.\n');
248
- }
249
- else {
250
- const topFilesToShow = fileHotspots.topFiles.slice(0, 7);
251
- const maxChanges = Math.max(...topFilesToShow.map((f) => f.changeCount));
252
- topFilesToShow.forEach((file) => {
253
- const barLength = Math.max(1, Math.round((file.changeCount / maxChanges) * 30));
254
- const bar = chalk_1.default.hex('#f85149')('โ–ˆ'.repeat(barLength));
255
- const fileName = file.path.length > 40 ? '...' + file.path.slice(-37) : file.path;
256
- output += chalk_1.default.dim(` ${fileName}\n`);
257
- output += ` ${bar} ${chalk_1.default.bold(file.changeCount)} changes, ${file.authors.length} author${file.authors.length !== 1 ? 's' : ''}\n`;
258
- });
259
- }
260
- // Technical Debt Warnings
261
- if (fileHotspots && fileHotspots.technicalDebt.length > 0) {
262
- output += '\n' + chalk_1.default.bold.hex('#f85149')('โš ๏ธ TECHNICAL DEBT WARNINGS') + '\n';
263
- const topDebt = fileHotspots.technicalDebt.slice(0, 5);
264
- topDebt.forEach((debt) => {
265
- const color = (0, fileHotspotAnalyzer_1.getRiskLevelColor)(debt.riskLevel);
266
- const colorFn = color === 'red' ? chalk_1.default.red : color === 'yellow' ? chalk_1.default.yellow : chalk_1.default.gray;
267
- const fileName = debt.file.length > 45 ? '...' + debt.file.slice(-42) : debt.file;
268
- output += colorFn(` ${fileName}\n`);
269
- output += chalk_1.default.dim(` Risk: ${debt.riskLevel.toUpperCase()} (complexity: ${debt.score}/10)\n`);
270
- if (debt.recommendations.length > 0) {
271
- output += chalk_1.default.dim(` โ†’ ${debt.recommendations[0]}\n`);
272
- }
273
- });
274
- }
275
- // Ownership Risks
276
- if (fileHotspots && fileHotspots.ownershipRisks) {
277
- const highRiskOwnership = fileHotspots.ownershipRisks.filter((risk) => risk.busFactorRisk >= 7);
278
- if (highRiskOwnership.length > 0) {
279
- output += '\n' + chalk_1.default.bold.hex('#d29922')('๐ŸšŒ BUS FACTOR WARNINGS') + '\n';
280
- const topRisks = highRiskOwnership.slice(0, 5);
281
- topRisks.forEach((risk) => {
282
- const icon = (0, fileHotspotAnalyzer_1.getOwnershipIcon)(risk.ownershipType);
283
- const fileName = risk.file.length > 45 ? '...' + risk.file.slice(-42) : risk.file;
284
- output += chalk_1.default.yellow(` ${icon} ${fileName}\n`);
285
- const topOwner = risk.owners[0];
286
- output += chalk_1.default.dim(` ${topOwner.author} owns ${topOwner.percentage.toFixed(0)}% (${topOwner.commits} commits)\n`);
287
- });
288
- }
289
- }
290
- // Achievements Section
291
- if (achievementProgress) {
292
- output += '\n';
293
- output += chalk_1.default.bold.hex('#ffd700')('๐Ÿ† ACHIEVEMENTS') + '\n';
294
- output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
295
- // Show newly earned badges
296
- if (achievementProgress.recentlyEarned && achievementProgress.recentlyEarned.length > 0) {
297
- output += '\n' + chalk_1.default.bold.green('โœจ NEW BADGES EARNED! ๐ŸŽ‰') + '\n';
298
- achievementProgress.recentlyEarned.forEach((achievement) => {
299
- output += chalk_1.default.green(` ${achievement.emoji} ${achievement.name} - ${achievement.description}\n`);
300
- });
301
- }
302
- // Show progress
303
- output += `\n${chalk_1.default.cyan('Progress:')} ${chalk_1.default.bold.white(achievementProgress.unlockedCount)}/${achievementProgress.totalCount} badges unlocked `;
304
- output += chalk_1.default.dim(`(${achievementProgress.completionPercentage.toFixed(1)}%)\n`);
305
- // Progress bar
306
- const barLength = 30;
307
- const filledLength = Math.round((achievementProgress.completionPercentage / 100) * barLength);
308
- const bar = 'โ–ˆ'.repeat(filledLength) + 'โ–‘'.repeat(barLength - filledLength);
309
- output += ` ${chalk_1.default.hex('#ffd700')(bar)}\n`;
310
- // Show next milestone
311
- if (achievementProgress.nextMilestones && achievementProgress.nextMilestones.length > 0) {
312
- const next = achievementProgress.nextMilestones[0];
313
- output += `\n${chalk_1.default.yellow('๐ŸŽฏ Next Milestone:')}\n`;
314
- output += ` ${next.emoji} ${chalk_1.default.bold(next.name)} ${chalk_1.default.dim(`(${next.progress.toFixed(0)}%)`)}\n`;
315
- output += chalk_1.default.dim(` ${next.description}\n`);
316
- // Progress bar for next milestone
317
- const nextBarLength = 25;
318
- const nextFilledLength = Math.round((next.progress / 100) * nextBarLength);
319
- const nextBar = 'โ–ˆ'.repeat(nextFilledLength) + 'โ–‘'.repeat(nextBarLength - nextFilledLength);
320
- output += ` ${chalk_1.default.hex('#58a6ff')(nextBar)} ${chalk_1.default.dim((100 - next.progress).toFixed(0) + '% to go!')}\n`;
321
- }
322
- // Show recent unlocks (excluding today's new badges)
323
- const recentUnlocks = achievementProgress.achievements
324
- .filter((a) => a.isUnlocked &&
325
- !achievementProgress.recentlyEarned.some((r) => r.id === a.id) &&
326
- a.earnedDate)
327
- .sort((a, b) => b.earnedDate.getTime() - a.earnedDate.getTime())
328
- .slice(0, 3);
329
- if (recentUnlocks.length > 0) {
330
- output += `\n${chalk_1.default.dim('Recent Unlocks:')}\n`;
331
- recentUnlocks.forEach((achievement) => {
332
- const daysAgo = Math.floor((Date.now() - achievement.earnedDate.getTime()) / (1000 * 60 * 60 * 24));
333
- const timeStr = daysAgo === 0 ? 'today' : daysAgo === 1 ? '1 day ago' : `${daysAgo} days ago`;
334
- output += chalk_1.default.dim(` ${achievement.emoji} ${achievement.name} (${timeStr})\n`);
335
- });
336
- }
337
- }
338
- return output;
339
- }
340
- function formatCell(count) {
341
- if (count === 0)
342
- return chalk_1.default.hex('#ebedf0')('โ– ');
343
- if (count <= constants_1.CONTRIBUTION_LEVELS.LEVEL_1)
344
- return chalk_1.default.hex('#9be9a8')('โ– ');
345
- if (count <= constants_1.CONTRIBUTION_LEVELS.LEVEL_2)
346
- return chalk_1.default.hex('#40c463')('โ– ');
347
- if (count <= constants_1.CONTRIBUTION_LEVELS.LEVEL_3)
348
- return chalk_1.default.hex('#30a14e')('โ– ');
349
- return chalk_1.default.hex('#216e39')('โ– ');
350
- }
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateCommitMatrix = generateCommitMatrix;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const date_fns_1 = require("date-fns");
9
+ const achievementEngine_1 = require("./achievementEngine");
10
+ const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
11
+ const fileHotspotAnalyzer_1 = require("./fileHotspotAnalyzer");
12
+ const streakCalculator_1 = require("./streakCalculator");
13
+ const timePatternAnalyzer_1 = require("./timePatternAnalyzer");
14
+ /**
15
+ * Calculate dynamic thresholds based on commit distribution for terminal output
16
+ */
17
+ function calculateTerminalThresholds(commitMap) {
18
+ const counts = Array.from(commitMap.values()).filter(c => c > 0);
19
+ if (counts.length === 0) {
20
+ return { l1: 1, l2: 2, l3: 3 };
21
+ }
22
+ counts.sort((a, b) => a - b);
23
+ const percentile = (p) => {
24
+ const index = Math.ceil((p / 100) * counts.length) - 1;
25
+ return counts[Math.max(0, index)];
26
+ };
27
+ const l1 = Math.max(1, percentile(25));
28
+ const l2 = Math.max(l1 + 1, percentile(50));
29
+ const l3 = Math.max(l2 + 1, percentile(75));
30
+ return { l1, l2, l3 };
31
+ }
32
+ function generateCommitMatrix(commits, year, monthsToShow, repoPath, repoName, repoUrl) {
33
+ // Get today or end of year if it's a past year
34
+ const now = new Date();
35
+ const isCurrentYear = year === now.getFullYear();
36
+ // Calculate the end date (today if current year, or end of December for past years)
37
+ const endDate = isCurrentYear
38
+ ? now
39
+ : new Date(year, 11, 31);
40
+ // Calculate start date (N months back from end date)
41
+ const startDate = (0, date_fns_1.startOfMonth)((0, date_fns_1.subMonths)(endDate, monthsToShow - 1));
42
+ // Get the full week range (start on Sunday, end on Saturday)
43
+ const weekStartDate = (0, date_fns_1.startOfWeek)(startDate, { weekStartsOn: 0 });
44
+ const weekEndDate = (0, date_fns_1.endOfWeek)(endDate, { weekStartsOn: 0 });
45
+ // Create a map of date string -> commit count
46
+ const commitMap = new Map();
47
+ commits.forEach(commit => {
48
+ const commitDate = new Date(commit.date);
49
+ if (commitDate >= startDate && commitDate <= endDate) {
50
+ const dateKey = (0, date_fns_1.format)(commitDate, 'yyyy-MM-dd');
51
+ commitMap.set(dateKey, (commitMap.get(dateKey) || 0) + 1);
52
+ }
53
+ });
54
+ const totalCommits = Array.from(commitMap.values()).reduce((sum, count) => sum + count, 0);
55
+ // Calculate streaks
56
+ const streakData = (0, streakCalculator_1.calculateStreaks)(commits, startDate, endDate);
57
+ // Analyze time patterns
58
+ const timePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(commits, startDate, endDate);
59
+ // Analyze commit quality
60
+ const commitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(commits);
61
+ // Analyze file hotspots
62
+ const fileHotspots = (0, fileHotspotAnalyzer_1.analyzeFileHotspots)(repoPath, startDate, endDate);
63
+ // Prepare analysis data for achievements
64
+ const analysisData = {
65
+ commits,
66
+ totalCommits,
67
+ streakData,
68
+ timePattern,
69
+ commitQuality,
70
+ fileHotspots,
71
+ dateRange: { start: startDate, end: endDate }
72
+ };
73
+ // Check achievements
74
+ const achievementProgress = (0, achievementEngine_1.checkAchievements)(analysisData);
75
+ return formatGitHubStyle(commitMap, weekStartDate, weekEndDate, startDate, endDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl);
76
+ }
77
+ function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate, dataEndDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl) {
78
+ const days = (0, date_fns_1.eachDayOfInterval)({ start: weekStartDate, end: weekEndDate });
79
+ const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
80
+ // Calculate dynamic thresholds based on this repo's commit distribution
81
+ const thresholds = calculateTerminalThresholds(commitMap);
82
+ // Create a formatCell function with dynamic thresholds
83
+ const formatCell = (count) => {
84
+ if (count === 0)
85
+ return chalk_1.default.hex('#ebedf0')('โ– ');
86
+ if (count <= thresholds.l1)
87
+ return chalk_1.default.hex('#9be9a8')('โ– ');
88
+ if (count <= thresholds.l2)
89
+ return chalk_1.default.hex('#40c463')('โ– ');
90
+ if (count <= thresholds.l3)
91
+ return chalk_1.default.hex('#30a14e')('โ– ');
92
+ return chalk_1.default.hex('#216e39')('โ– ');
93
+ };
94
+ // Group days into weeks
95
+ const weeks = [];
96
+ let currentWeek = [];
97
+ days.forEach(day => {
98
+ currentWeek.push(day);
99
+ if (currentWeek.length === 7) {
100
+ weeks.push(currentWeek);
101
+ currentWeek = [];
102
+ }
103
+ });
104
+ // Build the output with header
105
+ let output = '\n';
106
+ // Add repository info if available
107
+ if (repoName) {
108
+ output += chalk_1.default.bold.cyan(` ${repoName}\n`);
109
+ if (repoUrl) {
110
+ output += chalk_1.default.dim(` ${repoUrl}\n`);
111
+ }
112
+ output += '\n';
113
+ }
114
+ // Add stats
115
+ output += chalk_1.default.dim(' ') + chalk_1.default.white(`${totalCommits} contributions in the last ${(0, date_fns_1.format)(dataStartDate, 'MMM yyyy')} - ${(0, date_fns_1.format)(dataEndDate, 'MMM yyyy')}\n\n`);
116
+ // Build month headers
117
+ let monthHeader = ' ';
118
+ let currentMonth = '';
119
+ weeks.forEach((week, weekIndex) => {
120
+ const firstDayOfWeek = week[0];
121
+ const monthName = (0, date_fns_1.format)(firstDayOfWeek, 'MMM');
122
+ if (monthName !== currentMonth) {
123
+ monthHeader += chalk_1.default.bold(monthName.padEnd(4));
124
+ currentMonth = monthName;
125
+ }
126
+ else {
127
+ monthHeader += ' ';
128
+ }
129
+ });
130
+ output += monthHeader + '\n';
131
+ // Day rows with better formatting
132
+ for (let dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
133
+ // Only show Mon, Wed, Fri labels
134
+ const label = [1, 3, 5].includes(dayOfWeek)
135
+ ? chalk_1.default.dim(dayLabels[dayOfWeek].padEnd(5))
136
+ : ' ';
137
+ let row = label + ' ';
138
+ weeks.forEach(week => {
139
+ const day = week[dayOfWeek];
140
+ if (day) {
141
+ const dateKey = (0, date_fns_1.format)(day, 'yyyy-MM-dd');
142
+ const count = commitMap.get(dateKey) || 0;
143
+ const isInRange = day >= dataStartDate && day <= dataEndDate;
144
+ if (isInRange) {
145
+ row += formatCell(count) + ' ';
146
+ }
147
+ else {
148
+ row += chalk_1.default.dim('โ– ') + ' ';
149
+ }
150
+ }
151
+ else {
152
+ row += ' ';
153
+ }
154
+ });
155
+ output += row + '\n';
156
+ }
157
+ // Add legend with better spacing
158
+ output += '\n';
159
+ output += chalk_1.default.dim(' Learn how we count contributions\n');
160
+ output += chalk_1.default.dim(' Less ') +
161
+ formatCell(0) + ' ' +
162
+ formatCell(1) + ' ' +
163
+ formatCell(4) + ' ' +
164
+ formatCell(7) + ' ' +
165
+ formatCell(10) + ' ' +
166
+ chalk_1.default.dim('More\n');
167
+ // Streak & Momentum Section
168
+ output += '\n';
169
+ output += chalk_1.default.bold.cyan('๐Ÿ“Š Streak & Momentum\n');
170
+ output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
171
+ const currentStreakEmoji = streakData.currentStreak.isActive ? '๐Ÿ”ฅ' : '๐Ÿ’ค';
172
+ const currentStreakText = streakData.currentStreak.days > 0
173
+ ? `${streakData.currentStreak.days} day${streakData.currentStreak.days > 1 ? 's' : ''}`
174
+ : 'No active streak';
175
+ output += `${currentStreakEmoji} Current Streak: ${chalk_1.default.bold.green(currentStreakText)}`;
176
+ if (streakData.currentStreak.isActive && streakData.currentStreak.days > 0) {
177
+ output += chalk_1.default.dim(` - ${(0, streakCalculator_1.getStreakMotivation)(streakData.currentStreak.days, true)}`);
178
+ }
179
+ output += '\n';
180
+ const longestStreakText = streakData.longestStreak.days > 0
181
+ ? `${streakData.longestStreak.days} day${streakData.longestStreak.days > 1 ? 's' : ''} (${(0, date_fns_1.format)(streakData.longestStreak.startDate, 'MMM d')} - ${(0, date_fns_1.format)(streakData.longestStreak.endDate, 'MMM d, yyyy')})`
182
+ : 'No streaks yet';
183
+ output += `๐Ÿ† Longest Streak: ${chalk_1.default.bold.yellow(longestStreakText)}\n`;
184
+ const activePercentage = streakData.activeDayPercentage.toFixed(1);
185
+ output += `๐Ÿ“ˆ Active Days: ${chalk_1.default.bold.cyan(streakData.totalActiveDays)}/${streakData.totalDays} (${activePercentage}%)\n`;
186
+ // Time & Productivity Patterns Section
187
+ output += '\n';
188
+ output += chalk_1.default.bold.cyan('โฐ Productivity Patterns\n');
189
+ output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
190
+ // Peak hour
191
+ const peakHourLabel = timePattern.peakHour.hour === 0 ? '12:00 AM' :
192
+ timePattern.peakHour.hour < 12 ? `${timePattern.peakHour.hour}:00 AM` :
193
+ timePattern.peakHour.hour === 12 ? '12:00 PM' :
194
+ `${timePattern.peakHour.hour - 12}:00 PM`;
195
+ output += `โฑ๏ธ Peak Hour: ${chalk_1.default.bold.green(peakHourLabel)} (${timePattern.peakHour.commitCount} commits)\n`;
196
+ // Chronotype
197
+ const chronotypeLabel = (0, timePatternAnalyzer_1.getChronotypeLabel)(timePattern.chronotype);
198
+ const chronotypeDesc = (0, timePatternAnalyzer_1.getChronotypeDescription)(timePattern.chronotype);
199
+ output += `${chronotypeLabel} ${chalk_1.default.dim(`(${timePattern.chronotypeConfidence.toFixed(0)}% confidence)`)}\n`;
200
+ if (chronotypeDesc) {
201
+ output += chalk_1.default.dim(` ${chronotypeDesc}\n`);
202
+ }
203
+ // Work-life balance
204
+ const stars = 'โญ'.repeat(timePattern.workLifeBalance.score);
205
+ output += `โš–๏ธ Work-Life Balance: ${stars} ${chalk_1.default.dim(`(${timePattern.workLifeBalance.score}/5)`)}\n`;
206
+ output += chalk_1.default.dim(` Weekday: ${timePattern.workLifeBalance.weekdayCommits} | Weekend: ${timePattern.workLifeBalance.weekendCommits}\n`);
207
+ // Consistency
208
+ output += `๐Ÿ“Š Consistency: ${chalk_1.default.bold.yellow(timePattern.consistency.score.toFixed(1))}/10 ${chalk_1.default.dim(`(${timePattern.consistency.regularity})`)}\n`;
209
+ // Burnout warnings
210
+ if (timePattern.burnoutRisk.level !== 'low') {
211
+ output += `\n${chalk_1.default.yellow('โš ๏ธ Burnout Risk: ' + timePattern.burnoutRisk.level.toUpperCase())}\n`;
212
+ timePattern.burnoutRisk.indicators.forEach((indicator) => {
213
+ output += chalk_1.default.dim(` โ€ข ${indicator}\n`);
214
+ });
215
+ }
216
+ // Commit Quality Section
217
+ output += '\n';
218
+ output += chalk_1.default.bold.cyan('๐Ÿ“ Commit Quality\n');
219
+ output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
220
+ const rating = (0, commitQualityAnalyzer_1.getQualityRating)(commitQuality.overallScore);
221
+ const level = (0, commitQualityAnalyzer_1.getQualityLevel)(commitQuality.overallScore);
222
+ output += `๐Ÿ“Š Overall Score: ${chalk_1.default.bold.green(commitQuality.overallScore.toFixed(1))}/10 ${rating} ${chalk_1.default.dim(`(${level})`)}\n`;
223
+ output += `โœ… Convention: ${chalk_1.default.bold.cyan(commitQuality.conventionalCommits.adherence.toFixed(1))}%`;
224
+ if (commitQuality.conventionalCommits.adherence < 70) {
225
+ output += chalk_1.default.yellow(' โš ๏ธ');
226
+ }
227
+ else {
228
+ output += chalk_1.default.green(' โœจ');
229
+ }
230
+ output += '\n';
231
+ // Subject quality
232
+ output += `๐Ÿ“ Subject Quality: ${chalk_1.default.bold.yellow(commitQuality.subjectQuality.score.toFixed(1))}/10 `;
233
+ output += chalk_1.default.dim(`(avg ${commitQuality.subjectQuality.avgLength} chars)\n`);
234
+ // Body quality
235
+ output += `๐Ÿ“„ Body Quality: ${chalk_1.default.bold.yellow(commitQuality.bodyQuality.score.toFixed(1))}/10 `;
236
+ output += chalk_1.default.dim(`(${commitQuality.bodyQuality.withBody}/${commitQuality.totalCommits} with body)\n`);
237
+ // Top commit types
238
+ if (Object.keys(commitQuality.conventionalCommits.types).length > 0) {
239
+ output += `\n${chalk_1.default.dim('Top Commit Types:')}\n`;
240
+ const sortedTypes = Object.entries(commitQuality.conventionalCommits.types)
241
+ .sort(([, a], [, b]) => b - a)
242
+ .slice(0, 3);
243
+ sortedTypes.forEach(([type, count]) => {
244
+ output += chalk_1.default.dim(` ${type}: ${count}\n`);
245
+ });
246
+ }
247
+ // Improvements
248
+ if (commitQuality.improvements && commitQuality.improvements.length > 0) {
249
+ output += `\n${chalk_1.default.yellow('๐Ÿ’ก Tips for Improvement:')}\n`;
250
+ commitQuality.improvements.slice(0, 2).forEach((tip) => {
251
+ output += chalk_1.default.dim(` โ€ข ${tip}\n`);
252
+ });
253
+ }
254
+ // Code hygiene metrics (if any)
255
+ const totalHygiene = commitQuality.codeHygiene.quickFixes +
256
+ commitQuality.codeHygiene.workInProgress +
257
+ commitQuality.codeHygiene.oops +
258
+ commitQuality.codeHygiene.vague;
259
+ if (totalHygiene > 0) {
260
+ output += `\n${chalk_1.default.dim('๐Ÿงน Code Hygiene Indicators:\n')}`;
261
+ if (commitQuality.codeHygiene.quickFixes > 0) {
262
+ output += chalk_1.default.dim(` Quick fixes (typos/spelling): ${commitQuality.codeHygiene.quickFixes}\n`);
263
+ }
264
+ if (commitQuality.codeHygiene.workInProgress > 0) {
265
+ output += chalk_1.default.dim(` Work in progress: ${commitQuality.codeHygiene.workInProgress}\n`);
266
+ }
267
+ if (commitQuality.codeHygiene.oops > 0) {
268
+ output += chalk_1.default.dim(` Correction commits: ${commitQuality.codeHygiene.oops}\n`);
269
+ }
270
+ if (commitQuality.codeHygiene.vague > 0) {
271
+ output += chalk_1.default.dim(` Vague messages: ${commitQuality.codeHygiene.vague}\n`);
272
+ }
273
+ }
274
+ // File & Code Hotspots Section
275
+ output += '\n' + chalk_1.default.bold.hex('#58a6ff')('๐Ÿ”ฅ TOP FILE HOTSPOTS') + '\n';
276
+ if (!fileHotspots || fileHotspots.topFiles.length === 0) {
277
+ output += chalk_1.default.dim(' No file changes detected in this period.\n');
278
+ }
279
+ else {
280
+ const topFilesToShow = fileHotspots.topFiles.slice(0, 7);
281
+ const maxChanges = Math.max(...topFilesToShow.map((f) => f.changeCount));
282
+ topFilesToShow.forEach((file) => {
283
+ const barLength = Math.max(1, Math.round((file.changeCount / maxChanges) * 30));
284
+ const bar = chalk_1.default.hex('#f85149')('โ–ˆ'.repeat(barLength));
285
+ const fileName = file.path.length > 40 ? '...' + file.path.slice(-37) : file.path;
286
+ output += chalk_1.default.dim(` ${fileName}\n`);
287
+ output += ` ${bar} ${chalk_1.default.bold(file.changeCount)} changes, ${file.authors.length} author${file.authors.length !== 1 ? 's' : ''}\n`;
288
+ });
289
+ }
290
+ // Technical Debt Warnings
291
+ if (fileHotspots && fileHotspots.technicalDebt.length > 0) {
292
+ output += '\n' + chalk_1.default.bold.hex('#f85149')('โš ๏ธ TECHNICAL DEBT WARNINGS') + '\n';
293
+ const topDebt = fileHotspots.technicalDebt.slice(0, 5);
294
+ topDebt.forEach((debt) => {
295
+ const color = (0, fileHotspotAnalyzer_1.getRiskLevelColor)(debt.riskLevel);
296
+ const colorFn = color === 'red' ? chalk_1.default.red : color === 'yellow' ? chalk_1.default.yellow : chalk_1.default.gray;
297
+ const fileName = debt.file.length > 45 ? '...' + debt.file.slice(-42) : debt.file;
298
+ output += colorFn(` ${fileName}\n`);
299
+ output += chalk_1.default.dim(` Risk: ${debt.riskLevel.toUpperCase()} (complexity: ${debt.score}/10)\n`);
300
+ if (debt.recommendations.length > 0) {
301
+ output += chalk_1.default.dim(` โ†’ ${debt.recommendations[0]}\n`);
302
+ }
303
+ });
304
+ }
305
+ // Ownership Risks
306
+ if (fileHotspots && fileHotspots.ownershipRisks) {
307
+ const highRiskOwnership = fileHotspots.ownershipRisks.filter((risk) => risk.busFactorRisk >= 7);
308
+ if (highRiskOwnership.length > 0) {
309
+ output += '\n' + chalk_1.default.bold.hex('#d29922')('๐ŸšŒ BUS FACTOR WARNINGS') + '\n';
310
+ const topRisks = highRiskOwnership.slice(0, 5);
311
+ topRisks.forEach((risk) => {
312
+ const icon = (0, fileHotspotAnalyzer_1.getOwnershipIcon)(risk.ownershipType);
313
+ const fileName = risk.file.length > 45 ? '...' + risk.file.slice(-42) : risk.file;
314
+ output += chalk_1.default.yellow(` ${icon} ${fileName}\n`);
315
+ const topOwner = risk.owners[0];
316
+ output += chalk_1.default.dim(` ${topOwner.author} owns ${topOwner.percentage.toFixed(0)}% (${topOwner.commits} commits)\n`);
317
+ });
318
+ }
319
+ }
320
+ // Achievements Section
321
+ if (achievementProgress) {
322
+ output += '\n';
323
+ output += chalk_1.default.bold.hex('#ffd700')('๐Ÿ† ACHIEVEMENTS') + '\n';
324
+ output += chalk_1.default.dim('โ”€'.repeat(50)) + '\n';
325
+ // Show newly earned badges
326
+ if (achievementProgress.recentlyEarned && achievementProgress.recentlyEarned.length > 0) {
327
+ output += '\n' + chalk_1.default.bold.green('โœจ NEW BADGES EARNED! ๐ŸŽ‰') + '\n';
328
+ achievementProgress.recentlyEarned.forEach((achievement) => {
329
+ output += chalk_1.default.green(` ${achievement.emoji} ${achievement.name} - ${achievement.description}\n`);
330
+ });
331
+ }
332
+ // Show progress
333
+ output += `\n${chalk_1.default.cyan('Progress:')} ${chalk_1.default.bold.white(achievementProgress.unlockedCount)}/${achievementProgress.totalCount} badges unlocked `;
334
+ output += chalk_1.default.dim(`(${achievementProgress.completionPercentage.toFixed(1)}%)\n`);
335
+ // Progress bar
336
+ const barLength = 30;
337
+ const filledLength = Math.round((achievementProgress.completionPercentage / 100) * barLength);
338
+ const bar = 'โ–ˆ'.repeat(filledLength) + 'โ–‘'.repeat(barLength - filledLength);
339
+ output += ` ${chalk_1.default.hex('#ffd700')(bar)}\n`;
340
+ // Show next milestone
341
+ if (achievementProgress.nextMilestones && achievementProgress.nextMilestones.length > 0) {
342
+ const next = achievementProgress.nextMilestones[0];
343
+ output += `\n${chalk_1.default.yellow('๐ŸŽฏ Next Milestone:')}\n`;
344
+ output += ` ${next.emoji} ${chalk_1.default.bold(next.name)} ${chalk_1.default.dim(`(${next.progress.toFixed(0)}%)`)}\n`;
345
+ output += chalk_1.default.dim(` ${next.description}\n`);
346
+ // Progress bar for next milestone
347
+ const nextBarLength = 25;
348
+ const nextFilledLength = Math.round((next.progress / 100) * nextBarLength);
349
+ const nextBar = 'โ–ˆ'.repeat(nextFilledLength) + 'โ–‘'.repeat(nextBarLength - nextFilledLength);
350
+ output += ` ${chalk_1.default.hex('#58a6ff')(nextBar)} ${chalk_1.default.dim((100 - next.progress).toFixed(0) + '% to go!')}\n`;
351
+ }
352
+ // Show recent unlocks (excluding today's new badges)
353
+ const recentUnlocks = achievementProgress.achievements
354
+ .filter((a) => a.isUnlocked &&
355
+ !achievementProgress.recentlyEarned.some((r) => r.id === a.id) &&
356
+ a.earnedDate)
357
+ .sort((a, b) => b.earnedDate.getTime() - a.earnedDate.getTime())
358
+ .slice(0, 3);
359
+ if (recentUnlocks.length > 0) {
360
+ output += `\n${chalk_1.default.dim('Recent Unlocks:')}\n`;
361
+ recentUnlocks.forEach((achievement) => {
362
+ const daysAgo = Math.floor((Date.now() - achievement.earnedDate.getTime()) / (1000 * 60 * 60 * 24));
363
+ const timeStr = daysAgo === 0 ? 'today' : daysAgo === 1 ? '1 day ago' : `${daysAgo} days ago`;
364
+ output += chalk_1.default.dim(` ${achievement.emoji} ${achievement.name} (${timeStr})\n`);
365
+ });
366
+ }
367
+ }
368
+ return output;
369
+ }