git-flex 1.0.0 → 1.1.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/package.json +3 -2
- package/src/git.js +20 -6
- package/src/terminal.js +76 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-flex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Show off your coding stats in style. Beautiful terminal cards, shareable SVGs, and fun highlights from your git history.",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"chalk": "5.6.2",
|
|
44
|
-
"commander": "12.1.0"
|
|
44
|
+
"commander": "12.1.0",
|
|
45
|
+
"string-width": "7.2.0"
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/git.js
CHANGED
|
@@ -86,17 +86,31 @@ export function getDiffStatsAndLangs({ author, since, until } = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function getAllAuthors({ since, until } = {}) {
|
|
89
|
-
|
|
89
|
+
// Get commits + lines in one pass using a marker to separate commits
|
|
90
|
+
const args = ['git', 'log', '--no-merges', '--numstat', '--format=COMMIT:%an'];
|
|
90
91
|
if (since) args.push(`--since=${since}`);
|
|
91
92
|
if (until) args.push(`--until=${until}`);
|
|
92
93
|
const raw = runArgs(args);
|
|
93
94
|
if (!raw) return [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
|
|
96
|
+
const authors = {};
|
|
97
|
+
let current = null;
|
|
98
|
+
|
|
99
|
+
for (const ln of raw.split('\n')) {
|
|
100
|
+
if (ln.startsWith('COMMIT:')) {
|
|
101
|
+
current = ln.slice(7);
|
|
102
|
+
if (!authors[current]) authors[current] = { commits: 0, added: 0, removed: 0 };
|
|
103
|
+
authors[current].commits++;
|
|
104
|
+
} else if (current && ln.includes('\t')) {
|
|
105
|
+
const [a, r] = ln.split('\t');
|
|
106
|
+
if (a === '-') continue;
|
|
107
|
+
authors[current].added += parseInt(a) || 0;
|
|
108
|
+
authors[current].removed += parseInt(r) || 0;
|
|
109
|
+
}
|
|
97
110
|
}
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
|
|
112
|
+
return Object.entries(authors)
|
|
113
|
+
.map(([name, s]) => ({ name, commits: s.commits, added: s.added, removed: s.removed, net: s.added - s.removed }))
|
|
100
114
|
.sort((a, b) => b.commits - a.commits);
|
|
101
115
|
}
|
|
102
116
|
|
package/src/terminal.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
2
3
|
import { formatHour, formatNumber } from './stats.js';
|
|
3
4
|
import { getRank, getLevel } from './rank.js';
|
|
4
5
|
import { getHighlights } from './highlights.js';
|
|
5
6
|
|
|
6
7
|
const BOX = { tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F', h: '\u2500', v: '\u2502' };
|
|
7
|
-
const W =
|
|
8
|
+
const W = 58;
|
|
8
9
|
|
|
9
|
-
function line(
|
|
10
|
-
const
|
|
11
|
-
const pad =
|
|
12
|
-
return `${
|
|
10
|
+
function line(content, width = W) {
|
|
11
|
+
const vis = stringWidth(content);
|
|
12
|
+
const pad = width - 2 - vis;
|
|
13
|
+
return `${chalk.cyan(BOX.v)}${content}${' '.repeat(Math.max(0, pad))}${chalk.cyan(BOX.v)}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function
|
|
16
|
-
return `${BOX.v} ${
|
|
16
|
+
function hr(width = W) {
|
|
17
|
+
return chalk.dim(`${BOX.v}${BOX.h.repeat(width - 2)}${BOX.v}`);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function
|
|
20
|
-
return chalk.
|
|
20
|
+
function top(width = W) {
|
|
21
|
+
return chalk.cyan(`${BOX.tl}${BOX.h.repeat(width - 2)}${BOX.tr}`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
function bot(width = W) {
|
|
25
|
+
return chalk.cyan(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sep(width = W) {
|
|
29
|
+
return chalk.cyan(`${BOX.v}${BOX.h.repeat(width - 2)}${BOX.v}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pad(str, len) {
|
|
33
|
+
const vis = stringWidth(str);
|
|
34
|
+
return str + ' '.repeat(Math.max(0, len - vis));
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function langBar(pct) {
|
|
@@ -35,24 +44,21 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
35
44
|
const level = getLevel(stats.commits);
|
|
36
45
|
const o = [];
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
o.push(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W - 2)}${BOX.tr}`));
|
|
47
|
+
o.push(top());
|
|
40
48
|
|
|
41
49
|
// Header
|
|
42
50
|
const title = ` FLEX \u2022 ${repoName}`;
|
|
43
51
|
const period = periodLabel;
|
|
44
|
-
const headerPad = W - 2 - title
|
|
52
|
+
const headerPad = W - 2 - stringWidth(title) - stringWidth(period);
|
|
45
53
|
o.push(chalk.cyan(BOX.v) + chalk.bold.white(title) + ' '.repeat(Math.max(1, headerPad)) + chalk.dim(period) + chalk.cyan(BOX.v));
|
|
46
54
|
|
|
47
|
-
o.push(
|
|
55
|
+
o.push(sep());
|
|
48
56
|
|
|
49
57
|
// User + rank
|
|
50
|
-
|
|
51
|
-
o.push(line(chalk.cyan(BOX.v), userLine, chalk.cyan(BOX.v)));
|
|
58
|
+
o.push(line(` ${chalk.bold.yellow(stats.author)} ${rank.icon} ${chalk.italic.magenta(rank.title)}`));
|
|
52
59
|
|
|
53
60
|
// Level bar
|
|
54
|
-
|
|
55
|
-
o.push(line(chalk.cyan(BOX.v), lvlLine, chalk.cyan(BOX.v)));
|
|
61
|
+
o.push(line(` ${chalk.dim('LVL')} ${chalk.white(level.level)} ${chalk.cyan(level.bar)} ${chalk.dim(level.name)}`));
|
|
56
62
|
|
|
57
63
|
o.push(hr());
|
|
58
64
|
|
|
@@ -71,21 +77,21 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
71
77
|
];
|
|
72
78
|
|
|
73
79
|
for (let i = 0; i < col1.length; i++) {
|
|
74
|
-
const left = ` ${
|
|
75
|
-
const right = `${
|
|
80
|
+
const left = ` ${pad(col1[i][0], 10)} ${pad(col1[i][1], 10)}`;
|
|
81
|
+
const right = `${pad(col2[i][0], 11)} ${col2[i][1]}`;
|
|
76
82
|
const full = left + ' ' + right;
|
|
77
|
-
o.push(line(
|
|
83
|
+
o.push(line(full));
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
// Languages
|
|
81
87
|
if (stats.languages.length) {
|
|
82
88
|
o.push(hr());
|
|
83
|
-
o.push(line(
|
|
89
|
+
o.push(line(` ${chalk.bold.white('Languages')}`));
|
|
84
90
|
for (const lang of stats.languages.slice(0, 5)) {
|
|
85
|
-
const lbl =
|
|
91
|
+
const lbl = pad(` ${chalk.white(lang.name)}`, 20);
|
|
86
92
|
const bar = langBar(lang.pct);
|
|
87
93
|
const pctStr = chalk.dim(`${lang.pct}%`);
|
|
88
|
-
o.push(line(
|
|
94
|
+
o.push(line(`${lbl} ${bar} ${pctStr}`));
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -94,13 +100,11 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
94
100
|
if (highlights.length) {
|
|
95
101
|
o.push(hr());
|
|
96
102
|
for (const h of highlights) {
|
|
97
|
-
o.push(line(
|
|
103
|
+
o.push(line(` ${chalk.yellow('>')} ${chalk.italic.white(h)}`));
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
o.push(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W - 2)}${BOX.br}`));
|
|
103
|
-
|
|
107
|
+
o.push(bot());
|
|
104
108
|
return o.join('\n');
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -108,71 +112,73 @@ export function renderComparison(stats1, stats2, streak1, streak2, repoName) {
|
|
|
108
112
|
const rank1 = getRank(stats1, streak1);
|
|
109
113
|
const rank2 = getRank(stats2, streak2);
|
|
110
114
|
const o = [];
|
|
111
|
-
const
|
|
115
|
+
const CW = 64;
|
|
112
116
|
|
|
113
|
-
o.push(
|
|
114
|
-
o.push(line(chalk.
|
|
115
|
-
o.push(
|
|
117
|
+
o.push(top(CW));
|
|
118
|
+
o.push(line(chalk.bold.white(` FLEX VS \u2022 ${repoName}`), CW));
|
|
119
|
+
o.push(sep(CW));
|
|
116
120
|
|
|
117
121
|
function vsLine(label, v1, v2) {
|
|
118
|
-
return line(chalk.
|
|
119
|
-
` ${padRight(chalk.dim(label), 12)} ${padRight(v1, 14)} ${chalk.dim('vs')} ${padRight(v2, 14)}`,
|
|
120
|
-
chalk.cyan(BOX.v));
|
|
122
|
+
return line(` ${pad(chalk.dim(label), 12)} ${pad(v1, 14)} ${chalk.dim('vs')} ${pad(v2, 14)}`, CW);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
// Names
|
|
124
125
|
o.push(vsLine('', chalk.bold.yellow(stats1.author), chalk.bold.blue(stats2.author)));
|
|
125
126
|
o.push(vsLine('Rank', chalk.magenta(`${rank1.icon} ${rank1.title}`), chalk.magenta(`${rank2.icon} ${rank2.title}`)));
|
|
126
|
-
o.push(hr());
|
|
127
|
+
o.push(hr(CW));
|
|
127
128
|
o.push(vsLine('Commits', chalk.white(formatNumber(stats1.commits)), chalk.white(formatNumber(stats2.commits))));
|
|
128
129
|
o.push(vsLine('Added', chalk.green('+' + formatNumber(stats1.added)), chalk.green('+' + formatNumber(stats2.added))));
|
|
129
130
|
o.push(vsLine('Removed', chalk.red('-' + formatNumber(stats1.removed)), chalk.red('-' + formatNumber(stats2.removed))));
|
|
130
131
|
o.push(vsLine('Files', chalk.white(formatNumber(stats1.filesCount)), chalk.white(formatNumber(stats2.filesCount))));
|
|
131
132
|
o.push(vsLine('Streak', chalk.yellow(`${streak1.current}d`), chalk.yellow(`${streak2.current}d`)));
|
|
132
133
|
|
|
133
|
-
o.push(
|
|
134
|
+
o.push(bot(CW));
|
|
134
135
|
return o.join('\n');
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export function renderTeam(authors, repoName, periodLabel) {
|
|
138
139
|
const o = [];
|
|
139
|
-
const
|
|
140
|
+
const TW = 76;
|
|
141
|
+
|
|
142
|
+
o.push(top(TW));
|
|
143
|
+
o.push(line(chalk.bold.white(` FLEX TEAM \u2022 ${repoName} \u2022 ${periodLabel}`), TW));
|
|
144
|
+
o.push(sep(TW));
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
o.push(line(chalk.
|
|
143
|
-
o.push(
|
|
146
|
+
// Header row
|
|
147
|
+
o.push(line(` ${pad('', 4)} ${pad(chalk.dim('Name'), 18)} ${pad(chalk.dim('Commits'), 8)} ${pad(chalk.dim('Added'), 9)} ${pad(chalk.dim('Removed'), 9)} ${pad(chalk.dim('Net'), 9)}`, TW));
|
|
148
|
+
o.push(hr(TW));
|
|
144
149
|
|
|
145
150
|
const medals = ['\u{1F947}', '\u{1F948}', '\u{1F949}'];
|
|
146
|
-
const maxCommits = authors[0]?.commits || 1;
|
|
147
151
|
|
|
148
152
|
for (let i = 0; i < Math.min(authors.length, 15); i++) {
|
|
149
153
|
const a = authors[i];
|
|
150
154
|
const medal = medals[i] || chalk.dim(`${i + 1}.`);
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
const name = pad(chalk.white(a.name), 18);
|
|
156
|
+
const commits = pad(chalk.bold.white(formatNumber(a.commits)), 8);
|
|
157
|
+
const added = pad(chalk.green('+' + formatNumber(a.added)), 9);
|
|
158
|
+
const removed = pad(chalk.red('-' + formatNumber(a.removed)), 9);
|
|
159
|
+
const net = a.net >= 0
|
|
160
|
+
? chalk.green('+' + formatNumber(a.net))
|
|
161
|
+
: chalk.red(formatNumber(a.net));
|
|
162
|
+
o.push(line(` ${pad(medal, 4)} ${name} ${commits} ${added} ${removed} ${pad(net, 9)}`, TW));
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
o.push(
|
|
165
|
+
o.push(bot(TW));
|
|
159
166
|
return o.join('\n');
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
export function renderStreak(streakData, stats, repoName) {
|
|
163
170
|
const o = [];
|
|
164
|
-
o.push(
|
|
165
|
-
o.push(line(chalk.
|
|
166
|
-
o.push(
|
|
171
|
+
o.push(top());
|
|
172
|
+
o.push(line(chalk.bold.white(` FLEX STREAK \u2022 ${repoName}`)));
|
|
173
|
+
o.push(sep());
|
|
167
174
|
|
|
168
|
-
o.push(line(
|
|
169
|
-
o.push(line(
|
|
170
|
-
o.push(line(
|
|
175
|
+
o.push(line(` ${chalk.dim('Author')} ${chalk.bold.yellow(stats.author)}`));
|
|
176
|
+
o.push(line(` ${chalk.dim('Current')} ${chalk.bold.yellow(`${streakData.current} days`)} ${streakData.current >= 7 ? '\u{1F525}' : ''}`));
|
|
177
|
+
o.push(line(` ${chalk.dim('Longest')} ${chalk.bold.white(`${streakData.longest} days`)} \u{1F3C6}`));
|
|
171
178
|
|
|
172
|
-
// Visual streak bar
|
|
173
179
|
const days = Math.min(streakData.current, 30);
|
|
174
180
|
const bar = chalk.yellow('\u2588'.repeat(days)) + chalk.dim('\u2591'.repeat(30 - days));
|
|
175
|
-
o.push(line(
|
|
181
|
+
o.push(line(` ${bar}`));
|
|
176
182
|
|
|
177
183
|
let msg = '';
|
|
178
184
|
if (streakData.current === 0) msg = 'Start a streak today!';
|
|
@@ -181,30 +187,31 @@ export function renderStreak(streakData, stats, repoName) {
|
|
|
181
187
|
else if (streakData.current >= 7) msg = 'One week strong!';
|
|
182
188
|
else if (streakData.current >= 3) msg = 'Building momentum...';
|
|
183
189
|
else msg = 'Keep it going!';
|
|
184
|
-
o.push(line(
|
|
190
|
+
o.push(line(` ${chalk.italic.dim(msg)}`));
|
|
185
191
|
|
|
186
|
-
o.push(
|
|
192
|
+
o.push(bot());
|
|
187
193
|
return o.join('\n');
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
export function renderLangs(stats, repoName, periodLabel) {
|
|
191
197
|
const o = [];
|
|
192
|
-
|
|
193
|
-
o.push(
|
|
194
|
-
o.push(chalk.
|
|
198
|
+
const LW = 62;
|
|
199
|
+
o.push(top(LW));
|
|
200
|
+
o.push(line(chalk.bold.white(` FLEX LANGS \u2022 ${repoName} \u2022 ${periodLabel}`), LW));
|
|
201
|
+
o.push(sep(LW));
|
|
195
202
|
|
|
196
203
|
if (!stats.languages.length) {
|
|
197
|
-
o.push(line(chalk.
|
|
204
|
+
o.push(line(chalk.dim(' No language data found'), LW));
|
|
198
205
|
} else {
|
|
199
206
|
for (const lang of stats.languages) {
|
|
200
|
-
const lbl =
|
|
207
|
+
const lbl = pad(` ${chalk.bold.white(lang.name)}`, 16);
|
|
201
208
|
const bar = langBar(lang.pct);
|
|
202
|
-
const pct =
|
|
209
|
+
const pct = pad(chalk.yellow(`${lang.pct}%`), 5);
|
|
203
210
|
const cnt = chalk.dim(`(${lang.count} files)`);
|
|
204
|
-
o.push(line(
|
|
211
|
+
o.push(line(`${lbl} ${bar} ${pct} ${cnt}`, LW));
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
o.push(
|
|
215
|
+
o.push(bot(LW));
|
|
209
216
|
return o.join('\n');
|
|
210
217
|
}
|