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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-flex",
3
- "version": "1.0.0",
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
- const args = ['git', 'log', '--no-merges', '--format=%an'];
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
- const counts = {};
95
- for (const name of raw.split('\n').filter(Boolean)) {
96
- counts[name] = (counts[name] || 0) + 1;
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
- return Object.entries(counts)
99
- .map(([name, commits]) => ({ name, commits }))
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 = 56;
8
+ const W = 58;
8
9
 
9
- function line(left, content, right) {
10
- const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
11
- const pad = W - 2 - stripped.length;
12
- return `${left}${content}${' '.repeat(Math.max(0, pad))}${right}`;
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 box(content) {
16
- return `${BOX.v} ${content}`;
16
+ function hr(width = W) {
17
+ return chalk.dim(`${BOX.v}${BOX.h.repeat(width - 2)}${BOX.v}`);
17
18
  }
18
19
 
19
- function hr() {
20
- return chalk.dim(`${BOX.v}${BOX.h.repeat(W - 2)}${BOX.v}`);
20
+ function top(width = W) {
21
+ return chalk.cyan(`${BOX.tl}${BOX.h.repeat(width - 2)}${BOX.tr}`);
21
22
  }
22
23
 
23
- function padRight(str, len) {
24
- const stripped = str.replace(/\x1B\[[0-9;]*m/g, '');
25
- return str + ' '.repeat(Math.max(0, len - stripped.length));
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
- // Top border
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.length - period.length;
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(chalk.cyan(`${BOX.v}${BOX.h.repeat(W - 2)}${BOX.v}`));
55
+ o.push(sep());
48
56
 
49
57
  // User + rank
50
- const userLine = ` ${chalk.bold.yellow(stats.author)} ${rank.icon} ${chalk.italic.magenta(rank.title)}`;
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
- const lvlLine = ` ${chalk.dim('LVL')} ${chalk.white(level.level)} ${chalk.cyan(level.bar)} ${chalk.dim(level.name)}`;
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 = ` ${padRight(col1[i][0], 10)} ${padRight(col1[i][1], 10)}`;
75
- const right = `${padRight(col2[i][0], 11)} ${col2[i][1]}`;
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(chalk.cyan(BOX.v), full, chalk.cyan(BOX.v)));
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(chalk.cyan(BOX.v), ` ${chalk.bold.white('Languages')}`, chalk.cyan(BOX.v)));
89
+ o.push(line(` ${chalk.bold.white('Languages')}`));
84
90
  for (const lang of stats.languages.slice(0, 5)) {
85
- const lbl = padRight(` ${chalk.white(lang.name)}`, 20);
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(chalk.cyan(BOX.v), `${lbl} ${bar} ${pctStr}`, chalk.cyan(BOX.v)));
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(chalk.cyan(BOX.v), ` ${chalk.yellow('>')} ${chalk.italic.white(h)}`, chalk.cyan(BOX.v)));
103
+ o.push(line(` ${chalk.yellow('>')} ${chalk.italic.white(h)}`));
98
104
  }
99
105
  }
100
106
 
101
- // Bottom
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 W2 = 64;
115
+ const CW = 64;
112
116
 
113
- o.push(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W2 - 2)}${BOX.tr}`));
114
- o.push(line(chalk.cyan(BOX.v), chalk.bold.white(` FLEX VS \u2022 ${repoName}`), chalk.cyan(BOX.v)));
115
- o.push(chalk.cyan(`${BOX.v}${BOX.h.repeat(W2 - 2)}${BOX.v}`));
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.cyan(BOX.v),
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(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W2 - 2)}${BOX.br}`));
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 W2 = 52;
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
- o.push(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W2 - 2)}${BOX.tr}`));
142
- o.push(line(chalk.cyan(BOX.v), chalk.bold.white(` FLEX TEAM \u2022 ${repoName} \u2022 ${periodLabel}`), chalk.cyan(BOX.v)));
143
- o.push(chalk.cyan(`${BOX.v}${BOX.h.repeat(W2 - 2)}${BOX.v}`));
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 bar = chalk.green('\u2588'.repeat(Math.max(1, Math.round((a.commits / maxCommits) * 15))));
152
- const name = padRight(chalk.white(a.name), 20);
153
- o.push(line(chalk.cyan(BOX.v),
154
- ` ${padRight(medal, 4)} ${name} ${bar} ${chalk.dim(a.commits)}`,
155
- chalk.cyan(BOX.v)));
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(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W2 - 2)}${BOX.br}`));
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(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W - 2)}${BOX.tr}`));
165
- o.push(line(chalk.cyan(BOX.v), chalk.bold.white(` FLEX STREAK \u2022 ${repoName}`), chalk.cyan(BOX.v)));
166
- o.push(chalk.cyan(`${BOX.v}${BOX.h.repeat(W - 2)}${BOX.v}`));
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(chalk.cyan(BOX.v), ` ${chalk.dim('Author')} ${chalk.bold.yellow(stats.author)}`, chalk.cyan(BOX.v)));
169
- o.push(line(chalk.cyan(BOX.v), ` ${chalk.dim('Current')} ${chalk.bold.yellow(`${streakData.current} days`)} ${streakData.current >= 7 ? '\u{1F525}' : ''}`, chalk.cyan(BOX.v)));
170
- o.push(line(chalk.cyan(BOX.v), ` ${chalk.dim('Longest')} ${chalk.bold.white(`${streakData.longest} days`)} \u{1F3C6}`, chalk.cyan(BOX.v)));
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(chalk.cyan(BOX.v), ` ${bar}`, chalk.cyan(BOX.v)));
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(chalk.cyan(BOX.v), ` ${chalk.italic.dim(msg)}`, chalk.cyan(BOX.v)));
190
+ o.push(line(` ${chalk.italic.dim(msg)}`));
185
191
 
186
- o.push(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W - 2)}${BOX.br}`));
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
- o.push(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W - 2)}${BOX.tr}`));
193
- o.push(line(chalk.cyan(BOX.v), chalk.bold.white(` FLEX LANGS \u2022 ${repoName} \u2022 ${periodLabel}`), chalk.cyan(BOX.v)));
194
- o.push(chalk.cyan(`${BOX.v}${BOX.h.repeat(W - 2)}${BOX.v}`));
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.cyan(BOX.v), chalk.dim(' No language data found'), chalk.cyan(BOX.v)));
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 = padRight(` ${chalk.bold.white(lang.name)}`, 22);
207
+ const lbl = pad(` ${chalk.bold.white(lang.name)}`, 16);
201
208
  const bar = langBar(lang.pct);
202
- const pct = padRight(chalk.yellow(`${lang.pct}%`), 6);
209
+ const pct = pad(chalk.yellow(`${lang.pct}%`), 5);
203
210
  const cnt = chalk.dim(`(${lang.count} files)`);
204
- o.push(line(chalk.cyan(BOX.v), `${lbl} ${bar} ${pct} ${cnt}`, chalk.cyan(BOX.v)));
211
+ o.push(line(`${lbl} ${bar} ${pct} ${cnt}`, LW));
205
212
  }
206
213
  }
207
214
 
208
- o.push(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W - 2)}${BOX.br}`));
215
+ o.push(bot(LW));
209
216
  return o.join('\n');
210
217
  }