commitshow 0.1.3 → 0.1.5

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.
@@ -4,6 +4,7 @@ import { renderAudit, renderMarkdown, renderJson, renderUpsell, renderQuotaFoote
4
4
  import { c } from '../lib/colors.js';
5
5
  export async function audit(args) {
6
6
  const asJson = args.includes('--json');
7
+ const force = args.includes('--refresh') || args.includes('--force');
7
8
  const positional = args.find(a => !a.startsWith('--'));
8
9
  let target;
9
10
  try {
@@ -16,11 +17,17 @@ export async function audit(args) {
16
17
  }
17
18
  throw err;
18
19
  }
19
- if (!asJson)
20
- console.log(c.dim(`Auditing ${target.slug}…`));
20
+ if (!asJson) {
21
+ if (force)
22
+ console.log(c.dim(`Refreshing audit for ${target.slug}…`));
23
+ else
24
+ console.log(c.dim(`Auditing ${target.slug}…`));
25
+ }
21
26
  // 1. Try cached/registered flow first — avoid re-running Claude if we
22
27
  // already have the snapshot. Covers all full-audition projects.
23
- const project = await findProjectByGithubUrl(target.github_url);
28
+ // --refresh / --force skips this entirely and goes straight to audit-preview
29
+ // with force=true (counts against IP + URL + global rate limits).
30
+ const project = force ? null : await findProjectByGithubUrl(target.github_url);
24
31
  if (project) {
25
32
  const [snapshot, standing] = await Promise.all([
26
33
  fetchLatestSnapshot(project.id),
@@ -77,9 +84,13 @@ export async function audit(args) {
77
84
  }
78
85
  // 2. Unregistered repo — kick off a preview audit. Full Claude depth,
79
86
  // no season entry. Rate-limited server-side.
80
- if (!asJson)
81
- console.log(c.dim('First time on commit.show for this repo — running a preview audit…'));
82
- const result = await runPreviewAudit(target.github_url);
87
+ if (!asJson) {
88
+ if (force)
89
+ console.log(c.dim('Forcing a fresh audit · counts against your daily IP cap…'));
90
+ else
91
+ console.log(c.dim('First time on commit.show for this repo — running a preview audit…'));
92
+ }
93
+ const result = await runPreviewAudit(target.github_url, undefined, { force });
83
94
  // Error envelope
84
95
  if ('error' in result) {
85
96
  const err = result;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { status } from './commands/status.js';
5
5
  import { login } from './commands/login.js';
6
6
  import { whoami } from './commands/whoami.js';
7
7
  import { c } from './lib/colors.js';
8
- const VERSION = '0.1.0';
8
+ const VERSION = '0.1.5';
9
9
  const USAGE = `
10
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
 
@@ -22,6 +22,7 @@ ${c.muted('COMMANDS')}
22
22
 
23
23
  ${c.muted('FLAGS')}
24
24
  ${c.gold('--json')} stable machine-readable output (for agents · CI · jq pipes)
25
+ ${c.gold('--refresh')} bypass the 7-day cache · re-run a fresh audit ${c.dim('(counts against IP cap)')}
25
26
 
26
27
  ${c.muted('TARGET FORMS')} ${c.dim('(default: cwd)')}
27
28
  ${c.cream('commitshow audit')} ${c.dim('# cwd · git remote origin')}
package/dist/lib/api.js CHANGED
@@ -61,12 +61,14 @@ export async function fetchStanding(projectId) {
61
61
  const rows = await rest(`/season_standings?project_id=eq.${projectId}&limit=1`);
62
62
  return rows[0] ?? null;
63
63
  }
64
- /** Kicks off (or returns cached) a preview audit. 202 → poll; 200 → done. */
65
- export async function runPreviewAudit(githubUrl, liveUrl) {
64
+ /** Kicks off (or returns cached) a preview audit. 202 → poll; 200 → done.
65
+ * force=true skips the 7-day cache and forces a fresh analyze-project run.
66
+ * Counts against IP + URL + global rate limits (real Claude spend). */
67
+ export async function runPreviewAudit(githubUrl, liveUrl, opts = {}) {
66
68
  const res = await fetch(`${baseUrl()}/functions/v1/audit-preview`, {
67
69
  method: 'POST',
68
70
  headers: headers(),
69
- body: JSON.stringify({ github_url: githubUrl, live_url: liveUrl }),
71
+ body: JSON.stringify({ github_url: githubUrl, live_url: liveUrl, force: opts.force === true }),
70
72
  });
71
73
  const body = await res.json().catch(() => ({ error: 'invalid_json' }));
72
74
  if (res.status === 202)
@@ -60,6 +60,27 @@ function centerPad(s, w) {
60
60
  const left = Math.floor(total / 2);
61
61
  return ' '.repeat(left) + s + ' '.repeat(total - left);
62
62
  }
63
+ // ── Single source of truth for box drawing ─────────────────────
64
+ // Every panel uses 58-char outer width (1 corner + 56 interior + 1 corner)
65
+ // so screenshots line up. Helper takes the *visible* length so colored
66
+ // content (multiple ANSI spans) renders at the right padding without
67
+ // having to strip escape codes at runtime.
68
+ const BOX_W = 58; // outer width including both corners
69
+ const INSIDE_W = BOX_W - 2; // chars between │ and │
70
+ const CONTENT_W = INSIDE_W - 2; // chars between '│ ' and ' │'
71
+ const boxTop = () => c.muted('┌' + '─'.repeat(INSIDE_W) + '┐');
72
+ const boxBottom = () => c.muted('└' + '─'.repeat(INSIDE_W) + '┘');
73
+ const boxBlank = () => c.muted('│' + ' '.repeat(INSIDE_W) + '│');
74
+ /**
75
+ * Render a content row inside the box with proper padding.
76
+ * @param visibleLen number of visible chars in `colored` (for padding math)
77
+ * @param colored the rendered string (may contain ANSI escapes)
78
+ * @param leftMargin extra spaces inside the box, after the leading `│ `
79
+ */
80
+ function boxRow(visibleLen, colored, leftMargin = 0) {
81
+ const padding = Math.max(0, CONTENT_W - leftMargin - visibleLen);
82
+ return c.muted('│ ') + ' '.repeat(leftMargin) + colored + ' '.repeat(padding) + c.muted(' │');
83
+ }
63
84
  /** Strip ANSI for width math on colored strings. */
64
85
  function visibleLength(s) {
65
86
  // eslint-disable-next-line no-control-regex
@@ -82,11 +103,11 @@ export function renderAudit(view) {
82
103
  const { project: p, snapshot, standing } = view;
83
104
  const total = p.score_total ?? 0;
84
105
  // Header
85
- const bar = '─'.repeat(58);
86
106
  const lines = [];
87
- lines.push(c.muted('┌' + bar + '┐'));
88
- lines.push(c.muted('│ ') + c.bold(c.gold('commit.show')) + c.muted(' · ') + c.cream('Audit report') + ' '.repeat(58 - 29) + c.muted('│'));
89
- lines.push(c.muted('' + bar + ''));
107
+ lines.push(boxTop());
108
+ lines.push(boxRow(
109
+ /* visibleLen */ 'commit.show · Audit report'.length, c.bold(c.gold('commit.show')) + c.muted(' · ') + c.cream('Audit report')));
110
+ lines.push(boxBottom());
90
111
  lines.push('');
91
112
  // Project title line
92
113
  const name = p.project_name ?? 'untitled';
@@ -124,14 +145,20 @@ export function renderAudit(view) {
124
145
  const strengths = asStringArray(snapshot?.rich_analysis?.scout_brief?.strengths, 3);
125
146
  const concerns = asStringArray(snapshot?.rich_analysis?.scout_brief?.weaknesses, 2);
126
147
  if (strengths.length > 0 || concerns.length > 0) {
127
- lines.push(' ' + c.muted('┌' + '─'.repeat(56) + '┐'));
148
+ // strengths/concerns each render as `↑ ` (2 visible) + truncated line.
149
+ // Total visible-line budget inside the box is CONTENT_W chars; reserve
150
+ // 2 for the arrow + space, leaving CONTENT_W - 2 for the bullet text.
151
+ const bulletWidth = CONTENT_W - 2;
152
+ lines.push(' ' + boxTop());
128
153
  for (const s of strengths) {
129
- lines.push(' ' + c.muted('│ ') + c.teal('↑ ') + truncate(s, 52) + fill(s, 52) + c.muted(' │'));
154
+ const txt = truncate(s, bulletWidth);
155
+ lines.push(' ' + boxRow(2 + txt.length, c.teal('↑ ') + c.cream(txt)));
130
156
  }
131
157
  for (const s of concerns) {
132
- lines.push(' ' + c.muted('│ ') + c.scarlet('↓ ') + truncate(s, 52) + fill(s, 52) + c.muted(' │'));
158
+ const txt = truncate(s, bulletWidth);
159
+ lines.push(' ' + boxRow(2 + txt.length, c.scarlet('↓ ') + c.cream(txt)));
133
160
  }
134
- lines.push(' ' + c.muted('└' + '─'.repeat(56) + '┘'));
161
+ lines.push(' ' + boxBottom());
135
162
  lines.push('');
136
163
  }
137
164
  // Standings + delta
@@ -339,29 +366,32 @@ export function renderAuditError(err, projectName, projectUrl) {
339
366
  const label = AUDIT_ERROR_LABEL[err.type] ?? AUDIT_ERROR_LABEL.anthropic_other;
340
367
  const detail = AUDIT_ERROR_DETAIL[err.type] ?? AUDIT_ERROR_DETAIL.anthropic_other;
341
368
  const lines = [];
342
- const horiz = '─'.repeat(58);
343
- lines.push(' ' + c.muted('┌' + horiz + '┐'));
344
- lines.push(' ' + c.muted('│ ') + c.bold(c.gold('commit.show')) + c.muted(' · ') + c.scarlet(label) + ' '.repeat(Math.max(0, 58 - 14 - label.length)) + c.muted('│'));
345
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
369
+ const titleVisible = `commit.show · ${label}`;
370
+ lines.push(' ' + boxTop());
371
+ lines.push(' ' + boxRow(titleVisible.length, c.bold(c.gold('commit.show')) + c.muted(' · ') + c.scarlet(label)));
372
+ lines.push(' ' + boxBlank());
346
373
  if (projectName) {
347
- lines.push(' ' + c.muted('│ ') + c.cream(`Repo: ${projectName}`) + ' '.repeat(Math.max(0, 56 - 6 - projectName.length)) + c.muted('│'));
348
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
374
+ const repoLine = `Repo: ${projectName}`;
375
+ lines.push(' ' + boxRow(repoLine.length, c.cream(repoLine)));
376
+ lines.push(' ' + boxBlank());
349
377
  }
350
- for (const w of wrapText(detail, 54)) {
351
- lines.push(' ' + c.muted('│ ') + c.cream(w) + ' '.repeat(Math.max(0, 56 - w.length)) + c.muted('│'));
378
+ for (const w of wrapText(detail, CONTENT_W)) {
379
+ lines.push(' ' + boxRow(w.length, c.cream(w)));
352
380
  }
353
381
  if (err.retry_after_seconds && err.retry_after_seconds > 0) {
354
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
382
+ lines.push(' ' + boxBlank());
355
383
  const t = `Retry after ~${untilHuman(err.retry_after_seconds)}`;
356
- lines.push(' ' + c.muted('│ ') + c.dim(t) + ' '.repeat(Math.max(0, 56 - t.length)) + c.muted('│'));
384
+ lines.push(' ' + boxRow(t.length, c.dim(t)));
357
385
  }
358
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
386
+ lines.push(' ' + boxBlank());
359
387
  const statusLine = `Status check: commitshow status ${projectName ?? '<repo>'}`;
360
- lines.push(' ' + c.muted('│ ') + c.dim(statusLine) + ' '.repeat(Math.max(0, 56 - statusLine.length)) + c.muted('│'));
388
+ lines.push(' ' + boxRow(Math.min(statusLine.length, CONTENT_W), c.dim(statusLine.slice(0, CONTENT_W))));
361
389
  if (projectUrl) {
362
- lines.push(' ' + c.muted('│ ') + c.dim('Web view: ') + c.cream(projectUrl.length > 44 ? projectUrl.slice(0, 41) + '…' : projectUrl) + ' '.repeat(Math.max(0, 46 - Math.min(44, projectUrl.length))) + c.muted('│'));
390
+ const urlMax = CONTENT_W - 'Web view: '.length;
391
+ const urlText = projectUrl.length > urlMax ? projectUrl.slice(0, urlMax - 1) + '…' : projectUrl;
392
+ lines.push(' ' + boxRow('Web view: '.length + urlText.length, c.dim('Web view: ') + c.cream(urlText)));
363
393
  }
364
- lines.push(' ' + c.muted('└' + horiz + '┘'));
394
+ lines.push(' ' + boxBottom());
365
395
  return lines.join('\n');
366
396
  }
367
397
  function timeUntil(isoTarget) {
@@ -407,26 +437,30 @@ function bar(filled, total, width = 20) {
407
437
  }
408
438
  export function renderRateLimitDeny(opts) {
409
439
  const lines = [];
410
- const horiz = '─'.repeat(58);
411
- lines.push(' ' + c.muted('┌' + horiz + '┐'));
412
- 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('│'));
413
- lines.push(' ' + c.muted('' + ' '.repeat(58) + '│'));
414
- lines.push(' ' + c.muted('│ ') + c.cream(`${opts.count}/${opts.limit} `) + bar(opts.count, opts.limit) + ' '.repeat(58 - 28) + c.muted('│'));
440
+ const reasonLabel = REASON_LABEL[opts.reason] ?? opts.reason;
441
+ const titleVisible = `Rate limit · ${reasonLabel}`;
442
+ lines.push(' ' + boxTop());
443
+ lines.push(' ' + boxRow(titleVisible.length, c.bold(c.scarlet('Rate limit')) + c.muted(' · ') + c.cream(reasonLabel)));
444
+ lines.push(' ' + boxBlank());
445
+ // Count + bar row · "5/5 " (5 chars) + 20-char bar = 25 visible
446
+ const counter = `${opts.count}/${opts.limit} `;
447
+ lines.push(' ' + boxRow(counter.length + 20, c.cream(counter) + bar(opts.count, opts.limit)));
415
448
  if (opts.quota) {
416
- const reset = timeUntil(opts.quota.reset_at);
417
- lines.push(' ' + c.muted('│ ') + c.dim(`resets in ${reset}`) + ' '.repeat(58 - 12 - reset.length - 9 - 2) + c.muted('│'));
449
+ const reset = `resets in ${timeUntil(opts.quota.reset_at)}`;
450
+ lines.push(' ' + boxRow(reset.length, c.dim(reset)));
418
451
  }
419
- // Wrap the message into ~54-char lines.
420
- for (const w of wrapText(opts.message, 54)) {
421
- lines.push(' ' + c.muted('│ ') + c.cream(w) + ' '.repeat(56 - w.length) + c.muted('│'));
452
+ for (const w of wrapText(opts.message, CONTENT_W)) {
453
+ lines.push(' ' + boxRow(w.length, c.cream(w)));
422
454
  }
423
455
  if (opts.reason === 'url_cap') {
424
- lines.push(' ' + c.muted('│ ') + c.dim('Tip: cached audit (< 7d) is free — `commitshow status <repo>`.') + c.muted(' │'));
456
+ const tip = 'Tip: cached audit (< 7d) is free — commitshow status <repo>';
457
+ lines.push(' ' + boxRow(tip.length, c.dim(tip)));
425
458
  }
426
459
  if (opts.reason === 'ip_cap' && opts.quota?.ip.tier === 'anon') {
427
- lines.push(' ' + c.muted('│ ') + c.dim('Sign in (commit.show) for a higher daily cap.') + ' '.repeat(58 - 49) + c.muted('│'));
460
+ const tip = 'Sign in (commit.show) for a higher daily cap.';
461
+ lines.push(' ' + boxRow(tip.length, c.dim(tip)));
428
462
  }
429
- lines.push(' ' + c.muted('└' + horiz + '┘'));
463
+ lines.push(' ' + boxBottom());
430
464
  return lines.join('\n');
431
465
  }
432
466
  function wrapText(s, width) {
@@ -449,24 +483,35 @@ function wrapText(s, width) {
449
483
  }
450
484
  export function renderUpsell() {
451
485
  const lines = [];
452
- const bar = ''.repeat(58);
453
- lines.push(' ' + c.muted('┌' + bar + '┐'));
454
- lines.push(' ' + c.muted('│ ') + c.bold(c.gold('Preview')) + c.muted(' · ') + c.cream('not entered in the season') + ' '.repeat(58 - 35) + c.muted('│'));
455
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
456
- lines.push(' ' + c.muted('') + c.cream('Audition to unlock:') + ' '.repeat(58 - 20) + c.muted('│'));
486
+ const titleVisible = 'Preview · not entered in the season';
487
+ const headVisible = 'Audition to unlock:';
488
+ const ctaVisible = ' https://commit.show/submit';
489
+ lines.push(' ' + boxTop());
490
+ lines.push(' ' + boxRow(titleVisible.length, c.bold(c.gold('Preview')) + c.muted(' · ') + c.cream('not entered in the season')));
491
+ lines.push(' ' + boxBlank());
492
+ lines.push(' ' + boxRow(headVisible.length, c.cream(headVisible)));
493
+ // Backstage = our brand for the prompt-extraction analysis (Phase 2 brief).
494
+ // Lead the unlock list with it because it's the most concrete, immediate
495
+ // payoff on signup: see how the project was built (delegation map, failure
496
+ // log, decision archaeology) and unlock +15-20 audit points typical.
497
+ // Tags are 15-char column-aligned so the · separator lines up vertically.
498
+ // Tags column-padded to 16 chars (1 trailing space guaranteed so the · in
499
+ // `rest` always has a visual gap from the tag, even when the tag fills 15).
457
500
  const items = [
458
- 'Scout forecasts · human verdicts (30% of score)',
459
- 'Season ranking · top 20% graduate each 3-week cycle',
460
- 'Hall of Fame · permanent archive + public badge',
461
- 'Live applauds · notifications when reviewers react',
462
- 'Recommit loop · weekly delta + trajectory share card',
501
+ { tag: 'Backstage ', rest: '· build process + prompts · +15 pts', tone: c.gold },
502
+ { tag: 'Scout forecasts ', rest: '· human verdicts (30% of score)', tone: c.teal },
503
+ { tag: 'Season ranking ', rest: '· top 20% graduate per cycle', tone: c.teal },
504
+ { tag: 'Hall of Fame ', rest: '· permanent archive + badge', tone: c.teal },
505
+ { tag: 'Live applauds ', rest: '· notifications on reactions', tone: c.teal },
506
+ { tag: 'Recommit loop ', rest: '· weekly delta + share card', tone: c.teal },
463
507
  ];
464
508
  for (const it of items) {
465
- lines.push(' ' + c.muted('│ ') + c.teal('→ ') + c.cream(pad(it, 54)) + c.muted('│'));
509
+ const visible = `→ ${it.tag}${it.rest}`;
510
+ lines.push(' ' + boxRow(visible.length, it.tone('→ ') + c.cream(it.tag) + c.muted(it.rest)));
466
511
  }
467
- lines.push(' ' + c.muted('│' + ' '.repeat(58) + '│'));
468
- lines.push(' ' + c.muted('│ ') + c.gold('→ https://commit.show/submit') + ' '.repeat(58 - 30) + c.muted('│'));
469
- lines.push(' ' + c.muted('└' + bar + '┘'));
512
+ lines.push(' ' + boxBlank());
513
+ lines.push(' ' + boxRow(ctaVisible.length, c.gold(ctaVisible)));
514
+ lines.push(' ' + boxBottom());
470
515
  return lines.join('\n');
471
516
  }
472
517
  export function writeAuditJson(dir, json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {