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.
Files changed (2) hide show
  1. package/dist/lib/render.js +142 -127
  2. package/package.json +1 -1
@@ -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
- "11111",
29
- "10001",
30
- "10001",
31
- "10001",
32
- "10001",
33
- "10001",
34
- "11111",
34
+ " ██████╗ ",
35
+ " ██╔═████╗",
36
+ " ██║██╔██║",
37
+ " ████╔╝██║",
38
+ " ╚██████╔╝",
39
+ " ╚═════╝ ",
35
40
  ],
36
41
  "1": [
37
- "00100",
38
- "01100",
39
- "00100",
40
- "00100",
41
- "00100",
42
- "00100",
43
- "01110",
42
+ " ██╗",
43
+ " ███║",
44
+ " ╚██║",
45
+ " ██║",
46
+ " ██║",
47
+ " ╚═╝",
44
48
  ],
45
49
  "2": [
46
- "11111",
47
- "00001",
48
- "00001",
49
- "11111",
50
- "10000",
51
- "10000",
52
- "11111",
50
+ " ██████╗ ",
51
+ " ╚════██╗",
52
+ " █████╔╝",
53
+ " ██╔═══╝ ",
54
+ " ███████╗",
55
+ " ╚══════╝",
53
56
  ],
54
57
  "3": [
55
- "11111",
56
- "00001",
57
- "00001",
58
- "11111",
59
- "00001",
60
- "00001",
61
- "11111",
58
+ " ██████╗ ",
59
+ " ╚════██╗",
60
+ " █████╔╝",
61
+ " ╚═══██╗",
62
+ " ██████╔╝",
63
+ " ╚═════╝ ",
62
64
  ],
63
65
  "4": [
64
- "10001",
65
- "10001",
66
- "10001",
67
- "11111",
68
- "00001",
69
- "00001",
70
- "00001",
66
+ " ██╗ ██╗",
67
+ " ██║ ██║",
68
+ " ███████║",
69
+ " ╚════██║",
70
+ " ██║",
71
+ " ╚═╝",
71
72
  ],
72
73
  "5": [
73
- "11111",
74
- "10000",
75
- "10000",
76
- "11111",
77
- "00001",
78
- "00001",
79
- "11111",
74
+ " ███████╗",
75
+ " ██╔════╝",
76
+ " ███████╗",
77
+ " ╚════██║",
78
+ " ███████║",
79
+ " ╚══════╝",
80
80
  ],
81
81
  "6": [
82
- "11111",
83
- "10000",
84
- "10000",
85
- "11111",
86
- "10001",
87
- "10001",
88
- "11111",
82
+ " ██████╗ ",
83
+ " ██╔════╝ ",
84
+ " ███████╗ ",
85
+ " ██╔═══██╗",
86
+ " ╚██████╔╝",
87
+ " ╚═════╝ ",
89
88
  ],
90
89
  "7": [
91
- "11111",
92
- "00001",
93
- "00001",
94
- "00001",
95
- "00001",
96
- "00001",
97
- "00001",
90
+ " ███████╗",
91
+ " ╚════██║",
92
+ " ██╔╝",
93
+ " ██╔╝ ",
94
+ " ██║ ",
95
+ " ╚═╝ ",
98
96
  ],
99
97
  "8": [
100
- "11111",
101
- "10001",
102
- "10001",
103
- "11111",
104
- "10001",
105
- "10001",
106
- "11111",
98
+ " █████╗ ",
99
+ " ██╔══██╗",
100
+ " ╚█████╔╝",
101
+ " ██╔══██╗",
102
+ " ╚█████╔╝",
103
+ " ╚════╝ ",
107
104
  ],
108
105
  "9": [
109
- "11111",
110
- "10001",
111
- "10001",
112
- "11111",
113
- "00001",
114
- "00001",
115
- "11111",
106
+ " █████╗ ",
107
+ " ██╔══██╗",
108
+ " ╚██████║",
109
+ " ╚═══██║",
110
+ " █████╔╝",
111
+ " ╚════╝ ",
116
112
  ],
117
113
  "/": [
118
- "00001",
119
- "00001",
120
- "00010",
121
- "00100",
122
- "01000",
123
- "10000",
124
- "10000",
114
+ " ██╗",
115
+ " ██╔╝",
116
+ " ██╔╝ ",
117
+ " ██╔╝ ",
118
+ " ██╔╝ ",
119
+ " ╚═╝ ",
125
120
  ],
126
121
  " ": [
127
- "00000",
128
- "00000",
129
- "00000",
130
- "00000",
131
- "00000",
132
- "00000",
133
- "00000",
122
+ " ",
123
+ " ",
124
+ " ",
125
+ " ",
126
+ " ",
127
+ " ",
134
128
  ],
135
129
  };
136
- const BIG_ROWS = 7;
137
- const PIXEL_FILL = ""; // 1 char per pixel · halves width vs ██ so 3-digit scores stay inside narrow PC grid columns
138
- const PIXEL_BLANK = " ";
139
- const PIXEL_GAP = " "; // visible seam between pixels the "block" feel
140
- const DIGIT_GAP = " "; // wider gap between digits so they read as separate numerals
141
- function digitToRows(d) {
142
- const m = BIG_DIGITS[d] ?? BIG_DIGITS[" "];
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 chars = text.split("");
149
- for (let i = 0; i < chars.length; i++) {
150
- const glyph = digitToRows(chars[i]);
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 < chars.length - 1 ? DIGIT_GAP : "");
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 · big-digit ASCII for X-share screenshots.
275
- // Now positioned AFTER concerns/strengths · the score is the receipt
276
- // for the findings above, not the lead. Always brand gold for cohesive
277
- // wordmark + score brand mark.
278
- const bigRows = bigText(String(total));
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 capPad = Math.floor((58 - captionVisible.length) / 2);
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
- const evTrunc = truncate(it.evidence, 50);
356
- lines.push(' ' + boxRow(2 + evTrunc.length, c.muted(' → ') + c.muted(evTrunc)));
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.3.9",
3
+ "version": "0.3.12",
4
4
  "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {