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 +32 -0
- package/README.md +18 -29
- package/package.json +1 -1
- package/src/commands/compare.js +6 -8
- package/src/commands/history.js +3 -40
- package/src/commands/share.js +16 -34
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:**
|
|
256
|
-
>
|
|
257
|
-
>
|
|
258
|
-
>
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|
package/src/commands/compare.js
CHANGED
|
@@ -97,18 +97,16 @@ compare
|
|
|
97
97
|
logger.info(`Results saved to ${options.output}`);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// the
|
|
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
|
|
110
|
-
'
|
|
111
|
-
'
|
|
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
|
}
|
package/src/commands/history.js
CHANGED
|
@@ -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}/
|
|
45
|
-
// Backend
|
|
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;
|
package/src/commands/share.js
CHANGED
|
@@ -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
|
|
69
|
-
//
|
|
70
|
-
//
|
|
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
|
|
78
|
-
//
|
|
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
|
-
|
|
89
|
-
|
|
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;
|