commitshow 0.3.33 → 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
@@ -323,3 +330,101 @@ function emitError(asJson, code, message, target) {
323
330
  console.error(c.scarlet(message));
324
331
  }
325
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/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.33",
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": {