commitshow 0.1.2 → 0.1.4
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/commands/audit.js +49 -1
- package/dist/lib/render.js +143 -35
- package/package.json +1 -1
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, renderQuotaFooter, renderRateLimitDeny, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
|
|
3
|
+
import { renderAudit, renderMarkdown, renderJson, renderUpsell, renderQuotaFooter, renderRateLimitDeny, renderAuditError, 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');
|
|
@@ -27,6 +27,30 @@ export async function audit(args) {
|
|
|
27
27
|
fetchStanding(project.id),
|
|
28
28
|
]);
|
|
29
29
|
const view = { project, snapshot, standing };
|
|
30
|
+
// The snapshot may carry an audit-engine error (Claude quota exceeded,
|
|
31
|
+
// rate limit, etc.). Render the friendly explanation panel and exit
|
|
32
|
+
// 2 so CI scripts can detect "engine unavailable" without conflating
|
|
33
|
+
// it with a genuine low score.
|
|
34
|
+
const auditErr = snapshot?.rich_analysis?.error;
|
|
35
|
+
if (auditErr) {
|
|
36
|
+
if (asJson) {
|
|
37
|
+
process.stdout.write(JSON.stringify({
|
|
38
|
+
error: 'audit_engine_error',
|
|
39
|
+
reason: auditErr.type,
|
|
40
|
+
message: auditErr.message ?? null,
|
|
41
|
+
retry_after: auditErr.retry_after_seconds ?? null,
|
|
42
|
+
project: { id: project.id, name: project.project_name, github_url: project.github_url },
|
|
43
|
+
}) + '\n');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error('');
|
|
47
|
+
console.error(renderAuditError({ type: auditErr.type, message: auditErr.message ?? undefined,
|
|
48
|
+
retry_after_seconds: auditErr.retry_after_seconds ?? null,
|
|
49
|
+
http_status: auditErr.http_status }, project.project_name, `https://commit.show/projects/${project.id}`));
|
|
50
|
+
console.error('');
|
|
51
|
+
}
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
30
54
|
if (asJson) {
|
|
31
55
|
process.stdout.write(renderJson(view) + '\n');
|
|
32
56
|
}
|
|
@@ -106,6 +130,30 @@ export async function audit(args) {
|
|
|
106
130
|
envelope = result;
|
|
107
131
|
}
|
|
108
132
|
const view = { project: envelope.project, snapshot: envelope.snapshot, standing: null };
|
|
133
|
+
// Same audit-engine error check as the cached path. The polled snapshot
|
|
134
|
+
// can carry a Claude failure even though the audit-preview Edge Function
|
|
135
|
+
// returned 202 (the failure happened in the background analyze-project).
|
|
136
|
+
const polledErr = envelope.snapshot?.rich_analysis?.error;
|
|
137
|
+
if (polledErr) {
|
|
138
|
+
if (asJson) {
|
|
139
|
+
process.stdout.write(JSON.stringify({
|
|
140
|
+
error: 'audit_engine_error',
|
|
141
|
+
reason: polledErr.type,
|
|
142
|
+
message: polledErr.message ?? null,
|
|
143
|
+
retry_after: polledErr.retry_after_seconds ?? null,
|
|
144
|
+
project: { id: envelope.project.id, name: envelope.project.project_name, github_url: envelope.project.github_url },
|
|
145
|
+
quota: envelope.quota,
|
|
146
|
+
}) + '\n');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error('');
|
|
150
|
+
console.error(renderAuditError({ type: polledErr.type, message: polledErr.message ?? undefined,
|
|
151
|
+
retry_after_seconds: polledErr.retry_after_seconds ?? null,
|
|
152
|
+
http_status: polledErr.http_status }, envelope.project.project_name, `https://commit.show/projects/${envelope.project.id}`));
|
|
153
|
+
console.error('');
|
|
154
|
+
}
|
|
155
|
+
return 2;
|
|
156
|
+
}
|
|
109
157
|
if (asJson) {
|
|
110
158
|
// Inject quota into the v1 schema as an additive field — schema_version
|
|
111
159
|
// unchanged because additive-only fields don't bump it.
|
package/dist/lib/render.js
CHANGED
|
@@ -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(
|
|
88
|
-
lines.push(
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
const txt = truncate(s, bulletWidth);
|
|
159
|
+
lines.push(' ' + boxRow(2 + txt.length, c.scarlet('↓ ') + c.cream(txt)));
|
|
133
160
|
}
|
|
134
|
-
lines.push(' ' +
|
|
161
|
+
lines.push(' ' + boxBottom());
|
|
135
162
|
lines.push('');
|
|
136
163
|
}
|
|
137
164
|
// Standings + delta
|
|
@@ -301,6 +328,72 @@ export function toAgentShape(view) {
|
|
|
301
328
|
export function renderJson(view) {
|
|
302
329
|
return JSON.stringify(toAgentShape(view), null, 2);
|
|
303
330
|
}
|
|
331
|
+
// ── Audit-engine error panel ────────────────────────────────────────
|
|
332
|
+
// Rendered when the snapshot exists but rich_analysis.error is set —
|
|
333
|
+
// i.e., the Claude call itself failed (quota, rate limit, network). The
|
|
334
|
+
// project's auto-50 signals may still be valid; we explain that and tell
|
|
335
|
+
// the user when fresh audits will resume.
|
|
336
|
+
const AUDIT_ERROR_LABEL = {
|
|
337
|
+
anthropic_quota_exceeded: 'Daily audit budget reached',
|
|
338
|
+
anthropic_rate_limited: 'Audit engine rate-limited',
|
|
339
|
+
anthropic_overloaded: 'Audit engine overloaded',
|
|
340
|
+
anthropic_auth_error: 'Audit engine auth issue',
|
|
341
|
+
anthropic_other: 'Audit engine error',
|
|
342
|
+
claude_returned_no_data: 'Audit engine returned no data',
|
|
343
|
+
network_error: 'Audit engine network error',
|
|
344
|
+
};
|
|
345
|
+
const AUDIT_ERROR_DETAIL = {
|
|
346
|
+
anthropic_quota_exceeded: "commit.show paused fresh audits until the daily budget refills. " +
|
|
347
|
+
"Cached audits (any repo audited in the last 7 days) still work normally.",
|
|
348
|
+
anthropic_rate_limited: "Too many fresh audits in a short window. Wait a minute and retry. " +
|
|
349
|
+
"Cached results stay available.",
|
|
350
|
+
anthropic_overloaded: "The audit engine is briefly overloaded. Retry in a minute or two.",
|
|
351
|
+
anthropic_auth_error: "commit.show's API key needs attention. Cached results still work.",
|
|
352
|
+
anthropic_other: "Something on the audit engine side blocked this run. Try again later.",
|
|
353
|
+
claude_returned_no_data: "The audit engine ran but returned an empty response. Try again.",
|
|
354
|
+
network_error: "The audit engine couldn't be reached. Check your connection or retry.",
|
|
355
|
+
};
|
|
356
|
+
function untilHuman(seconds) {
|
|
357
|
+
if (seconds < 60)
|
|
358
|
+
return `${seconds}s`;
|
|
359
|
+
if (seconds < 3600)
|
|
360
|
+
return `${Math.round(seconds / 60)}m`;
|
|
361
|
+
if (seconds < 86400)
|
|
362
|
+
return `${Math.round(seconds / 3600)}h`;
|
|
363
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
364
|
+
}
|
|
365
|
+
export function renderAuditError(err, projectName, projectUrl) {
|
|
366
|
+
const label = AUDIT_ERROR_LABEL[err.type] ?? AUDIT_ERROR_LABEL.anthropic_other;
|
|
367
|
+
const detail = AUDIT_ERROR_DETAIL[err.type] ?? AUDIT_ERROR_DETAIL.anthropic_other;
|
|
368
|
+
const lines = [];
|
|
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());
|
|
373
|
+
if (projectName) {
|
|
374
|
+
const repoLine = `Repo: ${projectName}`;
|
|
375
|
+
lines.push(' ' + boxRow(repoLine.length, c.cream(repoLine)));
|
|
376
|
+
lines.push(' ' + boxBlank());
|
|
377
|
+
}
|
|
378
|
+
for (const w of wrapText(detail, CONTENT_W)) {
|
|
379
|
+
lines.push(' ' + boxRow(w.length, c.cream(w)));
|
|
380
|
+
}
|
|
381
|
+
if (err.retry_after_seconds && err.retry_after_seconds > 0) {
|
|
382
|
+
lines.push(' ' + boxBlank());
|
|
383
|
+
const t = `Retry after ~${untilHuman(err.retry_after_seconds)}`;
|
|
384
|
+
lines.push(' ' + boxRow(t.length, c.dim(t)));
|
|
385
|
+
}
|
|
386
|
+
lines.push(' ' + boxBlank());
|
|
387
|
+
const statusLine = `Status check: commitshow status ${projectName ?? '<repo>'}`;
|
|
388
|
+
lines.push(' ' + boxRow(Math.min(statusLine.length, CONTENT_W), c.dim(statusLine.slice(0, CONTENT_W))));
|
|
389
|
+
if (projectUrl) {
|
|
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)));
|
|
393
|
+
}
|
|
394
|
+
lines.push(' ' + boxBottom());
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
304
397
|
function timeUntil(isoTarget) {
|
|
305
398
|
const ms = Math.max(0, new Date(isoTarget).getTime() - Date.now());
|
|
306
399
|
const h = Math.floor(ms / 3_600_000);
|
|
@@ -344,26 +437,30 @@ function bar(filled, total, width = 20) {
|
|
|
344
437
|
}
|
|
345
438
|
export function renderRateLimitDeny(opts) {
|
|
346
439
|
const lines = [];
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
lines.push(' ' +
|
|
350
|
-
lines.push(' ' + c.
|
|
351
|
-
lines.push(' ' +
|
|
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)));
|
|
352
448
|
if (opts.quota) {
|
|
353
|
-
const reset = timeUntil(opts.quota.reset_at)
|
|
354
|
-
lines.push(' ' +
|
|
449
|
+
const reset = `resets in ${timeUntil(opts.quota.reset_at)}`;
|
|
450
|
+
lines.push(' ' + boxRow(reset.length, c.dim(reset)));
|
|
355
451
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
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)));
|
|
359
454
|
}
|
|
360
455
|
if (opts.reason === 'url_cap') {
|
|
361
|
-
|
|
456
|
+
const tip = 'Tip: cached audit (< 7d) is free — commitshow status <repo>';
|
|
457
|
+
lines.push(' ' + boxRow(tip.length, c.dim(tip)));
|
|
362
458
|
}
|
|
363
459
|
if (opts.reason === 'ip_cap' && opts.quota?.ip.tier === 'anon') {
|
|
364
|
-
|
|
460
|
+
const tip = 'Sign in (commit.show) for a higher daily cap.';
|
|
461
|
+
lines.push(' ' + boxRow(tip.length, c.dim(tip)));
|
|
365
462
|
}
|
|
366
|
-
lines.push(' ' +
|
|
463
|
+
lines.push(' ' + boxBottom());
|
|
367
464
|
return lines.join('\n');
|
|
368
465
|
}
|
|
369
466
|
function wrapText(s, width) {
|
|
@@ -386,24 +483,35 @@ function wrapText(s, width) {
|
|
|
386
483
|
}
|
|
387
484
|
export function renderUpsell() {
|
|
388
485
|
const lines = [];
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
lines.push(' ' +
|
|
393
|
-
lines.push(' ' + c.
|
|
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).
|
|
394
500
|
const items = [
|
|
395
|
-
'
|
|
396
|
-
'
|
|
397
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
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 },
|
|
400
507
|
];
|
|
401
508
|
for (const it of items) {
|
|
402
|
-
|
|
509
|
+
const visible = `→ ${it.tag}${it.rest}`;
|
|
510
|
+
lines.push(' ' + boxRow(visible.length, it.tone('→ ') + c.cream(it.tag) + c.muted(it.rest)));
|
|
403
511
|
}
|
|
404
|
-
lines.push(' ' +
|
|
405
|
-
lines.push(' ' +
|
|
406
|
-
lines.push(' ' +
|
|
512
|
+
lines.push(' ' + boxBlank());
|
|
513
|
+
lines.push(' ' + boxRow(ctaVisible.length, c.gold(ctaVisible)));
|
|
514
|
+
lines.push(' ' + boxBottom());
|
|
407
515
|
return lines.join('\n');
|
|
408
516
|
}
|
|
409
517
|
export function writeAuditJson(dir, json) {
|