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 +9 -6
- package/dist/commands/audit.js +36 -4
- package/dist/index.js +2 -2
- package/dist/lib/render.js +124 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# commit.show CLI
|
|
2
2
|
|
|
3
3
|
> Audit any vibe-coded project from your terminal.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
a project and renders it inline
|
|
7
|
-
+ 2 concerns, current season rank,
|
|
8
|
-
also save a `.commitshow/audit.md`
|
|
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
|
package/dist/commands/audit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
package/dist/lib/render.js
CHANGED
|
@@ -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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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);
|