Skip to content

Commit cb4b7b2

Browse files
committed
cli share link feature added
1 parent f05da51 commit cb4b7b2

10 files changed

Lines changed: 759 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# SpecShield CLI changelog
2+
3+
## 3.2.0 — 2026-05-17 — Conversion fixes
4+
5+
Three CLI changes designed to make the Cloud features (history, share URLs,
6+
PR checks, BDCT) visible to the 2,000+ existing CLI users who run local
7+
`specshield compare` but never discover what signing up unlocks.
8+
9+
### Added
10+
11+
- **Post-install welcome banner** — runs once after a fresh `npm install -g specshield`.
12+
Briefly explains the value progression (Local → Cloud Free → Pro) and points
13+
to the next command to try. Skipped automatically in CI, in non-TTY shells,
14+
on update installs, and when `SPECSHIELD_NO_BANNER=1` is set.
15+
16+
- **Contextual signup prompt after `specshield compare`** — after 3+ compares
17+
per week, a soft 3-line nudge appears below the regular output:
18+
```
19+
● Track these comparisons over time:
20+
specshield login # 30-sec signup via GitHub / Google · no credit card
21+
Unlocks: compare history, shareable report URLs, PR badge
22+
```
23+
Throttled to once per week so it never spams. Suppressed entirely for
24+
logged-in users, `--json` output, CI environments, and on opt-out. The
25+
prompt copy escalates at 10 and 25 compares per window.
26+
27+
- **`specshield history`** — new command to list recent comparisons saved
28+
in your Cloud account. Surfaces in `specshield --help` so local-only
29+
users discover the feature exists.
30+
31+
- **`specshield share <report-id | base.yaml target.yaml>`** — generate a
32+
public shareable URL for a comparison. Designed for pasting diffs into
33+
Slack, PR comments, or Jira. Cloud account required.
34+
35+
Both new commands print a friendly "Get started in 30 seconds" message
36+
with a `specshield login` deep link when run without credentials.
37+
38+
### Why
39+
40+
Background: in May 2026 the CLI had 2,000+ active monthly users (npm download
41+
estimate; real human count likely 200-500) but the SpecShield Cloud signup
42+
rate from the CLI was effectively zero. Local compare was so capable that
43+
users got 100% of their immediate value without ever needing an account.
44+
45+
These changes don't remove any free functionality — local compare is still
46+
fully usable without signup — they just make the gap between local and
47+
cloud visible at the moments when a user is most engaged (post-install,
48+
post-compare, on `--help`).
49+
50+
### Tests
51+
52+
- Added `tests/core/conversionPrompt.test.js` covering all skip conditions,
53+
threshold logic, escalation, and 7-day window reset (10 tests).
54+
- All 134 existing tests still pass.
55+
56+
### Opting out
57+
58+
If you don't want the banner or contextual prompts:
59+
60+
```sh
61+
export SPECSHIELD_NO_BANNER=1 # disable banner + post-compare nudge
62+
```
63+
64+
Or just sign up — logged-in users never see either.

README.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ specshield bdct can-i-deploy --version $GITHUB_SHA
137137

138138
See [§ specshield init](#specshield-init--first-run-setup-wizard) below.
139139

140+
> **Quiet install for CI / Docker images:**
141+
> The post-install welcome banner auto-detects CI environments (`CI`,
142+
> `GITHUB_ACTIONS`, `BUILDKITE`, `CIRCLECI`, `GITLAB_CI`, `JENKINS_URL`,
143+
> `TRAVIS`, `TF_BUILD`) and skips itself there — so your CI logs stay clean.
144+
> To silence it on a workstation too:
145+
> ```bash
146+
> export SPECSHIELD_NO_BANNER=1
147+
> ```
148+
140149
---
141150
142151
## 🚀 Create Your Free Account
@@ -160,7 +169,9 @@ See [§ specshield init](#specshield-init--first-run-setup-wizard) below.
160169
| Breaking change detection ||||
161170
| JSON / human output ||||
162171
| Fail CI on breaking change ||||
163-
| **Compare history & dashboard** ||||
172+
| **`specshield history` — compare timeline** ||||
173+
| **`specshield share` — public report URLs** ||||
174+
| **Dashboard** ||||
164175
| **GitHub App PR checks** ||||
165176
| **BDCT bi-directional contracts** ||||
166177
| **BDCT can-i-deploy gating** ||||
@@ -322,6 +333,63 @@ specshield compare base.yaml target.yaml --remote --json --output result.json
322333
323334
---
324335
336+
## Comparison History
337+
338+
Every `specshield compare --remote` is saved to your SpecShield account.
339+
List the recent comparisons your account has run from any machine — useful
340+
for tracking API drift over time across CI pipelines + local runs.
341+
342+
```bash
343+
specshield history # last 20 comparisons
344+
specshield history --limit 50 # show more
345+
specshield history --json # machine-readable for scripts
346+
```
347+
348+
```
349+
Your recent comparisons
350+
─────────────────────────────────────────────────────
351+
482 3 breaking 2026-05-17 14:30 payment-v1.yaml → payment-v2.yaml
352+
481 0 breaking 2026-05-17 11:02 user-api.yaml → user-api-updated.yaml
353+
480 7 breaking 2026-05-16 18:55 billing-v3.yaml → billing-v4.yaml
354+
```
355+
356+
Account required — run `specshield login` to set up (free, no credit card).
357+
358+
---
359+
360+
## Share a Comparison
361+
362+
Generate a public, tokenized URL for any comparison report. Anyone with
363+
the link can view the diff — no SpecShield account needed. Great for
364+
pasting into Slack threads, PR comments, or Jira tickets.
365+
366+
```bash
367+
# Share an existing report by ID (from `specshield history`)
368+
specshield share 482
369+
370+
# Compare two specs and share the result in one step
371+
specshield share base.yaml target.yaml
372+
373+
# Time-limited link — expires in 30 days
374+
specshield share 482 --expires 30
375+
```
376+
377+
```
378+
✔ Share link ready
379+
─────────────────────────────────────────────────────
380+
https://specshield.io/r/_Ru8OVubxY3r9zHOsylESaULphCqBYH5jTPYldSMU88
381+
Expires: 2026-06-16T12:34:56Z
382+
383+
Anyone with this link can view the diff — no SpecShield account required.
384+
```
385+
386+
Links use a 256-bit random token, so they can't be guessed by enumeration.
387+
Revoke any time from your dashboard at [specshield.io](https://specshield.io).
388+
389+
Account required — `specshield login` to set up.
390+
391+
---
392+
325393
## GitHub Integration
326394
327395
**Automatic API contract checks on every pull request — no workflow YAML required.**
@@ -989,6 +1057,25 @@ specshield compare <base> <target> [options]
9891057
| `--config <path>` | Path to `.specshield.yml` |
9901058
| `--timeout <ms>` | Request timeout for remote mode |
9911059
1060+
```bash
1061+
specshield history [options]
1062+
```
1063+
1064+
| Option | Description |
1065+
|---|---|
1066+
| `--limit <n>` | Number of comparisons to list (default 20) |
1067+
| `--json` | Machine-readable JSON output |
1068+
| `--api-key <key>` | Override stored API key |
1069+
1070+
```bash
1071+
specshield share <reportId | base.yaml target.yaml> [options]
1072+
```
1073+
1074+
| Option | Description |
1075+
|---|---|
1076+
| `--expires <days>` | Make the link expire after N days (default: never) |
1077+
| `--api-key <key>` | Override stored API key |
1078+
9921079
```bash
9931080
specshield bdct <subcommand> [options]
9941081
```

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "specshield",
3-
"version": "3.1.2",
3+
"version": "3.2.0",
44
"description": "CLI for OpenAPI breaking change detection and bi-directional contract verification — with can-i-deploy gating, GitHub PR checks, and a first-run setup wizard.",
55
"main": "src/cli.js",
66
"bin": {
@@ -10,7 +10,8 @@
1010
"start": "node bin/specshield.js",
1111
"test": "jest --coverage",
1212
"test:watch": "jest --watch",
13-
"lint": "eslint src tests --ext .js"
13+
"lint": "eslint src tests --ext .js",
14+
"postinstall": "node scripts/welcome.js || true"
1415
},
1516
"keywords": [
1617
"openapi",
@@ -47,7 +48,9 @@
4748
"files": [
4849
"bin",
4950
"src",
51+
"scripts",
5052
"README.md",
53+
"CHANGELOG.md",
5154
"LICENSE"
5255
],
5356
"dependencies": {

scripts/welcome.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Post-install welcome banner.
4+
*
5+
* Runs once via the `postinstall` hook in package.json after a fresh
6+
* `npm install -g specshield`. Prints a brief value-progression banner so
7+
* users discover the cloud features that exist beyond local compare —
8+
* historically users would install, run compare, get value, and never look
9+
* at the README to find out signup unlocks more.
10+
*
11+
* We bail out silently in environments where a banner would be noise or
12+
* could break automation:
13+
* - CI environment variables present (CI, GITHUB_ACTIONS, BUILDKITE,
14+
* CIRCLECI, GITLAB_CI, JENKINS_URL, TRAVIS, TF_BUILD)
15+
* - npm_config_loglevel is `silent` or `error` (user opted out of noise)
16+
* - SPECSHIELD_NO_BANNER=1 (explicit opt-out)
17+
* - Not running under npm (`npm_command` unset)
18+
* - Update install rather than fresh install (npm_command !== "install")
19+
*
20+
* No state is written; we let the user's terminal scroll and move on.
21+
*/
22+
'use strict';
23+
24+
function shouldSkip() {
25+
const env = process.env;
26+
if (env.SPECSHIELD_NO_BANNER === '1') return true;
27+
if (env.CI || env.GITHUB_ACTIONS || env.BUILDKITE || env.CIRCLECI ||
28+
env.GITLAB_CI || env.JENKINS_URL || env.TRAVIS || env.TF_BUILD) {
29+
return true;
30+
}
31+
const loglevel = env.npm_config_loglevel;
32+
if (loglevel === 'silent' || loglevel === 'error') return true;
33+
// Only show on the user-initiated "install" command. Things like
34+
// `npm ci`, `npm update`, transitive dep installs all set npm_command
35+
// to something other than 'install'.
36+
if (env.npm_command !== 'install') return true;
37+
return false;
38+
}
39+
40+
if (shouldSkip()) {
41+
process.exit(0);
42+
}
43+
44+
// ANSI color helpers — kept local to avoid pulling in chalk during a
45+
// post-install script (chalk requires node_modules to be fully populated,
46+
// which is racy during installs).
47+
const isTTY = !!process.stdout.isTTY;
48+
const c = (code, s) => (isTTY ? `[${code}m${s}` : s);
49+
const bold = (s) => c('1', s);
50+
const dim = (s) => c('2', s);
51+
const cyan = (s) => c('36', s);
52+
const grn = (s) => c('32', s);
53+
54+
const banner = `
55+
${bold('SpecShield installed')} ${dim('— OpenAPI breaking-change detection + BDCT')}
56+
57+
${bold('Get started')}
58+
${cyan('specshield compare')} base.yaml target.yaml --fail-on-breaking
59+
${cyan('specshield init')} ${dim('# project setup wizard')}
60+
61+
${bold('Your usage tier')}
62+
${grn('●')} ${bold('Local (free, no account)')} spec compare, breaking-change detection
63+
${dim('○')} Cloud Free ${dim('+ compare history, PR badge, share URLs')}
64+
${dim('○')} Pro ${dim('+ BDCT, can-i-deploy, GitHub PR checks, team')}
65+
66+
${dim('Sign in when you want history & sharing:')} ${cyan('specshield login')}
67+
${dim('Docs:')} ${cyan('https://specshield.io/docs')}
68+
`;
69+
70+
process.stdout.write(banner + '\n');

src/cli.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const initCommand = require('./commands/init');
77
const loginCommand = require('./commands/login');
88
const logoutCommand = require('./commands/logout');
99
const bdctCommand = require('./commands/bdct');
10+
const historyCommand = require('./commands/history');
11+
const shareCommand = require('./commands/share');
1012

1113
const program = new Command();
1214

@@ -24,6 +26,8 @@ program.addCommand(initCommand);
2426
program.addCommand(loginCommand);
2527
program.addCommand(logoutCommand);
2628
program.addCommand(bdctCommand);
29+
program.addCommand(historyCommand);
30+
program.addCommand(shareCommand);
2731

2832
program.parseAsync(process.argv).catch((err) => {
2933
const logger = require('./utils/logger');

src/commands/compare.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const { classifyChanges, filterBySeverity } = require('../core/classifyChanges')
1212
const { formatHuman, formatJson } = require('../core/outputFormatter');
1313
const { loadConfig } = require('../core/configLoader');
1414
const { resolveExitCode } = require('../core/exitCode');
15+
const { recordCompareAndMaybeRender } = require('../core/conversionPrompt');
1516
const logger = require('../utils/logger');
1617
const fsExtra = require('fs-extra');
1718
const { getStoredApiKey } = require('../config/localConfig');
@@ -92,6 +93,22 @@ compare
9293
}
9394
}
9495

96+
// Contextual signup nudge — runs only when the user isn't logged in,
97+
// output is human-readable, not in CI, and they've crossed a usage
98+
// threshold this week. The recordCompareAndMaybeRender function is
99+
// best-effort; any failure (disk read/write, etc.) is silently
100+
// swallowed so the conversion path never blocks compare.
101+
try {
102+
const loggedIn = !!(options.resolvedApiKey ||
103+
process.env.SPECSHIELD_API_KEY ||
104+
await getStoredApiKey());
105+
const prompt = await recordCompareAndMaybeRender({
106+
loggedIn,
107+
jsonOutput: !!options.json,
108+
});
109+
if (prompt) process.stdout.write('\n' + prompt + '\n');
110+
} catch { /* never block on the nudge */ }
111+
95112
// Exit code
96113
const code = resolveExitCode(result, options);
97114
process.exit(code);

0 commit comments

Comments
 (0)