commitshow 0.3.9 → 0.3.12
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/dist/lib/render.js +142 -127
- package/package.json +1 -1
package/dist/lib/render.js
CHANGED
|
@@ -23,137 +23,132 @@ function scoreBar(value, max) {
|
|
|
23
23
|
const tone = scoreTone(Math.round((value / max) * 100));
|
|
24
24
|
return tone('▰'.repeat(filled)) + c.muted('▱'.repeat(empty));
|
|
25
25
|
}
|
|
26
|
+
// ANSI Shadow figlet font · transcribed via oh-my-logo --filled. CEO
|
|
27
|
+
// referenced the Claude Code style; Claude Code itself doesn't embed a
|
|
28
|
+
// text-as-ASCII logo (only the Clawd mascot lives in its bundle), but
|
|
29
|
+
// oh-my-logo is the open-source library that imitates the look and ANSI
|
|
30
|
+
// Shadow is the free figlet font it uses. 6 rows tall, variable width per
|
|
31
|
+
// glyph (4 to 10 cols), pre-padded so bigText concats cleanly.
|
|
26
32
|
const BIG_DIGITS = {
|
|
27
33
|
"0": [
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"11111",
|
|
34
|
+
" ██████╗ ",
|
|
35
|
+
" ██╔═████╗",
|
|
36
|
+
" ██║██╔██║",
|
|
37
|
+
" ████╔╝██║",
|
|
38
|
+
" ╚██████╔╝",
|
|
39
|
+
" ╚═════╝ ",
|
|
35
40
|
],
|
|
36
41
|
"1": [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"01110",
|
|
42
|
+
" ██╗",
|
|
43
|
+
" ███║",
|
|
44
|
+
" ╚██║",
|
|
45
|
+
" ██║",
|
|
46
|
+
" ██║",
|
|
47
|
+
" ╚═╝",
|
|
44
48
|
],
|
|
45
49
|
"2": [
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"11111",
|
|
50
|
+
" ██████╗ ",
|
|
51
|
+
" ╚════██╗",
|
|
52
|
+
" █████╔╝",
|
|
53
|
+
" ██╔═══╝ ",
|
|
54
|
+
" ███████╗",
|
|
55
|
+
" ╚══════╝",
|
|
53
56
|
],
|
|
54
57
|
"3": [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"11111",
|
|
58
|
+
" ██████╗ ",
|
|
59
|
+
" ╚════██╗",
|
|
60
|
+
" █████╔╝",
|
|
61
|
+
" ╚═══██╗",
|
|
62
|
+
" ██████╔╝",
|
|
63
|
+
" ╚═════╝ ",
|
|
62
64
|
],
|
|
63
65
|
"4": [
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"00001",
|
|
66
|
+
" ██╗ ██╗",
|
|
67
|
+
" ██║ ██║",
|
|
68
|
+
" ███████║",
|
|
69
|
+
" ╚════██║",
|
|
70
|
+
" ██║",
|
|
71
|
+
" ╚═╝",
|
|
71
72
|
],
|
|
72
73
|
"5": [
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"11111",
|
|
74
|
+
" ███████╗",
|
|
75
|
+
" ██╔════╝",
|
|
76
|
+
" ███████╗",
|
|
77
|
+
" ╚════██║",
|
|
78
|
+
" ███████║",
|
|
79
|
+
" ╚══════╝",
|
|
80
80
|
],
|
|
81
81
|
"6": [
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"11111",
|
|
82
|
+
" ██████╗ ",
|
|
83
|
+
" ██╔════╝ ",
|
|
84
|
+
" ███████╗ ",
|
|
85
|
+
" ██╔═══██╗",
|
|
86
|
+
" ╚██████╔╝",
|
|
87
|
+
" ╚═════╝ ",
|
|
89
88
|
],
|
|
90
89
|
"7": [
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"00001",
|
|
90
|
+
" ███████╗",
|
|
91
|
+
" ╚════██║",
|
|
92
|
+
" ██╔╝",
|
|
93
|
+
" ██╔╝ ",
|
|
94
|
+
" ██║ ",
|
|
95
|
+
" ╚═╝ ",
|
|
98
96
|
],
|
|
99
97
|
"8": [
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"11111",
|
|
98
|
+
" █████╗ ",
|
|
99
|
+
" ██╔══██╗",
|
|
100
|
+
" ╚█████╔╝",
|
|
101
|
+
" ██╔══██╗",
|
|
102
|
+
" ╚█████╔╝",
|
|
103
|
+
" ╚════╝ ",
|
|
107
104
|
],
|
|
108
105
|
"9": [
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"11111",
|
|
106
|
+
" █████╗ ",
|
|
107
|
+
" ██╔══██╗",
|
|
108
|
+
" ╚██████║",
|
|
109
|
+
" ╚═══██║",
|
|
110
|
+
" █████╔╝",
|
|
111
|
+
" ╚════╝ ",
|
|
116
112
|
],
|
|
117
113
|
"/": [
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"10000",
|
|
114
|
+
" ██╗",
|
|
115
|
+
" ██╔╝",
|
|
116
|
+
" ██╔╝ ",
|
|
117
|
+
" ██╔╝ ",
|
|
118
|
+
" ██╔╝ ",
|
|
119
|
+
" ╚═╝ ",
|
|
125
120
|
],
|
|
126
121
|
" ": [
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"00000",
|
|
122
|
+
" ",
|
|
123
|
+
" ",
|
|
124
|
+
" ",
|
|
125
|
+
" ",
|
|
126
|
+
" ",
|
|
127
|
+
" ",
|
|
134
128
|
],
|
|
135
129
|
};
|
|
136
|
-
const BIG_ROWS =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return m.map(row => row.split("").map(c => c === "1" ? PIXEL_FILL : PIXEL_BLANK).join(PIXEL_GAP));
|
|
144
|
-
}
|
|
145
|
-
/** Render a string ("68", "100", "82/100") as a pixel-grid block numeral. */
|
|
130
|
+
const BIG_ROWS = 6;
|
|
131
|
+
/** Render a string ("68", "100", "82/100") in ANSI Shadow figlet font.
|
|
132
|
+
* Each glyph already carries 1 col of leading + 1 col of trailing space,
|
|
133
|
+
* so a 1-space gutter is enough to separate adjacent digits without the
|
|
134
|
+
* tracking looking gappy. Variable-width glyphs are NOT padded — the
|
|
135
|
+
* font is a proportional shadow font and looks best when concatenated
|
|
136
|
+
* natively, exactly as `figlet` and oh-my-logo render it. */
|
|
146
137
|
function bigText(text) {
|
|
147
|
-
const rows = Array.from({ length: BIG_ROWS }, () =>
|
|
148
|
-
const
|
|
149
|
-
for (let i = 0; i <
|
|
150
|
-
const
|
|
138
|
+
const rows = Array.from({ length: BIG_ROWS }, () => '');
|
|
139
|
+
const GAP = ' ';
|
|
140
|
+
for (let i = 0; i < text.length; i++) {
|
|
141
|
+
const ch = text[i];
|
|
142
|
+
const glyph = BIG_DIGITS[ch] ?? BIG_DIGITS[' '];
|
|
151
143
|
for (let r = 0; r < BIG_ROWS; r++) {
|
|
152
|
-
rows[r] += glyph[r] + (i <
|
|
144
|
+
rows[r] += glyph[r] + (i < text.length - 1 ? GAP : '');
|
|
153
145
|
}
|
|
154
146
|
}
|
|
155
147
|
return rows;
|
|
156
148
|
}
|
|
149
|
+
/** Visible (rune) length — counts Unicode code points so the centering math
|
|
150
|
+
* treats `█` and `╔` as one column each, matching how monospace terminals
|
|
151
|
+
* render the ANSI Shadow font. */
|
|
157
152
|
function bigTextWidth(text) {
|
|
158
153
|
if (text.length === 0)
|
|
159
154
|
return 0;
|
|
@@ -271,42 +266,57 @@ export function renderAudit(view) {
|
|
|
271
266
|
lines.push(' ' + boxBottom());
|
|
272
267
|
lines.push('');
|
|
273
268
|
}
|
|
274
|
-
// Hero score ·
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
const bigWidth = bigRows[0].length;
|
|
280
|
-
const leftPad = Math.floor((58 - bigWidth) / 2);
|
|
281
|
-
for (const row of bigRows) {
|
|
282
|
-
lines.push(' ' + ' '.repeat(leftPad) + c.pixelInk(row));
|
|
283
|
-
}
|
|
284
|
-
// Breathing room between the hero ASCII and the small caption. Without
|
|
285
|
-
// it the "/ 100 · walk-on · strong" line glues to the bottom of the
|
|
286
|
-
// digits and the score reads as one block.
|
|
287
|
-
lines.push('');
|
|
288
|
-
// Caption · small "/ 100 · band" · band tinted so the signal lives there.
|
|
289
|
-
// Walk-on track gets an extra middle segment + a sub-line surfacing the
|
|
290
|
-
// 95 max so users read the score in the right context (88 walk-on ≠ 88
|
|
291
|
-
// league · perfect walk-on caps at 95 because Scout+Community pillars
|
|
292
|
-
// structurally unevaluated).
|
|
269
|
+
// Hero score · trophy plate. Score + caption wrapped in a double-line
|
|
270
|
+
// ╔═╗ box so the X/Twitter screenshot has a single hero artifact you can
|
|
271
|
+
// crop to. Box is intentionally wider than tall (≈3:2) — square in cell
|
|
272
|
+
// count is too tall once the 6-row ANSI Shadow + 1 caption + 2 padding
|
|
273
|
+
// rows are stacked. Outer 58-col layout still centers the box.
|
|
293
274
|
const band = total >= 75 ? 'strong' : total >= 50 ? 'mid' : 'weak';
|
|
294
275
|
const bandTone = scoreTone(total);
|
|
276
|
+
const bigRows = bigText(String(total));
|
|
277
|
+
const bigWidth = bigRows[0].length;
|
|
278
|
+
// Inner content width = the longer of (digit width, caption width) + a
|
|
279
|
+
// small breathing margin on each side. Box outer = inner + 2 (frame).
|
|
295
280
|
const captionVisible = isWalkOn
|
|
296
281
|
? `/ 100 · walk-on · ${band}`
|
|
297
282
|
: `/ 100 · ${band}`;
|
|
298
|
-
const
|
|
283
|
+
const SCORE_PAD = 4; // 2 cells of breathing room on each side of widest line
|
|
284
|
+
const scoreInsideW = Math.max(bigWidth, captionVisible.length) + SCORE_PAD;
|
|
285
|
+
const scoreOuterW = scoreInsideW + 2;
|
|
286
|
+
// Center the trophy box inside the 58-col layout
|
|
287
|
+
const trophyLeftPad = Math.max(0, Math.floor((58 - scoreOuterW) / 2));
|
|
288
|
+
const trophyIndent = ' ' + ' '.repeat(trophyLeftPad);
|
|
289
|
+
const trophyTop = c.muted('╔' + '═'.repeat(scoreInsideW) + '╗');
|
|
290
|
+
const trophyBottom = c.muted('╚' + '═'.repeat(scoreInsideW) + '╝');
|
|
291
|
+
const trophyBlank = c.muted('║' + ' '.repeat(scoreInsideW) + '║');
|
|
292
|
+
// Center a colored line inside the box. visibleLen is the *visible*
|
|
293
|
+
// (ANSI-stripped) cell count of the colored content.
|
|
294
|
+
const trophyRow = (visibleLen, colored) => {
|
|
295
|
+
const pad = Math.max(0, scoreInsideW - visibleLen);
|
|
296
|
+
const lp = Math.floor(pad / 2);
|
|
297
|
+
const rp = pad - lp;
|
|
298
|
+
return c.muted('║') + ' '.repeat(lp) + colored + ' '.repeat(rp) + c.muted('║');
|
|
299
|
+
};
|
|
300
|
+
lines.push(trophyIndent + trophyTop);
|
|
301
|
+
lines.push(trophyIndent + trophyBlank);
|
|
302
|
+
for (const row of bigRows) {
|
|
303
|
+
lines.push(trophyIndent + trophyRow(bigWidth, c.pixelInk(row)));
|
|
304
|
+
}
|
|
305
|
+
lines.push(trophyIndent + trophyBlank);
|
|
306
|
+
// Caption row · band-tinted so the signal lives on the band word.
|
|
307
|
+
const captionColored = isWalkOn
|
|
308
|
+
? c.muted('/ 100 · ') + c.gold('walk-on') + c.muted(' · ') + bandTone(band)
|
|
309
|
+
: c.muted('/ 100 · ') + bandTone(band);
|
|
310
|
+
lines.push(trophyIndent + trophyRow(captionVisible.length, captionColored));
|
|
311
|
+
lines.push(trophyIndent + trophyBottom);
|
|
312
|
+
// Walk-on sub-caption stays OUTSIDE the trophy — it's an explanation
|
|
313
|
+
// of the cap, not part of the headline. Keeps the box clean and tight
|
|
314
|
+
// for screenshot crops.
|
|
299
315
|
if (isWalkOn) {
|
|
300
|
-
lines.push(' ' + ' '.repeat(capPad)
|
|
301
|
-
+ c.muted('/ 100 · ') + c.gold('walk-on') + c.muted(' · ') + bandTone(band));
|
|
302
|
-
// Sub-caption explaining the 5pt headroom · centered, dim
|
|
303
316
|
const subVisible = 'audition unlocks final 5 · max walk-on score 95';
|
|
304
317
|
const subPad = Math.floor((58 - subVisible.length) / 2);
|
|
305
318
|
lines.push(' ' + ' '.repeat(subPad) + c.muted(subVisible));
|
|
306
319
|
}
|
|
307
|
-
else {
|
|
308
|
-
lines.push(' ' + ' '.repeat(capPad) + c.muted('/ 100 · ') + bandTone(band));
|
|
309
|
-
}
|
|
310
320
|
lines.push('');
|
|
311
321
|
// Axis bars · league shows all three; walk-on shows Audit only and
|
|
312
322
|
// surfaces Scout + Community as locked-with-unlock-hint rows.
|
|
@@ -352,8 +362,13 @@ export function renderAudit(view) {
|
|
|
352
362
|
lines.push(' ' + boxRow(2 + detailTrunc.length, c.muted(' ') + c.muted(detailTrunc)));
|
|
353
363
|
// Third line: evidence (if any) · only on fail/warn so success cases stay compact
|
|
354
364
|
if (it.evidence && (it.status === 'fail' || it.status === 'warn')) {
|
|
355
|
-
|
|
356
|
-
|
|
365
|
+
// `' → '` is 4 visible chars (two leading spaces + arrow + space)
|
|
366
|
+
// — boxRow needs the full visible length or its right │ lands 2
|
|
367
|
+
// cells too far right and the row "bursts" out of the box.
|
|
368
|
+
// Cap evTrunc at 48 so even on the widest evidence string the row
|
|
369
|
+
// stays inside CONTENT_W (54): 4 prefix + 48 evidence + 2 padding.
|
|
370
|
+
const evTrunc = truncate(it.evidence, 48);
|
|
371
|
+
lines.push(' ' + boxRow(4 + evTrunc.length, c.muted(' → ') + c.muted(evTrunc)));
|
|
357
372
|
}
|
|
358
373
|
}
|
|
359
374
|
lines.push(' ' + boxBottom());
|