commitshow 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,12 +1,15 @@
1
- # commitshow
1
+ # commit.show CLI
2
2
 
3
3
  > Audit any vibe-coded project from your terminal.
4
4
 
5
- `commitshow` pulls the latest [commit.show](https://commit.show) audit report for
6
- a project and renders it inline Audit / Scout / Community scores, 3 strengths
7
- + 2 concerns, current season rank, delta since the last snapshot. Local runs
8
- also save a `.commitshow/audit.md` file so your AI coding agent can read the
9
- report in the next turn and iterate.
5
+ The official CLI for **[commit.show](https://commit.show)** pulls the latest
6
+ audit report for a project and renders it inline. Score breakdown
7
+ (Audit / Scout / Community), 3 strengths + 2 concerns, current season rank,
8
+ delta since the last snapshot. Local runs also save a `.commitshow/audit.md`
9
+ file so your AI coding agent can read the report in the next turn and iterate.
10
+
11
+ The npm package + command is `commitshow` (no dot — npm doesn't allow it in
12
+ package names). Everything else uses the brand `commit.show`.
10
13
 
11
14
  ```bash
12
15
  npx commitshow audit
@@ -1,6 +1,6 @@
1
1
  import { resolveTarget, TargetError } from '../lib/target.js';
2
2
  import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding, runPreviewAudit, waitForPreviewSnapshot, } from '../lib/api.js';
3
- import { renderAudit, renderMarkdown, renderJson, renderUpsell, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
3
+ import { renderAudit, renderMarkdown, renderJson, renderUpsell, renderQuotaFooter, renderRateLimitDeny, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
4
4
  import { c } from '../lib/colors.js';
5
5
  export async function audit(args) {
6
6
  const asJson = args.includes('--json');
@@ -60,7 +60,28 @@ export async function audit(args) {
60
60
  if ('error' in result) {
61
61
  const err = result;
62
62
  if (err.error === 'rate_limited') {
63
- emitError(asJson, 'rate_limited', err.message ?? 'Rate limit hit. Try again tomorrow or sign in.', target.github_url);
63
+ if (asJson) {
64
+ process.stdout.write(JSON.stringify({
65
+ error: 'rate_limited',
66
+ reason: err.reason,
67
+ message: err.message,
68
+ limit: err.limit,
69
+ count: err.count,
70
+ quota: err.quota,
71
+ target: target.github_url,
72
+ }) + '\n');
73
+ }
74
+ else {
75
+ console.error('');
76
+ console.error(renderRateLimitDeny({
77
+ reason: err.reason ?? 'ip_cap',
78
+ message: err.message ?? 'Rate limit hit. Try again later.',
79
+ limit: err.limit ?? 0,
80
+ count: err.count ?? 0,
81
+ quota: err.quota,
82
+ }));
83
+ console.error('');
84
+ }
64
85
  return 1;
65
86
  }
66
87
  emitError(asJson, err.error, err.message ?? 'Preview audit failed.', target.github_url);
@@ -77,19 +98,30 @@ export async function audit(args) {
77
98
  emitError(asJson, 'timeout', 'Preview audit is taking longer than expected. Try `commitshow status <repo>` in a minute.', target.github_url);
78
99
  return 1;
79
100
  }
80
- envelope = waited;
101
+ // Carry the original quota from the first response — server doesn't re-issue
102
+ // one when we poll for the snapshot.
103
+ envelope = { ...waited, quota: pending.quota };
81
104
  }
82
105
  else {
83
106
  envelope = result;
84
107
  }
85
108
  const view = { project: envelope.project, snapshot: envelope.snapshot, standing: null };
86
109
  if (asJson) {
87
- process.stdout.write(renderJson(view) + '\n');
110
+ // Inject quota into the v1 schema as an additive field — schema_version
111
+ // unchanged because additive-only fields don't bump it.
112
+ const shape = JSON.parse(renderJson(view));
113
+ if (envelope.quota)
114
+ shape.quota = envelope.quota;
115
+ process.stdout.write(JSON.stringify(shape, null, 2) + '\n');
88
116
  }
89
117
  else {
90
118
  console.log('');
91
119
  console.log(renderAudit(view));
92
120
  console.log('');
121
+ if (envelope.quota) {
122
+ console.log(renderQuotaFooter(envelope.quota));
123
+ console.log('');
124
+ }
93
125
  console.log(renderUpsell());
94
126
  console.log('');
95
127
  }
package/dist/index.js CHANGED
@@ -7,10 +7,10 @@ import { whoami } from './commands/whoami.js';
7
7
  import { c } from './lib/colors.js';
8
8
  const VERSION = '0.1.0';
9
9
  const USAGE = `
10
- ${c.bold(c.gold('commitshow'))} ${c.dim(`v${VERSION}`)} ${c.muted('—')} ${c.cream('audit any vibe-coded project from your terminal.')}
10
+ ${c.bold(c.gold('commit.show'))} ${c.dim(`v${VERSION}`)} ${c.muted('—')} ${c.cream('audit any vibe-coded project from your terminal.')}
11
11
 
12
12
  ${c.muted('USAGE')}
13
- ${c.cream('commitshow')} ${c.gold('<command>')} [target] [flags]
13
+ ${c.cream('commitshow')} ${c.gold('<command>')} [target] [flags] ${c.dim('# CLI is `commitshow` (no dot — npm constraint)')}
14
14
 
15
15
  ${c.muted('COMMANDS')}
16
16
  ${c.gold('audit')} [target] run audit and render the report
@@ -23,6 +23,33 @@ 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
+ // 5-row × 5-col ASCII digit set · used for the hero score.
27
+ // Hand-rolled (no external font dep) so the bundle stays tiny.
28
+ const BIG_DIGITS = {
29
+ '0': ['█▀▀▀█', '█ █', '█ █', '█ █', '█▄▄▄█'],
30
+ '1': [' ▄█ ', ' █ ', ' █ ', ' █ ', ' ▄█▄'],
31
+ '2': ['█▀▀▀█', ' █', '█▀▀▀▀', '█ ', '█▄▄▄▄'],
32
+ '3': ['█▀▀▀█', ' █', ' ▀▀▀█', ' █', '█▄▄▄█'],
33
+ '4': ['█ █', '█ █', '█▄▄▄█', ' █', ' █'],
34
+ '5': ['█▀▀▀▀', '█ ', '▀▀▀▀█', ' █', '█▄▄▄█'],
35
+ '6': ['█▀▀▀▀', '█ ', '█▀▀▀█', '█ █', '█▄▄▄█'],
36
+ '7': ['█▀▀▀█', ' █', ' ▄▀', ' ▄▀ ', ' ▄▀ '],
37
+ '8': ['█▀▀▀█', '█ █', '█▀▀▀█', '█ █', '█▄▄▄█'],
38
+ '9': ['█▀▀▀█', '█ █', '█▄▄▄█', ' █', '█▄▄▄█'],
39
+ '/': [' █', ' ▄▀', ' ▄▀ ', ' ▄▀ ', '█ '],
40
+ ' ': [' ', ' ', ' ', ' ', ' '],
41
+ };
42
+ /** Render a string ("68", "100", "82/100") as 5 rows of big ASCII. */
43
+ function bigText(text) {
44
+ const rows = ['', '', '', '', ''];
45
+ for (let i = 0; i < text.length; i++) {
46
+ const ch = text[i];
47
+ const glyph = BIG_DIGITS[ch] ?? BIG_DIGITS[' '];
48
+ for (let r = 0; r < 5; r++)
49
+ rows[r] += glyph[r] + (i < text.length - 1 ? ' ' : '');
50
+ }
51
+ return rows;
52
+ }
26
53
  function pad(s, w) {
27
54
  return s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);
28
55
  }
@@ -66,13 +93,20 @@ export function renderAudit(view) {
66
93
  const slug = p.github_url?.replace(/^https?:\/\//, '') ?? '';
67
94
  lines.push(' ' + c.bold(c.cream(name)) + ' ' + c.muted(slug));
68
95
  lines.push('');
69
- // Hero score card
70
- const scoreText = `${total} / 100`;
71
- const scoreTinted = scoreTone(total)(scoreText);
72
- const cardW = 14;
73
- lines.push(' ' + ' '.repeat(20) + c.muted('╔' + '═'.repeat(cardW) + '╗'));
74
- lines.push(' ' + ' '.repeat(20) + c.muted('║') + centerPadAnsi(scoreTinted, cardW) + c.muted('║'));
75
- lines.push(' ' + ' '.repeat(20) + c.muted('╚' + '═'.repeat(cardW) + '╝'));
96
+ // Hero score · big-digit ASCII for X-share screenshots.
97
+ // Renders the score number 5 rows tall, color-coded by tier band.
98
+ // Width budget: 3-digit (e.g. "100") = 17 cols → centered in 58.
99
+ const tone = scoreTone(total);
100
+ const bigRows = bigText(String(total));
101
+ const bigWidth = bigRows[0].length;
102
+ const leftPad = Math.floor((58 - bigWidth) / 2);
103
+ for (const row of bigRows) {
104
+ lines.push(' ' + ' '.repeat(leftPad) + tone(row));
105
+ }
106
+ // Caption beneath the big number — small "/ 100 · band"
107
+ const band = total >= 75 ? 'strong' : total >= 50 ? 'mid' : 'weak';
108
+ const caption = `/ 100 · ${band}`;
109
+ lines.push(' ' + ' '.repeat(Math.floor((58 - caption.length) / 2)) + c.muted(caption));
76
110
  lines.push('');
77
111
  // 3-axis bars
78
112
  const auditLine = ` Audit ${pad(`${p.score_auto}/50`, 7)} ${scoreBar(p.score_auto, 50)}`;
@@ -264,10 +298,89 @@ export function toAgentShape(view) {
264
298
  export function renderJson(view) {
265
299
  return JSON.stringify(toAgentShape(view), null, 2);
266
300
  }
267
- // ── Upsell panel (CLI-only · appended to preview audits) ─────────────
268
- // Shown after a preview audit render so Creator sees what a real audition
269
- // unlocks. Intentionally NOT shown for registered projects — they already
270
- // have access to everything listed here.
301
+ function timeUntil(isoTarget) {
302
+ const ms = Math.max(0, new Date(isoTarget).getTime() - Date.now());
303
+ const h = Math.floor(ms / 3_600_000);
304
+ const m = Math.floor((ms % 3_600_000) / 60_000);
305
+ if (h >= 24)
306
+ return `${Math.floor(h / 24)}d ${h % 24}h`;
307
+ if (h > 0)
308
+ return `${h}h ${m}m`;
309
+ return `${m}m`;
310
+ }
311
+ export function renderQuotaFooter(q) {
312
+ // Pick the tier closest to its cap so the user sees the most relevant pressure.
313
+ const tiers = [
314
+ { name: 'IP', count: q.ip.count, limit: q.ip.limit, remaining: q.ip.remaining },
315
+ { name: 'repo', count: q.url.count, limit: q.url.limit, remaining: q.url.remaining },
316
+ { name: 'global', count: q.global.count, limit: q.global.limit, remaining: q.global.remaining },
317
+ ];
318
+ const tightest = tiers.slice().sort((a, b) => a.remaining - b.remaining)[0];
319
+ const tone = tightest.remaining === 0 ? c.scarlet :
320
+ tightest.remaining <= 1 ? c.gold :
321
+ c.muted;
322
+ const reset = timeUntil(q.reset_at);
323
+ const ipPart = `IP ${q.ip.remaining}/${q.ip.limit}`;
324
+ const urlPart = `repo ${q.url.remaining}/${q.url.limit}`;
325
+ return ' ' + c.muted('quota: ') +
326
+ tone(ipPart) + c.muted(' · ') +
327
+ tone(urlPart) + c.muted(' · ') +
328
+ c.dim(`resets in ${reset}`);
329
+ }
330
+ // ── Rate-limit panel (deny path) ────────────────────────────────
331
+ // Replaces the bare error line. Shows which tier was hit, count vs cap,
332
+ // time until reset, and what to do next.
333
+ const REASON_LABEL = {
334
+ ip_cap: 'Daily limit hit · per IP',
335
+ url_cap: 'This repo audited too many times today',
336
+ global_cap: 'commit.show daily audit cap reached',
337
+ };
338
+ function bar(filled, total, width = 20) {
339
+ const f = Math.max(0, Math.min(width, Math.round((filled / Math.max(1, total)) * width)));
340
+ return c.scarlet('▰'.repeat(f)) + c.muted('▱'.repeat(width - f));
341
+ }
342
+ export function renderRateLimitDeny(opts) {
343
+ const lines = [];
344
+ const horiz = '─'.repeat(58);
345
+ lines.push(' ' + c.muted('┌' + horiz + '┐'));
346
+ lines.push(' ' + c.muted('│ ') + c.bold(c.scarlet('Rate limit')) + c.muted(' · ') + c.cream(REASON_LABEL[opts.reason] ?? opts.reason) + ' '.repeat(Math.max(0, 58 - 14 - (REASON_LABEL[opts.reason]?.length ?? opts.reason.length))) + c.muted('│'));
347
+ lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
348
+ lines.push(' ' + c.muted('│ ') + c.cream(`${opts.count}/${opts.limit} `) + bar(opts.count, opts.limit) + ' '.repeat(58 - 28) + c.muted('│'));
349
+ if (opts.quota) {
350
+ const reset = timeUntil(opts.quota.reset_at);
351
+ lines.push(' ' + c.muted('│ ') + c.dim(`resets in ${reset}`) + ' '.repeat(58 - 12 - reset.length - 9 - 2) + c.muted('│'));
352
+ }
353
+ // Wrap the message into ~54-char lines.
354
+ for (const w of wrapText(opts.message, 54)) {
355
+ lines.push(' ' + c.muted('│ ') + c.cream(w) + ' '.repeat(56 - w.length) + c.muted('│'));
356
+ }
357
+ if (opts.reason === 'url_cap') {
358
+ lines.push(' ' + c.muted('│ ') + c.dim('Tip: cached audit (< 7d) is free — `commitshow status <repo>`.') + c.muted(' │'));
359
+ }
360
+ if (opts.reason === 'ip_cap' && opts.quota?.ip.tier === 'anon') {
361
+ lines.push(' ' + c.muted('│ ') + c.dim('Sign in (commit.show) for a higher daily cap.') + ' '.repeat(58 - 49) + c.muted('│'));
362
+ }
363
+ lines.push(' ' + c.muted('└' + horiz + '┘'));
364
+ return lines.join('\n');
365
+ }
366
+ function wrapText(s, width) {
367
+ const words = s.split(/\s+/);
368
+ const out = [];
369
+ let line = '';
370
+ for (const w of words) {
371
+ if ((line + ' ' + w).trim().length > width) {
372
+ if (line)
373
+ out.push(line.trim());
374
+ line = w;
375
+ }
376
+ else {
377
+ line += ' ' + w;
378
+ }
379
+ }
380
+ if (line.trim())
381
+ out.push(line.trim());
382
+ return out;
383
+ }
271
384
  export function renderUpsell() {
272
385
  const lines = [];
273
386
  const bar = '─'.repeat(58);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {