@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 +21 -0
- package/README.md +62 -0
- package/bin/vibe-check.js +620 -0
- package/package.json +29 -0
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
|
+
}
|