specshield 3.2.6 → 3.2.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # SpecShield CLI changelog
2
2
 
3
+ ## 3.2.7 — 2026-06-12 — `history` + `share` now work from the CLI
4
+
5
+ `specshield history` and `specshield share` were previously "UI-only" stubs —
6
+ the backend's comparison-history and share-link endpoints lived on JWT-only
7
+ routes that rejected the CLI's API key. The backend now exposes API-key-callable
8
+ endpoints (`/api/compare-history`, `/api/share-links`) and `/compare` returns the
9
+ saved comparison's id, so the full CLI/CI flow works end to end.
10
+
11
+ ### Added
12
+
13
+ - **`specshield history`** lists your recent comparisons (id, breaking count,
14
+ date, `base → target`). Honors `--limit` and `--json`. Reads the new
15
+ `/api/compare-history` endpoint.
16
+ - **`specshield share <id>`** generates a public `/r/<token>` link for a saved
17
+ comparison via the new `/api/share-links` endpoint. `--expires <days>` sets an
18
+ expiry. `specshield share <base> <target>` still compares-and-shares in one step.
19
+ - **`compare --remote` now returns a `historyId`** when run with an API key —
20
+ feed it straight into `specshield share` (the CI "compare → share link in PR
21
+ comment" flow).
22
+
23
+ ### Changed
24
+
25
+ - `compare --remote`'s footer is accurate again: with an API key the comparison
26
+ is saved (no note); without one it notes the diff ran but wasn't saved.
27
+ - Removed the "currently UI-only" notices from `history` / `share` (and the
28
+ matching README callouts) now that both work.
29
+
30
+ ### Fixed
31
+
32
+ - `specshield share` validates the report id is a positive number and gives a
33
+ clear message (pointing at `specshield history`) instead of a server 400.
34
+
3
35
  ## 3.2.3 — 2026-05-28 — BDCT fidelity: HAR capture + provider conformance
4
36
 
5
37
  Two new `bdct` sub-commands that close the contract-fidelity gap PactFlow
package/README.md CHANGED
@@ -252,29 +252,16 @@ specshield compare base.yaml target.yaml --remote --fail-on-breaking
252
252
  specshield compare base.yaml target.yaml --remote --json --output result.json
253
253
  ```
254
254
 
255
- > **History note:** `--remote` runs the diff on our servers but currently
256
- > does **not** persist the result to your comparison history (saving is
257
- > UI-only right now see the
258
- > [Comparison History](#comparison-history) section below). To save a
259
- > comparison, run it from the dashboard at
260
- > [specshield.io/account/compare](https://specshield.io/account/compare).
261
- > A CLI-driven save is on the roadmap.
255
+ > **History note:** with an API key set (run `specshield login`, or pass
256
+ > `--api-key` / `SPECSHIELD_API_KEY`), `--remote` saves each comparison to
257
+ > your history and the response includes a `historyId` you can hand to
258
+ > `specshield share`. Without a key the diff still runs — it just isn't saved.
262
259
 
263
260
  ## Comparison History
264
261
 
265
- > **Status: currently UI-only.** Comparison history is available in the
266
- > dashboard at [specshield.io/account/history](https://specshield.io/account/history).
267
- > The `specshield history` CLI command exists but currently exits with a
268
- > "use the dashboard" message — a CLI-callable API endpoint is on the
269
- > roadmap (the backend's `/me/*` routes require browser-session JWT auth,
270
- > not the CLI's API key). The dashboard shows the same data and adds
271
- > per-comparison drill-down.
272
-
273
- Every comparison you run **in the dashboard's `/account/compare` page** is
274
- saved to your account. Useful for tracking API drift over time across team
275
- members + machines.
276
-
277
- The CLI command (once the backend endpoint ships):
262
+ Every comparison you run with `--remote` (and every one from the dashboard) is
263
+ saved to your account — useful for tracking API drift over time across team
264
+ members and machines. List them straight from the CLI:
278
265
 
279
266
  ```bash
280
267
  specshield history # last 20 comparisons
@@ -282,7 +269,11 @@ specshield history --limit 50
282
269
  specshield history --json
283
270
  ```
284
271
 
285
- Expected output (once enabled):
272
+ The dashboard at
273
+ [specshield.io/account/history](https://specshield.io/account/history) shows
274
+ the same data with per-comparison drill-down.
275
+
276
+ Example output:
286
277
 
287
278
  ```
288
279
  Your recent comparisons
@@ -294,15 +285,13 @@ Expected output (once enabled):
294
285
 
295
286
  ## Share a Comparison
296
287
 
297
- > **Status: currently UI-only.** Share links are generated from the
298
- > dashboard open any comparison at
299
- > [specshield.io/account/history](https://specshield.io/account/history)
300
- > and click the "Share" button. Same `/r/<token>` URL shape. The
301
- > `specshield share` CLI command currently exits with a "use the dashboard"
302
- > message; CLI integration ships when the backend exposes a CLI-callable
303
- > share-link endpoint.
288
+ Generate a public `/r/<token>` link for any comparison paste it into Slack, a
289
+ GitHub PR comment, or a Jira ticket and anyone can view the diff without a
290
+ SpecShield account. You can also create links from the dashboard (open a
291
+ comparison at [specshield.io/account/history](https://specshield.io/account/history)
292
+ "Share").
304
293
 
305
- Once enabled, the CLI usage will be:
294
+ Usage:
306
295
 
307
296
  ```bash
308
297
  # Share an existing report by ID (from `specshield history`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.2.6",
3
+ "version": "3.2.7",
4
4
  "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.",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -97,18 +97,16 @@ compare
97
97
  logger.info(`Results saved to ${options.output}`);
98
98
  }
99
99
 
100
- // Honest note for --remote when the backend didn't return a history
101
- // ID. The `/compare` endpoint runs the diff but does NOT persist to
102
- // the user's comparison history when called with X-Api-Key auth —
103
- // that's a UI-only path right now (see the share/history commands).
104
- // When/if the backend starts returning historyId, this note auto-hides.
100
+ // Note for an anonymous --remote: the diff ran but wasn't saved
101
+ // because no API key was sent. An authenticated --remote persists the
102
+ // comparison and the backend returns a historyId, so this note hides.
105
103
  if ((options.remote || options.remoteUrl)
106
104
  && !result.historyId && !result.reportId && !result.id) {
107
105
  const chalk = require('chalk');
108
106
  process.stdout.write('\n' + chalk.gray(
109
- ' Note: --remote ran the diff on our servers but did NOT save it to your\n' +
110
- ' comparison history (history save is currently UI-only). For history, run\n' +
111
- ' the compare in the dashboard at https://specshield.io/account/compare.\n'
107
+ ' Note: --remote ran the diff but did NOT save it to your history\n' +
108
+ ' (no API key). Run "specshield login" (or set SPECSHIELD_API_KEY) to\n' +
109
+ ' save comparisons and unlock "specshield history" / "specshield share".\n'
112
110
  ));
113
111
  }
114
112
  }
@@ -41,25 +41,15 @@ history
41
41
  const spinner = opts.json ? null : ora('Fetching your comparison history...').start();
42
42
  try {
43
43
  const size = parseInt(opts.limit, 10) || 20;
44
- const response = await axios.get(`${opts.apiUrl}/me/compare-history`, {
45
- // Backend uses Spring Data Page conventions: page (0-indexed) + size.
44
+ const response = await axios.get(`${opts.apiUrl}/api/compare-history`, {
45
+ // Backend paginates with page (0-indexed) + size; the /api endpoint
46
+ // returns a PagedResponse envelope ({ content, page, size, ... }).
46
47
  params: { page: 0, size },
47
48
  headers: { 'X-Api-Key': apiKey, 'X-SpecShield-Client': 'cli' },
48
49
  timeout: 10000,
49
50
  });
50
51
  if (spinner) spinner.stop();
51
52
 
52
- // The /me/* routes currently require JWT (browser session) auth — the
53
- // CLI's X-Api-Key isn't accepted, so the backend redirects to /login
54
- // and axios follows the 302 to the SPA's HTML shell. Detect that and
55
- // tell the user where the feature really lives, instead of silently
56
- // returning an empty list.
57
- if (typeof response.data === 'string'
58
- && (response.data.includes('<!doctype') || response.data.includes('<html'))) {
59
- printWebOnlyMessage(opts);
60
- process.exit(2);
61
- }
62
-
63
53
  const items = response.data?.content
64
54
  || response.data?.items
65
55
  || (Array.isArray(response.data) ? response.data : []);
@@ -118,31 +108,4 @@ function printSignupNudge() {
118
108
  console.log('');
119
109
  }
120
110
 
121
- /**
122
- * Surfaced when the backend redirects our API-key request to /login (HTML
123
- * response). The /me/compare-history endpoint is currently JWT-only — the
124
- * web UI signs in via OAuth and uses a browser-session JWT. A CLI-callable
125
- * API endpoint is on the roadmap; until then, the dashboard is the way.
126
- */
127
- function printWebOnlyMessage(opts) {
128
- if (opts.json) {
129
- process.stdout.write(JSON.stringify({
130
- error: 'CLI_UNAVAILABLE',
131
- message: 'Comparison history is currently UI-only. The CLI API endpoint is on the roadmap.',
132
- dashboard: 'https://specshield.io/account/history',
133
- }, null, 2) + '\n');
134
- return;
135
- }
136
- console.log('');
137
- console.log(chalk.bold(' Comparison history is currently UI-only.'));
138
- console.log('');
139
- console.log(' View your comparisons at:');
140
- console.log(' ' + chalk.cyan('https://specshield.io/account/history'));
141
- console.log('');
142
- console.log(chalk.gray(' CLI access is on the roadmap — needs a backend API endpoint that'));
143
- console.log(chalk.gray(' accepts the CLI\'s X-Api-Key auth (currently `/me/*` routes require'));
144
- console.log(chalk.gray(' the browser-session JWT). Until then, use the dashboard.'));
145
- console.log('');
146
- }
147
-
148
111
  module.exports = history;
@@ -50,14 +50,6 @@ share
50
50
  process.exit(2);
51
51
  }
52
52
 
53
- // The share-link endpoint lives on `/me/*` which currently requires
54
- // browser-session JWT auth — the CLI's X-Api-Key gets redirected to
55
- // /login. Until the backend exposes a CLI-callable endpoint, point
56
- // users at the dashboard rather than letting the request fail opaquely.
57
- printWebOnlyMessage();
58
- process.exit(2);
59
-
60
- // eslint-disable-next-line no-unreachable
61
53
  const spinner = ora('Generating share link...').start();
62
54
  try {
63
55
  const headers = { 'X-Api-Key': apiKey, 'X-SpecShield-Client': 'cli' };
@@ -65,17 +57,17 @@ share
65
57
 
66
58
  let reportId;
67
59
  if (target) {
68
- // Inline-compare path: run a /compare with --remote semantics to
69
- // persist the result, then immediately share the resulting history
70
- // row. Two API calls but keeps the backend share endpoint single-purpose.
60
+ // Inline-compare path: run a /compare (which persists when called with
61
+ // a valid X-Api-Key), then immediately share the resulting history row.
62
+ // Two API calls but keeps the backend share endpoint single-purpose.
71
63
  spinner.text = 'Running remote comparison...';
72
64
  const baseSpec = await loadSpec(reportOrBase);
73
65
  const targetSpec = await loadSpec(target);
74
66
  const compareResp = await axios.post(`${opts.apiUrl}/compare`,
75
67
  { baseSpec, targetSpec },
76
68
  { headers, timeout: 30000 });
77
- // The /compare endpoint returns the diff; the persisted history ID is
78
- // on the response (added by CompareHistoryService).
69
+ // The /compare endpoint returns the diff plus the persisted row's
70
+ // historyId (present because we authenticated with X-Api-Key).
79
71
  reportId = compareResp.data?.historyId || compareResp.data?.reportId;
80
72
  if (!reportId) {
81
73
  throw new Error('Comparison succeeded but no history ID was returned — cannot create share link.');
@@ -85,8 +77,17 @@ share
85
77
  reportId = reportOrBase;
86
78
  }
87
79
 
88
- const response = await axios.post(`${opts.apiUrl}/me/share-links`,
89
- { reportId, expiresInDays },
80
+ // The backend's reportId is a numeric history id. Validate before the
81
+ // call so a typo gets a clear message instead of a 400 from the server.
82
+ const numericReportId = Number(reportId);
83
+ if (!Number.isInteger(numericReportId) || numericReportId <= 0) {
84
+ spinner.stop();
85
+ logger.error(`Invalid report ID "${reportId}". Expected a numeric comparison ID — see "specshield history".`);
86
+ process.exit(2);
87
+ }
88
+
89
+ const response = await axios.post(`${opts.apiUrl}/api/share-links`,
90
+ { reportId: numericReportId, expiresInDays },
90
91
  { headers, timeout: 15000 });
91
92
  spinner.stop();
92
93
 
@@ -126,23 +127,4 @@ function printSignupNudge() {
126
127
  console.log('');
127
128
  }
128
129
 
129
- /**
130
- * Surfaced because `/me/share-links` currently requires JWT (web sign-in).
131
- * The CLI X-Api-Key gets redirected to /login. Until a CLI-callable endpoint
132
- * exists, the dashboard is the way.
133
- */
134
- function printWebOnlyMessage() {
135
- console.log('');
136
- console.log(chalk.bold(' Share links are currently UI-only.'));
137
- console.log('');
138
- console.log(' Generate a share link at:');
139
- console.log(' ' + chalk.cyan('https://specshield.io/account/history'));
140
- console.log(' (open any comparison → click "Share").');
141
- console.log('');
142
- console.log(chalk.gray(' CLI access is on the roadmap — needs a backend API endpoint that'));
143
- console.log(chalk.gray(' accepts the CLI\'s X-Api-Key auth (currently `/me/*` routes require'));
144
- console.log(chalk.gray(' the browser-session JWT). Until then, use the dashboard.'));
145
- console.log('');
146
- }
147
-
148
130
  module.exports = share;