@buckle42/vibe-check 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dan Buckley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # vibe-check
2
+
3
+ A fun stats dashboard for your code projects. See lines of code, commit streaks, activity heatmaps, and nerd culture fun facts.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Open your terminal
8
+ 2. Navigate to any git project:
9
+ ```bash
10
+ cd ~/your-project
11
+ ```
12
+ 3. Run the vibe check:
13
+ ```bash
14
+ npx @buckle42/vibe-check
15
+ ```
16
+
17
+ That's it! Works on any git repository with code files.
18
+
19
+ ## What You Get
20
+
21
+ - **Code stats** - lines, files, breakdown by language
22
+ - **Git stats** - commits, days active, streak
23
+ - **Activity calendar** - GitHub-style heatmap of last 50 days
24
+ - **Commit hours heatmap** - when you code most
25
+ - **Trophy case** - achievements unlocked
26
+ - **Vibecoding vs Human** - how much faster you shipped with Claude
27
+ - **Fun facts** - your codebase measured in Death Star trench runs, Tetris lines, LOTR journeys, and more
28
+ - **Random encouragement** - keep shipping
29
+
30
+ ## Requirements
31
+
32
+ - [Node.js](https://nodejs.org/) v18+
33
+ - Git
34
+ - Must be run inside a git repository
35
+
36
+ ## Claude Code Plugin
37
+
38
+ Want `/vibe-check` as a slash command in [Claude Code](https://claude.ai/claude-code)?
39
+
40
+ 1. Open Claude Code:
41
+ ```
42
+ claude
43
+ ```
44
+
45
+ 2. Add the marketplace and install:
46
+ ```
47
+ /plugin marketplace add buckle42/vibe-check
48
+ /plugin install vibe-check@buckle42-vibe-check
49
+ ```
50
+
51
+ 3. Restart Claude Code
52
+
53
+ 4. Navigate to a git project and run:
54
+ ```
55
+ /vibe-check
56
+ ```
57
+
58
+ 5. Press **ctrl+o** to expand and see the full dashboard
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * vibe-check - Fun CLI dashboard showing project stats
5
+ *
6
+ * Usage:
7
+ * npx vibe-check
8
+ * vibe-check (if installed globally)
9
+ */
10
+
11
+ const { execSync } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // ═══════════════════════════════════════════════════════════════════
16
+ // ANSI COLORS & UTILITIES
17
+ // ═══════════════════════════════════════════════════════════════════
18
+
19
+ const c = {
20
+ reset: '\x1b[0m',
21
+ bold: '\x1b[1m',
22
+ dim: '\x1b[2m',
23
+ cyan: '\x1b[36m',
24
+ green: '\x1b[32m',
25
+ yellow: '\x1b[33m',
26
+ magenta: '\x1b[35m',
27
+ white: '\x1b[37m',
28
+ red: '\x1b[31m',
29
+ };
30
+
31
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
32
+
33
+ function clearScreen() {
34
+ process.stdout.write('\x1b[2J\x1b[H');
35
+ }
36
+
37
+ function hideCursor() {
38
+ process.stdout.write('\x1b[?25l');
39
+ }
40
+
41
+ function showCursor() {
42
+ process.stdout.write('\x1b[?25h');
43
+ }
44
+
45
+ function moveCursor(row, col) {
46
+ process.stdout.write(`\x1b[${row};${col}H`);
47
+ }
48
+
49
+ function clearLine() {
50
+ process.stdout.write('\x1b[2K');
51
+ }
52
+
53
+ // ═══════════════════════════════════════════════════════════════════
54
+ // LOADING ANIMATION
55
+ // ═══════════════════════════════════════════════════════════════════
56
+
57
+ async function showLoading() {
58
+ hideCursor();
59
+
60
+ const frames = [
61
+ 'Scanning stats',
62
+ 'Scanning stats.',
63
+ 'Scanning stats..',
64
+ 'Scanning stats...',
65
+ ];
66
+
67
+ // Run through animation 2 times
68
+ for (let i = 0; i < 8; i++) {
69
+ process.stdout.write(`\r${c.dim}${frames[i % 4]}${c.reset} `);
70
+ await sleep(200);
71
+ }
72
+
73
+ // Clear the line and screen
74
+ process.stdout.write('\r\x1b[2K');
75
+ clearScreen();
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════════
79
+ // GIT UTILITIES
80
+ // ═══════════════════════════════════════════════════════════════════
81
+
82
+ function exec(cmd) {
83
+ try {
84
+ return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
85
+ } catch {
86
+ return '';
87
+ }
88
+ }
89
+
90
+ function getGitStats() {
91
+ const commitCount = parseInt(exec('git rev-list --count HEAD') || '0', 10);
92
+ const contributors = exec('git log --format="%aN" | sort -u').split('\n').filter(Boolean);
93
+ const firstCommitDate = exec('git log --reverse --format="%ci" | head -1');
94
+ const lastCommitDate = exec('git log --format="%ci" | head -1');
95
+ const daysActive = parseInt(exec('git log --format="%cd" --date=format:"%Y-%m-%d" | sort -u | wc -l') || '0', 10);
96
+
97
+ // Commit dates for calendar
98
+ const commitDates = exec('git log --format="%cd" --date=format:"%Y-%m-%d"').split('\n').filter(Boolean);
99
+
100
+ // Commit hours for heatmap
101
+ const commitHours = exec('git log --format="%cd" --date=format:"%H"').split('\n').filter(Boolean);
102
+
103
+ // Recent commits
104
+ const recentCommits = exec('git log --format="%s" -5').split('\n').filter(Boolean);
105
+
106
+ // Commits per day for finding best day
107
+ const commitsPerDay = exec('git log --format="%cd" --date=format:"%Y-%m-%d" | uniq -c | sort -rn | head -1');
108
+
109
+ // Current streak
110
+ const allDays = exec('git log --format="%cd" --date=format:"%Y-%m-%d" | sort -u').split('\n').filter(Boolean).reverse();
111
+
112
+ let streak = 0;
113
+ const today = new Date();
114
+ for (let i = 0; i < allDays.length; i++) {
115
+ const commitDate = new Date(allDays[i]);
116
+ const diffDays = Math.floor((today.getTime() - commitDate.getTime()) / (1000 * 60 * 60 * 24));
117
+ if (diffDays <= i + 1) {
118
+ streak++;
119
+ } else {
120
+ break;
121
+ }
122
+ }
123
+
124
+ return {
125
+ commitCount,
126
+ contributors,
127
+ firstCommitDate: firstCommitDate ? new Date(firstCommitDate) : null,
128
+ lastCommitDate: lastCommitDate ? new Date(lastCommitDate) : null,
129
+ daysActive,
130
+ commitDates,
131
+ commitHours,
132
+ recentCommits,
133
+ commitsPerDay,
134
+ streak,
135
+ };
136
+ }
137
+
138
+ // ═══════════════════════════════════════════════════════════════════
139
+ // CODE UTILITIES
140
+ // ═══════════════════════════════════════════════════════════════════
141
+
142
+ function getCodeStats() {
143
+ const extensions = ['ts', 'tsx', 'js', 'jsx', 'css', 'scss', 'json', 'md'];
144
+ const breakdown = {};
145
+ let totalLines = 0;
146
+ let totalFiles = 0;
147
+
148
+ for (const ext of extensions) {
149
+ const files = exec(`find . -name "*.${ext}" -not -path "*/node_modules/*" -not -path "*/.next/*" -not -name "package-lock.json" 2>/dev/null | wc -l`);
150
+ const fileCount = parseInt(files || '0', 10);
151
+
152
+ if (fileCount > 0) {
153
+ const lines = exec(`find . -name "*.${ext}" -not -path "*/node_modules/*" -not -path "*/.next/*" -not -name "package-lock.json" -exec cat {} \\; 2>/dev/null | wc -l`);
154
+ const lineCount = parseInt(lines || '0', 10);
155
+
156
+ // Only count source code (not json/md) in totals
157
+ if (['ts', 'tsx', 'js', 'jsx', 'css', 'scss'].includes(ext)) {
158
+ totalLines += lineCount;
159
+ totalFiles += fileCount;
160
+ }
161
+
162
+ if (lineCount > 0) {
163
+ breakdown[ext] = { files: fileCount, lines: lineCount };
164
+ }
165
+ }
166
+ }
167
+
168
+ return { totalLines, totalFiles, breakdown };
169
+ }
170
+
171
+ // ═══════════════════════════════════════════════════════════════════
172
+ // PROJECT INFO
173
+ // ═══════════════════════════════════════════════════════════════════
174
+
175
+ function getProjectName() {
176
+ try {
177
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
178
+ return pkg.name || path.basename(process.cwd());
179
+ } catch {
180
+ return path.basename(process.cwd());
181
+ }
182
+ }
183
+
184
+ function getProjectAge(firstCommitDate) {
185
+ if (!firstCommitDate) return 'just born';
186
+ const days = Math.floor((Date.now() - firstCommitDate.getTime()) / (1000 * 60 * 60 * 24));
187
+ if (days === 0) return 'born today';
188
+ if (days === 1) return 'born yesterday';
189
+ return `born ${days} days ago`;
190
+ }
191
+
192
+ // ═══════════════════════════════════════════════════════════════════
193
+ // VISUALIZATIONS
194
+ // ═══════════════════════════════════════════════════════════════════
195
+
196
+ function buildActivityCalendar(commitDates) {
197
+ const counts = new Map();
198
+ for (const date of commitDates) {
199
+ counts.set(date, (counts.get(date) || 0) + 1);
200
+ }
201
+
202
+ const today = new Date();
203
+ const cells = [];
204
+
205
+ // Last 50 days to fit better
206
+ for (let i = 49; i >= 0; i--) {
207
+ const d = new Date(today);
208
+ d.setDate(d.getDate() - i);
209
+ const key = d.toISOString().slice(0, 10);
210
+ const count = counts.get(key) || 0;
211
+
212
+ if (count === 0) cells.push(`${c.dim}░${c.reset}`);
213
+ else if (count <= 2) cells.push(`${c.green}▒${c.reset}`);
214
+ else if (count <= 5) cells.push(`${c.green}▓${c.reset}`);
215
+ else cells.push(`${c.green}█${c.reset}`);
216
+ }
217
+
218
+ return cells.join('');
219
+ }
220
+
221
+ function buildHourHeatmap(commitHours) {
222
+ const counts = new Array(24).fill(0);
223
+ for (const hour of commitHours) {
224
+ const h = parseInt(hour, 10);
225
+ if (!isNaN(h)) counts[h]++;
226
+ }
227
+
228
+ const max = Math.max(...counts, 1);
229
+ const blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
230
+
231
+ const bars = counts.map(count => {
232
+ const level = Math.floor((count / max) * 7);
233
+ const color = count === 0 ? c.dim : count === max ? c.yellow : c.green;
234
+ return `${color}${blocks[level]}${c.reset}`;
235
+ });
236
+
237
+ const peakHour = counts.indexOf(max);
238
+
239
+ return { heatmap: bars.join(''), peakHour };
240
+ }
241
+
242
+ function buildGrowthGraph(totalLines) {
243
+ // Create an ascending graph that ends at current total
244
+ const blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
245
+ const graph = [];
246
+
247
+ // Simulate growth curve (starts slow, accelerates)
248
+ const points = [0.05, 0.1, 0.18, 0.3, 0.45, 0.62, 0.8, 1.0];
249
+
250
+ for (const point of points) {
251
+ const level = Math.floor(point * 7);
252
+ graph.push(`${c.green}${blocks[level]}${c.reset}`);
253
+ }
254
+
255
+ return graph.join('');
256
+ }
257
+
258
+ function getCodingStyle(commitHours) {
259
+ const counts = new Array(24).fill(0);
260
+ for (const hour of commitHours) {
261
+ const h = parseInt(hour, 10);
262
+ if (!isNaN(h)) counts[h]++;
263
+ }
264
+
265
+ const morning = counts.slice(5, 12).reduce((a, b) => a + b, 0);
266
+ const afternoon = counts.slice(12, 18).reduce((a, b) => a + b, 0);
267
+ const evening = counts.slice(18, 22).reduce((a, b) => a + b, 0);
268
+ const night = counts.slice(22, 24).reduce((a, b) => a + b, 0) + counts.slice(0, 5).reduce((a, b) => a + b, 0);
269
+
270
+ const max = Math.max(morning, afternoon, evening, night);
271
+ if (max === night) return '🦉 Night owl';
272
+ if (max === morning) return '☕ Early bird';
273
+ if (max === evening) return '🌙 Evening coder';
274
+ return '☀️ Afternoon hacker';
275
+ }
276
+
277
+ function formatHour(hour) {
278
+ if (hour === 0) return '12am';
279
+ if (hour === 12) return '12pm';
280
+ if (hour < 12) return `${hour}am`;
281
+ return `${hour - 12}pm`;
282
+ }
283
+
284
+ // ═══════════════════════════════════════════════════════════════════
285
+ // TROPHIES (with emojis)
286
+ // ═══════════════════════════════════════════════════════════════════
287
+
288
+ function getTrophies(lines, commits) {
289
+ const trophies = [];
290
+
291
+ if (commits >= 1) trophies.push({ emoji: '🏆', label: 'First commit' });
292
+ if (lines >= 1000) trophies.push({ emoji: '📦', label: '1k lines' });
293
+ if (lines >= 10000) trophies.push({ emoji: '🚀', label: '10k lines' });
294
+ if (lines >= 25000) trophies.push({ emoji: '🔥', label: '25k lines' });
295
+ if (lines >= 50000) trophies.push({ emoji: '💎', label: '50k lines' });
296
+ if (commits >= 50) trophies.push({ emoji: '⚡', label: '50 commits' });
297
+ if (commits >= 100) trophies.push({ emoji: '💯', label: '100 commits' });
298
+
299
+ return trophies;
300
+ }
301
+
302
+ // ═══════════════════════════════════════════════════════════════════
303
+ // FUN FACTS
304
+ // ═══════════════════════════════════════════════════════════════════
305
+
306
+ function getFunFacts(lines) {
307
+ const facts = [];
308
+
309
+ // === STAR WARS ===
310
+ // A New Hope script ~7,500 words = ~1,500 lines
311
+ const aNewHopes = (lines / 1500).toFixed(1);
312
+ facts.push(`${aNewHopes}x longer than the entire Star Wars: A New Hope script`);
313
+
314
+ // Death Star trench run is 2km, if lines were meters
315
+ const trenchRuns = (lines / 2000).toFixed(1);
316
+ facts.push(`${trenchRuns} Death Star trench runs (if lines were meters)`);
317
+
318
+ // Lightsaber is ~1 meter, stacked lightsabers
319
+ const lightsabers = Math.round(lines);
320
+ facts.push(`${lightsabers.toLocaleString()} lightsabers stacked end to end`);
321
+
322
+ // === LORD OF THE RINGS ===
323
+ // Fellowship book is ~187k words = ~37,400 lines
324
+ const fellowships = (lines / 37400).toFixed(1);
325
+ facts.push(`${fellowships}x the length of The Fellowship of the Ring`);
326
+
327
+ // Full LOTR trilogy is ~481k words = ~96,200 lines
328
+ const lotrTrilogy = (lines / 96200).toFixed(1);
329
+ facts.push(`${lotrTrilogy}x the entire Lord of the Rings trilogy`);
330
+
331
+ // Mordor journey is ~1,800 miles, if lines were miles
332
+ const mordorTrips = (lines / 1800).toFixed(1);
333
+ facts.push(`${mordorTrips} journeys from the Shire to Mordor`);
334
+
335
+ // === HARRY POTTER ===
336
+ // Book 1 is ~77k words = ~15,400 lines
337
+ const hp1 = (lines / 15400).toFixed(1);
338
+ facts.push(`${hp1}x the length of Harry Potter and the Sorcerer's Stone`);
339
+
340
+ // Full HP series is ~1,084k words = ~216,800 lines
341
+ const hpSeries = (lines / 216800).toFixed(2);
342
+ facts.push(`${hpSeries}x the entire Harry Potter series`);
343
+
344
+ // === STAR TREK ===
345
+ // USS Enterprise-D is 642 meters long
346
+ const enterprises = (lines / 642).toFixed(1);
347
+ facts.push(`${enterprises} USS Enterprises laid end to end (if lines were meters)`);
348
+
349
+ // === TERMINATOR ===
350
+ // T-800's mini-gun fires ~100 rounds per second
351
+ const minigunSeconds = Math.round(lines / 100);
352
+ facts.push(`${minigunSeconds} seconds of T-800 mini-gun fire`);
353
+
354
+ // === ALIENS ===
355
+ // Colonial Marines pulse rifle has 99-round magazine
356
+ const pulseRifleMags = Math.round(lines / 99);
357
+ facts.push(`${pulseRifleMags} Colonial Marine pulse rifle magazines`);
358
+
359
+ // === GAMING ===
360
+ // Tetris - lines cleared (perfect 1:1 mapping!)
361
+ facts.push(`${lines.toLocaleString()} Tetris lines cleared - Grand Master status`);
362
+
363
+ // Legend of Zelda: original game had 128 screens
364
+ const zeldaGames = (lines / 128).toFixed(1);
365
+ facts.push(`${zeldaGames}x more lines than screens in the original Zelda`);
366
+
367
+ // Pac-Man: 240 dots per maze
368
+ const pacmanMazes = Math.round(lines / 240);
369
+ facts.push(`${pacmanMazes.toLocaleString()} Pac-Man mazes worth of dots chomped`);
370
+
371
+ // Pokemon - 151 original
372
+ const pokedexes = Math.round(lines / 151);
373
+ facts.push(`${pokedexes}x the original 151 Pokemon`);
374
+
375
+ // === DOCTOR WHO ===
376
+ // The Doctor is ~2000 years old
377
+ const doctorAges = (lines / 2000).toFixed(1);
378
+ facts.push(`${doctorAges}x the Doctor's age (in Time Lord years)`);
379
+
380
+ // === SPACE/SCI-FI ===
381
+ const moonCircumference = 6783;
382
+ const moonTrips = (lines / moonCircumference).toFixed(1);
383
+ facts.push(`${moonTrips} trips around the Moon (if lines were miles)`);
384
+
385
+ const enterprises2 = Math.round(lines / 289);
386
+ facts.push(`${enterprises2} Millennium Falcons long (if lines were meters)`);
387
+
388
+ // Return 2 random facts
389
+ const shuffled = facts.sort(() => Math.random() - 0.5);
390
+ return shuffled.slice(0, 2);
391
+ }
392
+
393
+ // ═══════════════════════════════════════════════════════════════════
394
+ // ENCOURAGEMENT
395
+ // ═══════════════════════════════════════════════════════════════════
396
+
397
+ function getEncouragement(commits, lines, streak) {
398
+ const messages = [
399
+ `${commits} commits deep. You're locked in.`,
400
+ `${lines.toLocaleString()} lines of pure determination.`,
401
+ `Shipping code and taking names.`,
402
+ `The vibes are immaculate.`,
403
+ `Your future self will thank you.`,
404
+ `This is what momentum looks like.`,
405
+ `You're building something real.`,
406
+ ];
407
+
408
+ if (streak >= 5) messages.push(`${streak} day streak. Don't break the chain.`);
409
+ if (commits >= 50) messages.push(`50+ commits. This thing has legs.`);
410
+ if (lines >= 10000) messages.push(`10k+ lines. You're not messing around.`);
411
+ if (lines >= 30000) messages.push(`30k+ lines. This is a real product now.`);
412
+
413
+ return messages[Math.floor(Math.random() * messages.length)];
414
+ }
415
+
416
+ // ═══════════════════════════════════════════════════════════════════
417
+ // BOX DRAWING HELPERS
418
+ // ═══════════════════════════════════════════════════════════════════
419
+
420
+ function line(width, char = '═') {
421
+ return char.repeat(width);
422
+ }
423
+
424
+ // Calculate visible length (strip ANSI codes, count emojis as 2)
425
+ function visibleLength(str) {
426
+ const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
427
+ let len = 0;
428
+ for (const char of stripped) {
429
+ // Emojis and special characters
430
+ const code = char.codePointAt(0) || 0;
431
+ if (code > 0x1F600 || (code >= 0x2600 && code <= 0x27BF) || code > 0xFF00) {
432
+ len += 2; // Emoji takes 2 spaces
433
+ } else {
434
+ len += 1;
435
+ }
436
+ }
437
+ return len;
438
+ }
439
+
440
+ function padRight(str, targetLen) {
441
+ const currentLen = visibleLength(str);
442
+ const padding = targetLen - currentLen;
443
+ return str + ' '.repeat(Math.max(0, padding));
444
+ }
445
+
446
+ // ═══════════════════════════════════════════════════════════════════
447
+ // MAIN RENDER
448
+ // ═══════════════════════════════════════════════════════════════════
449
+
450
+ async function render() {
451
+ // Handle exit gracefully
452
+ process.on('SIGINT', () => {
453
+ showCursor();
454
+ process.exit(0);
455
+ });
456
+
457
+ try {
458
+ // Show loading animation
459
+ await showLoading();
460
+
461
+ // Gather all stats
462
+ const projectName = getProjectName();
463
+ const gitStats = getGitStats();
464
+ const codeStats = getCodeStats();
465
+ const projectAge = getProjectAge(gitStats.firstCommitDate);
466
+ const trophies = getTrophies(codeStats.totalLines, gitStats.commitCount);
467
+ const calendar = buildActivityCalendar(gitStats.commitDates);
468
+ const { heatmap: hourHeatmap, peakHour } = buildHourHeatmap(gitStats.commitHours);
469
+ const growthGraph = buildGrowthGraph(codeStats.totalLines);
470
+ const codingStyle = getCodingStyle(gitStats.commitHours);
471
+ const encouragement = getEncouragement(gitStats.commitCount, codeStats.totalLines, gitStats.streak);
472
+ const tweetsWorth = Math.floor(codeStats.totalLines / 280);
473
+
474
+ // Parse best day
475
+ const bestDayMatch = gitStats.commitsPerDay.match(/(\d+)/);
476
+ const bestDay = bestDayMatch ? parseInt(bestDayMatch[1], 10) : 0;
477
+
478
+ // Date range
479
+ const dateRange = gitStats.firstCommitDate && gitStats.lastCommitDate
480
+ ? `${gitStats.firstCommitDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${gitStats.lastCommitDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`
481
+ : 'N/A';
482
+
483
+ // File type breakdown (top 2)
484
+ const sortedTypes = Object.entries(codeStats.breakdown)
485
+ .filter(([ext]) => ['ts', 'tsx', 'js', 'jsx', 'css'].includes(ext))
486
+ .sort((a, b) => b[1].lines - a[1].lines)
487
+ .slice(0, 2);
488
+
489
+ const W = 66; // Total inner width
490
+
491
+ const B = c.cyan; // Border color
492
+ const R = c.reset;
493
+
494
+ // Helper for bordered line - now with proper padding
495
+ const row = (content) => {
496
+ const padded = padRight(content, W);
497
+ console.log(`${B}║${R} ${padded} ${B}║${R}`);
498
+ };
499
+
500
+ // Header
501
+ console.log(`${B}╔${line(W + 2)}╗${R}`);
502
+ await sleep(30);
503
+
504
+ const pacman = `${c.magenta}ᗧ${c.yellow}··${c.magenta}ᗣ${c.yellow}··${c.magenta}ᗣ${R}`;
505
+ const nameUpper = projectName.toUpperCase();
506
+ const ageText = projectAge;
507
+ // pacman is 7 visible chars (ᗧ··ᗣ··ᗣ), 2 spaces, then name
508
+ const leftLen = 7 + 2 + nameUpper.length;
509
+ const rightLen = ageText.length;
510
+ const middlePad = W - leftLen - rightLen;
511
+ console.log(`${B}║${R} ${pacman} ${c.bold}${c.white}${nameUpper}${R}${' '.repeat(Math.max(1, middlePad))}${c.dim}${ageText}${R} ${B}║${R}`);
512
+
513
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
514
+ await sleep(50);
515
+
516
+ // Three column headers
517
+ row(`${c.bold}CODE${R} ${c.bold}GIT${R} ${c.bold}HABITS${R}`);
518
+ await sleep(30);
519
+
520
+ // Stats rows
521
+ row(`${c.yellow}${codeStats.totalLines.toLocaleString().padEnd(8)}${R}lines ${c.yellow}${gitStats.commitCount.toString().padEnd(6)}${R}commits ${c.green}${codingStyle}${R}`);
522
+ await sleep(30);
523
+ row(`${c.yellow}${codeStats.totalFiles.toString().padEnd(8)}${R}files ${c.yellow}${gitStats.daysActive.toString().padEnd(6)}${R}days active ${c.green}🔥 ${gitStats.streak} day streak${R}`);
524
+ await sleep(30);
525
+
526
+ if (sortedTypes[0]) {
527
+ row(`${c.dim}.${sortedTypes[0][0]}: ${sortedTypes[0][1].lines.toLocaleString().padEnd(7)}${R} ${c.dim}${dateRange.padEnd(15)}${R} ${c.yellow}⚡ ${bestDay} in a day${R}`);
528
+ }
529
+ if (sortedTypes[1]) {
530
+ row(`${c.dim}.${sortedTypes[1][0]}: ${sortedTypes[1][1].lines.toLocaleString()}${R}`);
531
+ }
532
+
533
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
534
+ await sleep(50);
535
+
536
+ // Git activity
537
+ row(`${c.bold}ACTIVITY${R}`);
538
+ row(calendar);
539
+ row(`${c.dim}50 days ago${' '.repeat(38)}now${R}`);
540
+
541
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
542
+ await sleep(50);
543
+
544
+ // Commit hours
545
+ row(`${c.bold}HOURS${R} ${c.dim}peak: ${c.yellow}${formatHour(peakHour)}${R}`);
546
+ row(hourHeatmap);
547
+ row(`${c.dim}12am 6am 12pm 6pm 11pm${R}`);
548
+
549
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
550
+ await sleep(50);
551
+
552
+ // Growth + Trophies side by side
553
+ // Trophy column starts at position 30
554
+ const trophyCol = 30;
555
+ const trophy = (i) => trophies[i] ? `${trophies[i].emoji} ${trophies[i].label}` : '';
556
+
557
+ row(`${c.bold}GROWTH${R}${' '.repeat(trophyCol - 6)}${c.bold}TROPHIES${R}`);
558
+ row(`${growthGraph} ${c.green}${codeStats.totalLines.toLocaleString()} lines${R}${' '.repeat(Math.max(1, trophyCol - 8 - codeStats.totalLines.toLocaleString().length - 7))}${trophy(0)}`);
559
+ row(`${c.dim}~${tweetsWorth} tweets worth${R}${' '.repeat(Math.max(1, trophyCol - tweetsWorth.toString().length - 14))}${trophy(1)}`);
560
+ row(`${' '.repeat(trophyCol)}${trophy(2)}`);
561
+ if (trophies[3]) {
562
+ row(`${' '.repeat(trophyCol)}${trophy(3)}`);
563
+ }
564
+ if (trophies[4]) {
565
+ row(`${' '.repeat(trophyCol)}${trophy(4)}`);
566
+ }
567
+
568
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
569
+ await sleep(50);
570
+
571
+ // Recent commits
572
+ row(`${c.bold}RECENT${R}`);
573
+ for (const commit of gitStats.recentCommits.slice(0, 3)) {
574
+ const maxLen = W - 4;
575
+ const truncated = commit.length > maxLen ? commit.slice(0, maxLen - 3) + '...' : commit;
576
+ row(`${c.dim}•${R} ${truncated}`);
577
+ }
578
+
579
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
580
+ await sleep(50);
581
+
582
+ // Vibecoding vs Human comparison
583
+ const industryDays = Math.round(codeStats.totalLines / 100);
584
+ const industryWeeks = (industryDays / 5).toFixed(1); // work weeks
585
+ const speedup = Math.round(industryDays / gitStats.daysActive);
586
+
587
+ row(`${c.bold}VIBECODING VS HUMAN${R}`);
588
+ row(`${c.dim}Human solo dev (~100 lines/day):${R} ${c.yellow}${industryDays} days${R}`);
589
+ row(`${c.dim}You + Claude:${R} ${c.green}${gitStats.daysActive} days${R}`);
590
+ row(`${c.dim}You shipped${R} ${c.magenta}${speedup}x faster${R} 🚀`);
591
+
592
+ // Fun facts
593
+ const funFacts = getFunFacts(codeStats.totalLines);
594
+
595
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
596
+ await sleep(50);
597
+ row(`${c.bold}FUN FACTS${R}`);
598
+ for (const fact of funFacts) {
599
+ row(`${c.dim}•${R} ${fact}`);
600
+ }
601
+
602
+ console.log(`${B}╠${line(W + 2)}╣${R}`);
603
+ await sleep(50);
604
+
605
+ // Encouragement
606
+ row(`${c.bold}${c.white}"${encouragement}"${R}`);
607
+
608
+ console.log(`${B}╚${line(W + 2)}╝${R}`);
609
+
610
+ } finally {
611
+ showCursor();
612
+ }
613
+ }
614
+
615
+ // Run it
616
+ render().catch(err => {
617
+ showCursor();
618
+ console.error('Error:', err);
619
+ process.exit(1);
620
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@buckle42/vibe-check",
3
+ "version": "1.0.0",
4
+ "description": "Fun CLI dashboard showing project stats - lines of code, git commits, activity heatmaps, and nerd culture fun facts",
5
+ "bin": {
6
+ "vibe-check": "./bin/vibe-check.js"
7
+ },
8
+ "scripts": {},
9
+ "keywords": [
10
+ "cli",
11
+ "dashboard",
12
+ "stats",
13
+ "git",
14
+ "developer-tools",
15
+ "productivity"
16
+ ],
17
+ "author": "Dan Buckley",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/buckle42/vibe-check.git"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "files": [
27
+ "bin"
28
+ ]
29
+ }