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.
@@ -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: err.reason ?? 'ip_cap',
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
+ }
@@ -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
- let target;
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
- console.error(c.scarlet(e.message));
149
- return 1;
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
@@ -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 (all resolve to a GitHub HTTPS URL):
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 (post-2026-05-06)
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 ?? '.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commitshow",
3
- "version": "0.3.31",
3
+ "version": "0.4.0",
4
4
  "description": "commit.show CLI — audit any vibe-coded project from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {