commitshow 0.3.31 → 0.4.0
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 +144 -2
- package/dist/commands/extract.js +17 -4
- package/dist/index.js +2 -0
- package/dist/lib/api.js +20 -0
- package/dist/lib/target.js +58 -5
- package/package.json +1 -1
package/dist/commands/audit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveTarget, verifyRemoteExists, TargetError } from '../lib/target.js';
|
|
2
|
-
import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding, runPreviewAudit, waitForPreviewSnapshot, } from '../lib/api.js';
|
|
2
|
+
import { findProjectByGithubUrl, fetchLatestSnapshot, fetchStanding, runPreviewAudit, runSiteFastLaneAudit, waitForPreviewSnapshot, } from '../lib/api.js';
|
|
3
3
|
import { renderAudit, renderMarkdown, renderJson, renderUpsell, renderStarCta, renderQuotaFooter, renderRateLimitDeny, renderAuditError, writeAuditMarkdown, writeAuditJson, } from '../lib/render.js';
|
|
4
4
|
import { c } from '../lib/colors.js';
|
|
5
5
|
import { Spinner } from '../lib/spinner.js';
|
|
@@ -47,6 +47,13 @@ export async function audit(args) {
|
|
|
47
47
|
}
|
|
48
48
|
throw err;
|
|
49
49
|
}
|
|
50
|
+
// §15-E URL Fast Lane · short-circuit before the github-specific
|
|
51
|
+
// verifyRemoteExists / findProjectByGithubUrl path. The site-url lane
|
|
52
|
+
// has its own Edge Function (audit-site-preview) and identifies projects
|
|
53
|
+
// by live_url, not github_url, so the cached-flow lookup wouldn't apply.
|
|
54
|
+
if (target.kind === 'site-url') {
|
|
55
|
+
return await runSiteAudit(target, { force, sourceFlag, asJson });
|
|
56
|
+
}
|
|
50
57
|
// Pre-flight: when the user pointed at a remote URL (or owner/repo
|
|
51
58
|
// shorthand), confirm it actually resolves on github.com before we
|
|
52
59
|
// spend any audit budget. Catches the 'agent invented a slug' case
|
|
@@ -161,6 +168,39 @@ export async function audit(args) {
|
|
|
161
168
|
// Error envelope
|
|
162
169
|
if ('error' in result) {
|
|
163
170
|
const err = result;
|
|
171
|
+
// Friendly path for the most common CLI miss: trying to audit a
|
|
172
|
+
// private/missing/typo'd repo. Used to silently produce a 'ghost
|
|
173
|
+
// repo' snapshot scored 4 — confusing because users assumed their
|
|
174
|
+
// project actually scored that. Server now bails early with a
|
|
175
|
+
// dedicated envelope; we render a clear panel instead of a score.
|
|
176
|
+
if (err.error === 'github_inaccessible') {
|
|
177
|
+
if (asJson) {
|
|
178
|
+
process.stdout.write(JSON.stringify({
|
|
179
|
+
error: 'github_inaccessible',
|
|
180
|
+
reason: err.reason,
|
|
181
|
+
slug: err.slug,
|
|
182
|
+
github_url: err.github_url,
|
|
183
|
+
message: err.message,
|
|
184
|
+
hints: err.hints,
|
|
185
|
+
target: target.github_url,
|
|
186
|
+
}) + '\n');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.error('');
|
|
190
|
+
console.error(` ${c.scarlet('✗')} ${c.bold(c.cream("Couldn't reach"))} ${c.gold(err.slug ?? target.github_url)}`);
|
|
191
|
+
console.error('');
|
|
192
|
+
console.error(` ${c.muted(err.message ?? "We can't see this repo.")}`);
|
|
193
|
+
console.error('');
|
|
194
|
+
if (err.hints && err.hints.length > 0) {
|
|
195
|
+
console.error(` ${c.muted('common causes:')}`);
|
|
196
|
+
for (const hint of err.hints) {
|
|
197
|
+
console.error(` ${c.gold('·')} ${c.muted(hint)}`);
|
|
198
|
+
}
|
|
199
|
+
console.error('');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
164
204
|
if (err.error === 'rate_limited') {
|
|
165
205
|
if (asJson) {
|
|
166
206
|
process.stdout.write(JSON.stringify({
|
|
@@ -175,8 +215,12 @@ export async function audit(args) {
|
|
|
175
215
|
}
|
|
176
216
|
else {
|
|
177
217
|
console.error('');
|
|
218
|
+
// err.reason can include non-cap values (e.g. private_or_missing)
|
|
219
|
+
// since PreviewError unions all reasons; renderRateLimitDeny only
|
|
220
|
+
// accepts the cap variants — narrow the input before passing.
|
|
221
|
+
const capReason = err.reason === 'url_cap' || err.reason === 'global_cap' ? err.reason : 'ip_cap';
|
|
178
222
|
console.error(renderRateLimitDeny({
|
|
179
|
-
reason:
|
|
223
|
+
reason: capReason,
|
|
180
224
|
message: err.message ?? 'Rate limit hit. Try again later.',
|
|
181
225
|
limit: err.limit ?? 0,
|
|
182
226
|
count: err.count ?? 0,
|
|
@@ -286,3 +330,101 @@ function emitError(asJson, code, message, target) {
|
|
|
286
330
|
console.error(c.scarlet(message));
|
|
287
331
|
}
|
|
288
332
|
}
|
|
333
|
+
// §15-E URL Fast Lane CLI handler. Mirrors the runPreviewAudit branch
|
|
334
|
+
// of the main flow but routes to audit-site-preview and uses live_url
|
|
335
|
+
// as the project identifier instead of github_url.
|
|
336
|
+
//
|
|
337
|
+
// On success: renders the same audit panel with the URL lane upsell —
|
|
338
|
+
// the only delta vs the repo lane is partial-cap framing in the upsell
|
|
339
|
+
// copy, handled by renderUpsell when github_url is empty.
|
|
340
|
+
async function runSiteAudit(target, opts) {
|
|
341
|
+
const { force, sourceFlag, asJson } = opts;
|
|
342
|
+
if (!target.site_url) {
|
|
343
|
+
emitError(asJson, 'bad_target', 'Site URL missing — internal target resolution issue.', target.slug);
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
if (!asJson) {
|
|
347
|
+
if (force)
|
|
348
|
+
console.log(c.dim(`Refreshing URL audit for ${target.slug}…`));
|
|
349
|
+
else
|
|
350
|
+
console.log(c.dim(`Auditing ${target.slug} (URL fast lane · partial)…`));
|
|
351
|
+
}
|
|
352
|
+
const result = await runSiteFastLaneAudit(target.site_url, { force, source: sourceFlag });
|
|
353
|
+
if ('error' in result) {
|
|
354
|
+
const err = result;
|
|
355
|
+
if (err.error === 'rate_limited' && err.quota) {
|
|
356
|
+
const reason = (err.reason ?? 'ip_cap');
|
|
357
|
+
// Map server's domain_cap onto url_cap for renderRateLimitDeny (same shape).
|
|
358
|
+
const capReason = reason === 'ip_cap' ? 'ip_cap' : reason === 'global_cap' ? 'global_cap' : 'url_cap';
|
|
359
|
+
if (asJson) {
|
|
360
|
+
process.stdout.write(JSON.stringify({
|
|
361
|
+
error: 'rate_limited', reason: capReason,
|
|
362
|
+
message: err.message ?? 'Rate limit hit.',
|
|
363
|
+
quota: err.quota,
|
|
364
|
+
}) + '\n');
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.error('');
|
|
368
|
+
console.error(renderRateLimitDeny({
|
|
369
|
+
reason: capReason,
|
|
370
|
+
message: err.message ?? 'Rate limit hit. Try again later.',
|
|
371
|
+
limit: err.limit ?? 0,
|
|
372
|
+
count: err.count ?? 0,
|
|
373
|
+
quota: err.quota,
|
|
374
|
+
}));
|
|
375
|
+
console.error('');
|
|
376
|
+
}
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
if (err.error === 'domain_opted_out') {
|
|
380
|
+
emitError(asJson, 'domain_opted_out', err.message ?? `${target.slug} declined audits via DNS TXT.`, target.site_url);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
emitError(asJson, err.error ?? 'site_audit_failed', err.message ?? 'URL audit failed.', target.site_url);
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
let envelope;
|
|
387
|
+
if ('status' in result && result.status === 'running') {
|
|
388
|
+
const pending = result;
|
|
389
|
+
const spinner = new Spinner();
|
|
390
|
+
if (!asJson)
|
|
391
|
+
spinner.start(`Auditing ${target.slug}`);
|
|
392
|
+
let waited;
|
|
393
|
+
try {
|
|
394
|
+
waited = await waitForPreviewSnapshot(pending.project_id, null);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
spinner.stop();
|
|
398
|
+
}
|
|
399
|
+
if (!waited) {
|
|
400
|
+
emitError(asJson, 'timeout', 'URL audit is taking longer than expected. Refresh in a minute.', target.site_url);
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
envelope = { ...waited, quota: pending.quota };
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
envelope = result;
|
|
407
|
+
}
|
|
408
|
+
const view = { project: envelope.project, snapshot: envelope.snapshot, standing: null };
|
|
409
|
+
if (asJson) {
|
|
410
|
+
const shape = JSON.parse(renderJson(view));
|
|
411
|
+
if (envelope.quota)
|
|
412
|
+
shape.quota = envelope.quota;
|
|
413
|
+
shape.audit_kind = 'url_fast_lane';
|
|
414
|
+
process.stdout.write(JSON.stringify(shape, null, 2) + '\n');
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log('');
|
|
418
|
+
console.log(renderAudit(view));
|
|
419
|
+
console.log('');
|
|
420
|
+
if (envelope.quota) {
|
|
421
|
+
console.log(renderQuotaFooter(envelope.quota));
|
|
422
|
+
console.log('');
|
|
423
|
+
}
|
|
424
|
+
// Custom upsell · partial-cap framing
|
|
425
|
+
console.log(c.dim(` ${c.gold('partial audit')} · the engine couldn't see your repo signals (tests · CI · LICENSE · brief).\n` +
|
|
426
|
+
` Push past the URL ceiling: ${c.cream('commit.show/submit')} with your repo.`));
|
|
427
|
+
console.log('');
|
|
428
|
+
}
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
package/dist/commands/extract.js
CHANGED
|
@@ -139,21 +139,34 @@ function copyToClipboard(text) {
|
|
|
139
139
|
export async function extract(args) {
|
|
140
140
|
const asJson = args.includes('--json');
|
|
141
141
|
const positional = args.find(a => !a.startsWith('--'));
|
|
142
|
-
|
|
142
|
+
// Unlike `audit`, extract doesn't NEED a GitHub URL — it just scans
|
|
143
|
+
// ~/.claude/projects/<encoded-cwd>/*.jsonl for token usage. github_url
|
|
144
|
+
// is purely optional metadata in the blob (helps the server match the
|
|
145
|
+
// receipt back to the right project on commit.show). So we try to
|
|
146
|
+
// resolve a target but fall back to a cwd-only target when there's no
|
|
147
|
+
// git remote — instead of bailing with audit's "No git remote" error.
|
|
148
|
+
let target = null;
|
|
143
149
|
try {
|
|
144
150
|
target = resolveTarget(positional, { workspace: null });
|
|
145
151
|
}
|
|
146
152
|
catch (e) {
|
|
147
153
|
if (e instanceof TargetError) {
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
// Treat as 'no github_url' rather than fatal · scan still works.
|
|
155
|
+
target = { github_url: null, localPath: positional ? positional : process.cwd() };
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
throw e;
|
|
150
159
|
}
|
|
151
|
-
throw e;
|
|
152
160
|
}
|
|
153
161
|
if (!asJson) {
|
|
154
162
|
console.log();
|
|
155
163
|
console.log(HEADER);
|
|
156
164
|
console.log();
|
|
165
|
+
if (!target.github_url) {
|
|
166
|
+
console.log(c.muted(` no git remote detected · receipt will scan local Claude Code sessions only`));
|
|
167
|
+
console.log(c.muted(` paste the blob into your project's audition form on commit.show — that's where it gets matched`));
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
// Use the local cwd (or the path target) as the lookup key. Remote URL
|
|
159
172
|
// targets fall back to scanning the entire ~/.claude/projects/ for any
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,8 @@ ${c.muted('TARGET FORMS')} ${c.dim('(default: cwd)')}
|
|
|
51
51
|
${c.cream('commitshow audit github.com/owner/repo')} ${c.dim('# remote shorthand')}
|
|
52
52
|
${c.cream('commitshow audit https://github.com/o/r')} ${c.dim('# full URL')}
|
|
53
53
|
${c.cream('commitshow audit owner/repo')} ${c.dim('# last-ditch shorthand')}
|
|
54
|
+
${c.cream('commitshow audit yoursite.com')} ${c.dim('# URL Fast Lane · partial audit · no repo needed')}
|
|
55
|
+
${c.cream('commitshow audit https://yoursite.com')} ${c.dim('# same · full URL form')}
|
|
54
56
|
|
|
55
57
|
${c.muted('MONOREPO TARGETS')} ${c.dim('(all three forms produce the same audit)')}
|
|
56
58
|
${c.cream('commitshow audit github.com/o/r --workspace apps/web')} ${c.dim('# explicit flag')}
|
package/dist/lib/api.js
CHANGED
|
@@ -121,6 +121,26 @@ export async function runPreviewAudit(githubUrl, liveUrl, opts = {}) {
|
|
|
121
121
|
return body;
|
|
122
122
|
return body;
|
|
123
123
|
}
|
|
124
|
+
/** §15-E URL Fast Lane · sister of runPreviewAudit. Kicks off audit on a
|
|
125
|
+
* deployed site URL (no repo). Server returns 202 + project_id like
|
|
126
|
+
* audit-preview · same poller (waitForPreviewSnapshot) handles the wait. */
|
|
127
|
+
export async function runSiteFastLaneAudit(siteUrl, opts = {}) {
|
|
128
|
+
const res = await fetch(`${baseUrl()}/functions/v1/audit-site-preview`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: headers(),
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
site_url: siteUrl,
|
|
133
|
+
force: opts.force === true,
|
|
134
|
+
source: opts.source ?? null,
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const body = await res.json().catch(() => ({ error: 'invalid_json' }));
|
|
138
|
+
if (res.status === 202)
|
|
139
|
+
return body;
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
return body;
|
|
142
|
+
return body;
|
|
143
|
+
}
|
|
124
144
|
/** Poll a preview job until the snapshot lands or we time out.
|
|
125
145
|
*
|
|
126
146
|
* `since` (ISO timestamp) is the baseline we wait past. For an INITIAL audit
|
package/dist/lib/target.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
// Target detection — turns the CLI positional arg into a canonical
|
|
2
|
-
// { kind: 'remote-url', github_url } or { kind: 'local', path, github_url? }
|
|
2
|
+
// { kind: 'remote-url', github_url } or { kind: 'local', path, github_url? }
|
|
3
|
+
// or { kind: 'site-url', site_url } (§15-E URL fast lane).
|
|
3
4
|
//
|
|
4
|
-
// Accepted inputs
|
|
5
|
+
// Accepted inputs:
|
|
5
6
|
// · (omitted) → cwd · read `git remote get-url origin`
|
|
6
7
|
// · ./my-repo · /abs/path → local dir · same remote inference
|
|
7
8
|
// · github.com/owner/repo → bare host shorthand
|
|
8
9
|
// · https://github.com/owner/repo → full URL
|
|
9
10
|
// · git@github.com:owner/repo.git → ssh form (common in `git remote`)
|
|
10
11
|
// · owner/repo → last-ditch shorthand (2 segments, no dot)
|
|
11
|
-
// · github.com/owner/repo/apps/web → inline workspace
|
|
12
|
+
// · github.com/owner/repo/apps/web → inline workspace
|
|
12
13
|
// · https://github.com/owner/repo/tree/main/apps/web → GitHub browse URL (paste-friendly)
|
|
14
|
+
// · yoursite.com / https://yoursite.com → site URL (§15-E URL Fast Lane · NEW)
|
|
15
|
+
// routes to audit-site-preview ·
|
|
16
|
+
// partial cap ~32/50 · no repo needed
|
|
13
17
|
//
|
|
14
|
-
// Workspace selection precedence:
|
|
18
|
+
// Workspace selection precedence (repo lanes only):
|
|
15
19
|
// 1. --workspace <path> CLI flag (highest · explicit override)
|
|
16
20
|
// 2. Inline path in target URL (sub-path after <owner>/<repo>)
|
|
17
21
|
// 3. Auto-pick on the server (Edge Function priority-name → repo-name → file count)
|
|
@@ -113,6 +117,43 @@ function matchUrl(raw) {
|
|
|
113
117
|
}
|
|
114
118
|
return null;
|
|
115
119
|
}
|
|
120
|
+
// Site URL detection (§15-E URL Fast Lane).
|
|
121
|
+
// Accepts:
|
|
122
|
+
// · https://example.com / http://example.com → full URL
|
|
123
|
+
// · example.com / sub.example.com → bare host (added https://)
|
|
124
|
+
// · example.com/path → host + path · we strip path (origin only)
|
|
125
|
+
// Rejects:
|
|
126
|
+
// · github.com URLs (those go to remote-url path · matchUrl above)
|
|
127
|
+
// · localhost / private IPs / 1-segment hosts (no dot)
|
|
128
|
+
// · commit.show itself (reflexive)
|
|
129
|
+
function matchSiteUrl(raw) {
|
|
130
|
+
const s = raw.trim();
|
|
131
|
+
if (!s)
|
|
132
|
+
return null;
|
|
133
|
+
// owner/repo shorthand has no dot in the second segment — handled by matchUrl.
|
|
134
|
+
// Anything reaching here that contains "github.com" is a github form we already
|
|
135
|
+
// tried · don't double-route.
|
|
136
|
+
if (/github\.com/i.test(s))
|
|
137
|
+
return null;
|
|
138
|
+
const candidate = /^https?:\/\//i.test(s) ? s : `https://${s}`;
|
|
139
|
+
let u;
|
|
140
|
+
try {
|
|
141
|
+
u = new URL(candidate);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:')
|
|
147
|
+
return null;
|
|
148
|
+
const host = u.host.toLowerCase().replace(/^www\./, '');
|
|
149
|
+
if (!host.includes('.'))
|
|
150
|
+
return null; // localhost · single-label hosts
|
|
151
|
+
if (/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host))
|
|
152
|
+
return null;
|
|
153
|
+
if (host === 'commit.show' || host.endsWith('.commit.show'))
|
|
154
|
+
return null;
|
|
155
|
+
return { origin: `${u.protocol}//${host}`, host };
|
|
156
|
+
}
|
|
116
157
|
function gitRemoteOrigin(cwd) {
|
|
117
158
|
try {
|
|
118
159
|
const out = execSync('git remote get-url origin', {
|
|
@@ -135,7 +176,7 @@ export function resolveTarget(rawArg, opts = {}) {
|
|
|
135
176
|
// Both normalized through the same path so the server receives a
|
|
136
177
|
// single shape.
|
|
137
178
|
const flagWorkspace = normalizeSubpath(opts.workspace ?? null);
|
|
138
|
-
// 1 · Explicit URL forms
|
|
179
|
+
// 1 · Explicit URL forms — github first (most common · most specific)
|
|
139
180
|
if (rawArg) {
|
|
140
181
|
const m = matchUrl(rawArg);
|
|
141
182
|
if (m) {
|
|
@@ -146,6 +187,18 @@ export function resolveTarget(rawArg, opts = {}) {
|
|
|
146
187
|
workspace: flagWorkspace ?? m.workspace ?? null,
|
|
147
188
|
};
|
|
148
189
|
}
|
|
190
|
+
// 1b · Site URL fast lane (§15-E) — anything URL-shaped that isn't
|
|
191
|
+
// github.com / a local path. Routes to audit-site-preview.
|
|
192
|
+
const site = matchSiteUrl(rawArg);
|
|
193
|
+
if (site) {
|
|
194
|
+
return {
|
|
195
|
+
kind: 'site-url',
|
|
196
|
+
github_url: '',
|
|
197
|
+
site_url: site.origin,
|
|
198
|
+
slug: site.host,
|
|
199
|
+
workspace: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
149
202
|
}
|
|
150
203
|
// 2 · Local path (arg resolves to a directory) or cwd
|
|
151
204
|
const path = resolve(rawArg ?? '.');
|