proof-of-commitment 1.18.2 → 1.20.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.
Files changed (2) hide show
  1. package/index.js +104 -26
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.18.1
3
+ * proof-of-commitment CLI v1.20.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -113,14 +113,80 @@ async function handle429(res) {
113
113
  );
114
114
  }
115
115
  console.error('');
116
- console.error(clr(c.cyan + c.bold, ` → Free API key in 30 seconds (no card): ${instantKeyUrl}`));
117
- if (retryAfter && retryAfter > 0) {
118
- const hours = Math.floor(retryAfter / 3600);
119
- const mins = Math.floor((retryAfter % 3600) / 60);
120
- const resetIn = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
121
- console.error(clr(c.dim, ` or wait — free-tier resets in ${resetIn} (00:00 UTC).`));
116
+
117
+ // TTY: inline signup collapses the 6-step browser flow (visit URL → enter
118
+ // email copy key → switch back to terminal → export key → re-run) to a
119
+ // single terminal prompt. Non-TTY (CI/piped) falls through to the URL.
120
+ if (process.stdin.isTTY && process.stdout.isTTY) {
121
+ console.error(clr(c.dim, ' ─────────────────────────────────────────────'));
122
+ console.error(clr(c.bold, ' Get a free key and keep scanning (no card, saves to ~/.commit/config):'));
123
+ console.error('');
124
+
125
+ const { createInterface } = await import('readline');
126
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
127
+
128
+ const email = await new Promise(resolve => {
129
+ rl.question(clr(c.dim, ' Your email (Enter to skip): '), answer => {
130
+ rl.close();
131
+ resolve(answer.trim());
132
+ });
133
+ });
134
+
135
+ if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
136
+ process.stderr.write(clr(c.dim, ' Creating key...'));
137
+ try {
138
+ const createRes = await fetch('https://poc-backend.amdal-dev.workers.dev/api/keys/create', {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify({ email, source: 'audit-cli-429' }),
142
+ });
143
+ const keyData = await createRes.json();
144
+ if (keyData.key) {
145
+ await writeApiKey(keyData.key);
146
+ console.error(clr(c.green, ' ✓ Key saved to ~/.commit/config'));
147
+ console.error(clr(c.dim, ` Backup sent to ${email}`));
148
+ console.error('');
149
+ console.error(clr(c.bold, ' Re-run your command to continue with your new key.'));
150
+ console.error('');
151
+ } else {
152
+ const errMsg = keyData.error === 'rate_limit_exceeded'
153
+ ? 'Too many keys from this IP today — try again tomorrow.'
154
+ : (keyData.message || 'Could not create key. Try the web: ' + instantKeyUrl);
155
+ console.error(clr(c.red, ` Failed: ${errMsg}`));
156
+ console.error('');
157
+ }
158
+ } catch (err) {
159
+ console.error(clr(c.red, ` Error: ${err.message}`));
160
+ console.error(clr(c.dim, ` Try the web: ${instantKeyUrl}`));
161
+ console.error('');
162
+ }
163
+ } else if (email) {
164
+ console.error(clr(c.red, ' Invalid email. Skipped.'));
165
+ console.error(clr(c.dim, ` Try the web: ${instantKeyUrl}`));
166
+ console.error('');
167
+ } else {
168
+ // User pressed Enter to skip — show URL as fallback
169
+ console.error(clr(c.cyan + c.bold, ` → Get a free key later: ${instantKeyUrl}`));
170
+ if (retryAfter && retryAfter > 0) {
171
+ const hours = Math.floor(retryAfter / 3600);
172
+ const mins = Math.floor((retryAfter % 3600) / 60);
173
+ const resetIn = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
174
+ console.error(clr(c.dim, ` or wait — free-tier resets in ${resetIn} (00:00 UTC).`));
175
+ }
176
+ console.error('');
177
+ }
178
+ } else {
179
+ // Non-TTY fallback: print URL for CI/piped contexts
180
+ console.error(clr(c.cyan + c.bold, ` → Free API key in 30 seconds (no card): ${instantKeyUrl}`));
181
+ if (retryAfter && retryAfter > 0) {
182
+ const hours = Math.floor(retryAfter / 3600);
183
+ const mins = Math.floor((retryAfter % 3600) / 60);
184
+ const resetIn = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
185
+ console.error(clr(c.dim, ` or wait — free-tier resets in ${resetIn} (00:00 UTC).`));
186
+ }
187
+ console.error('');
122
188
  }
123
- console.error('');
189
+
124
190
  process.exit(1);
125
191
  }
126
192
 
@@ -293,15 +359,11 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
293
359
  console.log(clr(c.dim, ' Then run: ') + clr(c.cyan, 'poc login'));
294
360
  }
295
361
  // else: TTY mode — inlineSignup() will prompt interactively after printTable
296
- } else if (!hasKey) {
297
- // HEALTHY case + no saved key: soft watchlist CTA. The all-healthy
298
- // footer previously surfaced only CI-shaped CTAs (Action, `poc init`)
299
- // which both require active commitment workflow change + repo edit.
300
- // The lowest-friction conversion (email API key watchlist) was
301
- // hidden behind the CRITICAL gate of inlineSignup(). Buyer-journey
302
- // dogfood 2026-05-24 found 1472 weekly downloads → 0 organic signups;
303
- // the watchlist value prop ("alert me when these degrade") is real
304
- // for healthy packages too — that's exactly when monitoring matters.
362
+ } else if (!hasKey && (!process.stdin.isTTY || !process.stdout.isTTY)) {
363
+ // HEALTHY case + no saved key + non-TTY (CI/piped): static baseline CTA.
364
+ // In TTY mode, inlineSignup() now prompts interactively for healthy results
365
+ // too the dim text below converted 0/621 weekly downloads. Keep static
366
+ // text only in CI/piped output where interactive prompts can't fire.
305
367
  // ref=audit-baseline distinguishes this funnel from audit-cli-429
306
368
  // (rate-limit rescue) and from the static utm_source=cli help-line.
307
369
  console.log(clr(c.dim, '\n 📊 Save this scan as your baseline. Re-run anytime with a free key:'));
@@ -311,23 +373,35 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
311
373
  }
312
374
 
313
375
  /**
314
- * Inline signup: after CRITICAL findings, offer one-step email→key flow.
376
+ * Inline signup: after any real audit, offer one-step email→key flow.
315
377
  * Collapses 6-step funnel (visit site → email → check inbox → copy key → login → watch)
316
378
  * into a single CLI prompt.
379
+ *
380
+ * v1.19: Triggers on healthy results too (≥3 packages). The dim "Save this scan
381
+ * as your baseline" footer line converted 0/621 weekly downloads — replacing it
382
+ * with an interactive prompt at the moment of audit success captures more
383
+ * intent. Copy adapts to context: degradation alerts (CRITICAL) vs baseline
384
+ * lock-in (healthy). Quick lookups (<3 packages) still skip the prompt.
317
385
  */
318
386
  async function inlineSignup(results) {
319
- // Only prompt in interactive TTY when findings make monitoring relevant and no key saved
387
+ // Only prompt in interactive TTY when no key saved
320
388
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
321
389
  const hasKey = !!process.env.COMMIT_API_KEY || _cachedHasKey;
322
390
  if (hasKey) return;
323
391
  const critPkgs = results.filter(r => hasCritical(r.riskFlags));
324
392
  const lowScorePkgs = results.filter(r => typeof r.score === 'number' && r.score < 60);
325
- // Gate: ≥1 CRITICAL, OR ≥2 packages with score<60, OR large scan (≥50 packages)
326
- const shouldPrompt = critPkgs.length >= 1 || lowScorePkgs.length >= 2 || results.length >= 50;
327
- if (!shouldPrompt) return;
393
+ // Gate: ≥3 packages scanned (real audit, not a one-off `npx poc somepkg` check)
394
+ if (results.length < 3) return;
395
+
396
+ const hasFindings = critPkgs.length >= 1 || lowScorePkgs.length >= 2;
397
+ // Copy adapts to context. Findings → degradation framing.
398
+ // Healthy → baseline-lock framing (still real value: alert me if any score drops).
399
+ const heading = hasFindings
400
+ ? ' 🔔 Lock in this audit. Get alerted if these packages get worse.'
401
+ : ' 🔔 Lock in this baseline. Get alerted if any of these packages degrade.';
328
402
 
329
403
  console.log(clr(c.dim, ' ─────────────────────────────────────────────'));
330
- console.log(clr(c.bold, ' 🔔 Lock in this audit. Get alerts if these packages get worse.'));
404
+ console.log(clr(c.bold, heading));
331
405
  console.log(clr(c.dim, ' Free, no card, 10 seconds. Saves to ~/.commit/config.\n'));
332
406
 
333
407
  const { createInterface } = await import('readline');
@@ -366,8 +440,12 @@ async function inlineSignup(results) {
366
440
  console.log();
367
441
  console.log(clr(c.bold, ' Next steps:'));
368
442
  console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc status') + clr(c.dim, ' — check your account'));
369
- if (critPkgs.length > 0) {
370
- console.log(clr(c.dim, ' • ') + clr(c.cyan, `poc watch ${critPkgs[0].name}`) + clr(c.dim, ' — start monitoring (Developer $15/mo)'));
443
+ // Surface a concrete watch target. CRITICAL first (highest urgency);
444
+ // otherwise pick the lowest-score package as the most-likely-to-degrade.
445
+ const watchTarget = critPkgs[0]?.name
446
+ || results.slice().sort((a, b) => (a.score || 100) - (b.score || 100))[0]?.name;
447
+ if (watchTarget) {
448
+ console.log(clr(c.dim, ' • ') + clr(c.cyan, `poc watch ${watchTarget}`) + clr(c.dim, ' — start monitoring (Developer $15/mo)'));
371
449
  }
372
450
  console.log(clr(c.dim, ' • ') + clr(c.cyan, 'poc init') + clr(c.dim, ' — add CI gate to this project'));
373
451
  } else if (data.message) {
@@ -384,7 +462,7 @@ async function inlineSignup(results) {
384
462
 
385
463
  function printHelp() {
386
464
  console.log(`
387
- ${clr(c.bold, 'proof-of-commitment')} v1.18.2 — supply chain risk scorer
465
+ ${clr(c.bold, 'proof-of-commitment')} v1.20.0 — supply chain risk scorer
388
466
 
389
467
  ${clr(c.bold, 'Usage:')}
390
468
  npx proof-of-commitment Auto-detect manifest in current dir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.18.2",
3
+ "version": "1.20.0",
4
4
  "mcpName": "io.github.piiiico/proof-of-commitment",
5
5
  "description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
6
6
  "type": "module",