specshield 3.0.0 โ†’ 3.1.1

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/README.md CHANGED
@@ -117,6 +117,26 @@ specshield compare base.yaml target.yaml --fail-on-breaking
117
117
 
118
118
  That's it. Works with any OpenAPI 3.x YAML or JSON spec.
119
119
 
120
+ **For BDCT**, run the wizard once and never re-type the same flags again:
121
+
122
+ ```bash
123
+ specshield init
124
+ ```
125
+
126
+ It autodetects your OpenAPI spec, your service name (from `package.json` /
127
+ `pyproject.toml` / `pom.xml` / `go.mod`), your git branch, and your
128
+ default environment, asks you a few questions, and writes a
129
+ **`.specshield.yml`** at the project root. Every subsequent
130
+ `specshield bdct ...` invocation reads this file, so your CI commands
131
+ collapse to:
132
+
133
+ ```bash
134
+ specshield bdct publish-provider --version $GITHUB_SHA
135
+ specshield bdct can-i-deploy --version $GITHUB_SHA
136
+ ```
137
+
138
+ See [ยง specshield init](#specshield-init--first-run-setup-wizard) below.
139
+
120
140
  ---
121
141
 
122
142
  ## ๐Ÿš€ Create Your Free Account
@@ -349,6 +369,83 @@ github:
349
369
 
350
370
  ---
351
371
 
372
+ ## `specshield init` โ€” first-run setup wizard
373
+
374
+ Run once at the root of your project. The wizard:
375
+
376
+ 1. Detects your OpenAPI spec (looks under `api/`, `spec/`, `docs/`, repo root).
377
+ 2. Detects your service name (`package.json`, `pyproject.toml`, `pom.xml`, `go.mod`, `Cargo.toml`, or directory name).
378
+ 3. Detects your git branch and suggests `production` for `main`/`master`, `staging` otherwise.
379
+ 4. Asks whether this project is a provider, a consumer, or both.
380
+ 5. Asks for your org key (autocompletes from your account if you're already signed in).
381
+ 6. Validates / stores your API key.
382
+ 7. Writes **`.specshield.yml`** and, optionally, a starter **`.github/workflows/specshield-bdct.yml`** that uses [`specshield26/bdct-action@v1`](https://github.com/marketplace/actions/specshield-bdct).
383
+
384
+ ```bash
385
+ specshield init
386
+ ```
387
+
388
+ ### Example `.specshield.yml`
389
+
390
+ ```yaml
391
+ schemaVersion: 1
392
+
393
+ failOnBreaking: true
394
+ severity: error
395
+
396
+ bdct:
397
+ org: acme-pay
398
+ environment: staging
399
+
400
+ provider:
401
+ name: payment-service
402
+ spec: api/openapi.yaml
403
+
404
+ # consumer (optional โ€” present when --kind=consumer or --kind=both):
405
+ # consumer:
406
+ # name: checkout-ui
407
+ # provider: payment-service
408
+ # contract: contracts/payment-service.yaml
409
+ # format: OPENAPI
410
+
411
+ github:
412
+ specPath: api/openapi.yaml
413
+ failOnBreaking: true
414
+ commentOnPr: true
415
+ ```
416
+
417
+ CLI flags **always** override this file. Paths in the file are resolved
418
+ relative to the file's own directory, so you can run `specshield bdct ...`
419
+ from any subdirectory of your project.
420
+
421
+ ### Non-interactive (scriptable) mode
422
+
423
+ ```bash
424
+ specshield init --no-interactive \
425
+ --kind provider \
426
+ --org acme-pay \
427
+ --provider payment-service \
428
+ --spec api/openapi.yaml \
429
+ --env staging \
430
+ --write-workflow
431
+ ```
432
+
433
+ ### Other useful flags
434
+
435
+ | Flag | Purpose |
436
+ | --- | --- |
437
+ | `--print` | Detect everything, print the proposed YAML, write nothing. Good for `--dry-run` review in CI. |
438
+ | `--force` | Skip the overwrite-confirmation if `.specshield.yml` already exists. |
439
+ | `--server <url>` | Use a non-default SpecShield endpoint (self-hosted / staging). |
440
+ | `--write-workflow` | Also write a starter GitHub Actions workflow under `.github/workflows/`. |
441
+
442
+ > **What is _not_ written into `.specshield.yml`:** your API key. It is
443
+ > stored in `~/.specshield/config.json` (set by `specshield login`) or
444
+ > read from `SPECSHIELD_API_KEY` in CI. The project file is meant to be
445
+ > committed; never commit a secret into it.
446
+
447
+ ---
448
+
352
449
  ## Bi-Directional Contract Testing (BDCT)
353
450
 
354
451
  **Spec-to-spec contract testing โ€” no running services required.**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "specshield",
3
- "version": "3.0.0",
4
- "description": "CLI for OpenAPI breaking change detection and bi-directional contract verification โ€” with can-i-deploy gating and GitHub PR checks.",
3
+ "version": "3.1.1",
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": {
7
7
  "specshield": "./bin/specshield.js"
@@ -13,36 +13,36 @@
13
13
  "lint": "eslint src tests --ext .js"
14
14
  },
15
15
  "keywords": [
16
- "openapi",
17
- "swagger",
18
- "api-diff",
19
- "openapi-diff",
20
- "swagger-diff",
21
- "breaking-change-detection",
22
- "api-breaking-changes",
23
- "api-compatibility",
24
- "contract-testing",
25
- "consumer-driven-contract",
26
- "api-contract",
27
- "contract-verification",
28
- "can-i-deploy",
29
- "ci-cd",
30
- "github-actions",
31
- "cli",
32
- "developer-tools",
33
- "openapi-compare",
34
- "swagger-compare",
35
- "pact-alternative",
36
- "api-contract-testing",
37
- "microservices",
38
- "api-quality",
39
- "api-governance",
40
- "bi-directional-contract-testing",
41
- "bdct",
42
- "pact-json",
43
- "github-app",
44
- "api-drift"
45
- ],
16
+ "openapi",
17
+ "swagger",
18
+ "api-diff",
19
+ "openapi-diff",
20
+ "swagger-diff",
21
+ "breaking-change-detection",
22
+ "api-breaking-changes",
23
+ "api-compatibility",
24
+ "contract-testing",
25
+ "consumer-driven-contract",
26
+ "api-contract",
27
+ "contract-verification",
28
+ "can-i-deploy",
29
+ "ci-cd",
30
+ "github-actions",
31
+ "cli",
32
+ "developer-tools",
33
+ "openapi-compare",
34
+ "swagger-compare",
35
+ "pact-alternative",
36
+ "api-contract-testing",
37
+ "microservices",
38
+ "api-quality",
39
+ "api-governance",
40
+ "bi-directional-contract-testing",
41
+ "bdct",
42
+ "pact-json",
43
+ "github-app",
44
+ "api-drift"
45
+ ],
46
46
  "license": "MIT",
47
47
  "files": [
48
48
  "bin",
@@ -56,7 +56,8 @@
56
56
  "commander": "^12.0.0",
57
57
  "fs-extra": "^11.2.0",
58
58
  "js-yaml": "^4.1.0",
59
- "ora": "^5.4.1"
59
+ "ora": "^5.4.1",
60
+ "prompts": "^2.4.2"
60
61
  },
61
62
  "devDependencies": {
62
63
  "jest": "^29.7.0"
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { Command } = require('commander');
4
4
  const { version } = require('../package.json');
5
5
  const compareCommand = require('./commands/compare');
6
+ const initCommand = require('./commands/init');
6
7
  const loginCommand = require('./commands/login');
7
8
  const logoutCommand = require('./commands/logout');
8
9
  const bdctCommand = require('./commands/bdct');
@@ -19,6 +20,7 @@ program
19
20
  .enablePositionalOptions();
20
21
 
21
22
  program.addCommand(compareCommand);
23
+ program.addCommand(initCommand);
22
24
  program.addCommand(loginCommand);
23
25
  program.addCommand(logoutCommand);
24
26
  program.addCommand(bdctCommand);
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const fsExtra = require('fs-extra');
8
8
  const logger = require('../utils/logger');
9
9
  const { getStoredApiKey } = require('../config/localConfig');
10
+ const { applyBdctDefaults } = require('../core/projectConfig');
10
11
  const {
11
12
  publishProviderSpec,
12
13
  publishConsumerContract,
@@ -31,6 +32,24 @@ function requireToken(token) {
31
32
  }
32
33
  }
33
34
 
35
+ /**
36
+ * Fill missing CLI options from `.specshield.yml` if one is present, then
37
+ * verify every required field for `command` is set. Exits 2 with a friendly
38
+ * message if anything is missing.
39
+ */
40
+ function withProjectDefaults(opts, command) {
41
+ try {
42
+ applyBdctDefaults(opts, command);
43
+ } catch (err) {
44
+ if (err.code === 'MISSING_REQUIRED_OPTIONS') {
45
+ logger.error(err.message);
46
+ process.exit(2);
47
+ }
48
+ throw err;
49
+ }
50
+ return opts;
51
+ }
52
+
34
53
  function fmtDate(iso) {
35
54
  if (!iso) return chalk.gray('โ€”');
36
55
  try {
@@ -99,16 +118,17 @@ function printTable(headers, rows) {
99
118
 
100
119
  const publishProviderCommand = new Command('publish-provider')
101
120
  .description('Publish a provider OpenAPI spec to the BDCT registry')
102
- .requiredOption('--spec <path>', 'Path to provider spec file (YAML or JSON)')
103
- .requiredOption('--provider <name>', 'Provider service name')
104
- .requiredOption('--version <ver>', 'Provider version tag')
105
- .requiredOption('--org <key>', 'Organization key')
121
+ .option('--spec <path>', 'Path to provider spec file (YAML or JSON)')
122
+ .option('--provider <name>', 'Provider service name')
123
+ .option('--version <ver>', 'Provider version tag')
124
+ .option('--org <key>', 'Organization key')
106
125
  .option('--env <environment>', 'Environment label (e.g. staging, production)')
107
126
  .option('--branch <branch>', 'Git branch name')
108
- .option('--json', 'Output raw JSON')
109
- .option('--server <url>', 'SpecShield server URL')
127
+ .option('--json', 'Output raw JSON')
128
+ .option('--server <url>', 'SpecShield server URL')
110
129
  .option('--api-token <token>', 'API token (overrides env / stored config)')
111
130
  .action(async (opts) => {
131
+ withProjectDefaults(opts, 'publish-provider');
112
132
  const token = await resolveApiToken(opts);
113
133
  requireToken(token);
114
134
 
@@ -168,16 +188,17 @@ const publishProviderCommand = new Command('publish-provider')
168
188
 
169
189
  const publishConsumerCommand = new Command('publish-consumer')
170
190
  .description('Publish a consumer contract to the BDCT registry')
171
- .requiredOption('--contract <path>', 'Path to consumer contract file (OpenAPI YAML/JSON or Pact JSON)')
172
- .requiredOption('--consumer <name>', 'Consumer service name')
173
- .requiredOption('--provider <name>', 'Provider service name')
174
- .requiredOption('--version <ver>', 'Consumer version tag')
175
- .requiredOption('--org <key>', 'Organization key')
176
- .option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
177
- .option('--json', 'Output raw JSON')
178
- .option('--server <url>', 'SpecShield server URL')
191
+ .option('--contract <path>', 'Path to consumer contract file (OpenAPI YAML/JSON or Pact JSON)')
192
+ .option('--consumer <name>', 'Consumer service name')
193
+ .option('--provider <name>', 'Provider service name')
194
+ .option('--version <ver>', 'Consumer version tag')
195
+ .option('--org <key>', 'Organization key')
196
+ .option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
197
+ .option('--json', 'Output raw JSON')
198
+ .option('--server <url>', 'SpecShield server URL')
179
199
  .option('--api-token <token>', 'API token (overrides env / stored config)')
180
200
  .action(async (opts) => {
201
+ withProjectDefaults(opts, 'publish-consumer');
181
202
  const token = await resolveApiToken(opts);
182
203
  requireToken(token);
183
204
 
@@ -240,16 +261,17 @@ const publishConsumerCommand = new Command('publish-consumer')
240
261
 
241
262
  const verifyCommand = new Command('verify')
242
263
  .description('Verify consumer-provider contract compatibility')
243
- .requiredOption('--consumer <name>', 'Consumer service name')
244
- .requiredOption('--provider <name>', 'Provider service name')
245
- .requiredOption('--consumer-version <ver>', 'Consumer version to verify')
246
- .requiredOption('--provider-version <ver>', 'Provider version to verify against')
247
- .requiredOption('--org <key>', 'Organization key')
248
- .option('--env <environment>', 'Environment label')
249
- .option('--json', 'Output raw JSON')
250
- .option('--server <url>', 'SpecShield server URL')
251
- .option('--api-token <token>', 'API token')
264
+ .option('--consumer <name>', 'Consumer service name')
265
+ .option('--provider <name>', 'Provider service name')
266
+ .option('--consumer-version <ver>', 'Consumer version to verify')
267
+ .option('--provider-version <ver>', 'Provider version to verify against')
268
+ .option('--org <key>', 'Organization key')
269
+ .option('--env <environment>', 'Environment label')
270
+ .option('--json', 'Output raw JSON')
271
+ .option('--server <url>', 'SpecShield server URL')
272
+ .option('--api-token <token>', 'API token')
252
273
  .action(async (opts) => {
274
+ withProjectDefaults(opts, 'verify');
253
275
  const token = await resolveApiToken(opts);
254
276
  requireToken(token);
255
277
 
@@ -266,14 +288,14 @@ const verifyCommand = new Command('verify')
266
288
  });
267
289
  if (spinner) spinner.stop();
268
290
 
291
+ const status = String(result.status || result.result || '').toUpperCase();
292
+ const success = status === 'COMPATIBLE';
293
+
269
294
  if (opts.json) {
270
295
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
271
- return;
296
+ process.exit(success ? 0 : 1);
272
297
  }
273
298
 
274
- const status = String(result.status || result.result || '').toUpperCase();
275
- const success = status === 'COMPATIBLE';
276
-
277
299
  process.stdout.write('\n');
278
300
  if (success) {
279
301
  process.stdout.write(chalk.green.bold(' โœ” COMPATIBLE') + '\n');
@@ -327,14 +349,15 @@ const verifyCommand = new Command('verify')
327
349
 
328
350
  const canIDeployCommand = new Command('can-i-deploy')
329
351
  .description('Check if a service version is safe to deploy')
330
- .requiredOption('--service <name>', 'Service name (consumer or provider)')
331
- .requiredOption('--version <ver>', 'Service version to check')
332
- .requiredOption('--org <key>', 'Organization key')
352
+ .option('--service <name>', 'Service name (consumer or provider)')
353
+ .option('--version <ver>', 'Service version to check')
354
+ .option('--org <key>', 'Organization key')
333
355
  .option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
334
- .option('--json', 'Output raw JSON')
335
- .option('--server <url>', 'SpecShield server URL')
356
+ .option('--json', 'Output raw JSON')
357
+ .option('--server <url>', 'SpecShield server URL')
336
358
  .option('--api-token <token>', 'API token')
337
359
  .action(async (opts) => {
360
+ withProjectDefaults(opts, 'can-i-deploy');
338
361
  const token = await resolveApiToken(opts);
339
362
  requireToken(token);
340
363
 
@@ -349,14 +372,14 @@ const canIDeployCommand = new Command('can-i-deploy')
349
372
  });
350
373
  if (spinner) spinner.stop();
351
374
 
375
+ const deployable = result.deployable ?? result.allowed ?? false;
376
+ const envLabel = opts.env ? ` in ${opts.env}` : '';
377
+
352
378
  if (opts.json) {
353
379
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
354
- return;
380
+ process.exit(deployable ? 0 : 1);
355
381
  }
356
382
 
357
- const deployable = result.deployable ?? result.allowed ?? false;
358
- const envLabel = opts.env ? ` in ${opts.env}` : '';
359
-
360
383
  process.stdout.write('\n');
361
384
  if (deployable) {
362
385
  process.stdout.write(chalk.green.bold(' โœ” PASS') + chalk.white(`: ${opts.service} v${opts.version} is deployable${envLabel}\n`));
@@ -400,7 +423,7 @@ const canIDeployCommand = new Command('can-i-deploy')
400
423
 
401
424
  const listCommand = new Command('list')
402
425
  .description('List BDCT verification history')
403
- .requiredOption('--org <key>', 'Organization key')
426
+ .option('--org <key>', 'Organization key')
404
427
  .option('--consumer <name>', 'Filter by consumer service name')
405
428
  .option('--provider <name>', 'Filter by provider service name')
406
429
  .option('--env <environment>', 'Filter by environment')
@@ -410,6 +433,7 @@ const listCommand = new Command('list')
410
433
  .option('--server <url>', 'SpecShield server URL')
411
434
  .option('--api-token <token>', 'API token')
412
435
  .action(async (opts) => {
436
+ withProjectDefaults(opts, 'list');
413
437
  const token = await resolveApiToken(opts);
414
438
  requireToken(token);
415
439
 
@@ -473,12 +497,13 @@ const listCommand = new Command('list')
473
497
 
474
498
  const matrixCommand = new Command('matrix')
475
499
  .description('Show ASCII compatibility matrix of consumers vs providers')
476
- .requiredOption('--org <key>', 'Organization key')
500
+ .option('--org <key>', 'Organization key')
477
501
  .option('--env <environment>', 'Environment label')
478
502
  .option('--json', 'Output raw JSON')
479
503
  .option('--server <url>', 'SpecShield server URL')
480
504
  .option('--api-token <token>', 'API token')
481
505
  .action(async (opts) => {
506
+ withProjectDefaults(opts, 'matrix');
482
507
  const token = await resolveApiToken(opts);
483
508
  requireToken(token);
484
509
 
@@ -538,12 +563,13 @@ const matrixCommand = new Command('matrix')
538
563
 
539
564
  const listProvidersCommand = new Command('list-providers')
540
565
  .description('List published provider specs')
541
- .requiredOption('--org <key>', 'Organization key')
566
+ .option('--org <key>', 'Organization key')
542
567
  .option('--provider <name>', 'Filter by provider service name')
543
- .option('--json', 'Output raw JSON')
544
- .option('--server <url>', 'SpecShield server URL')
568
+ .option('--json', 'Output raw JSON')
569
+ .option('--server <url>', 'SpecShield server URL')
545
570
  .option('--api-token <token>', 'API token')
546
571
  .action(async (opts) => {
572
+ withProjectDefaults(opts, 'list-providers');
547
573
  const token = await resolveApiToken(opts);
548
574
  requireToken(token);
549
575
 
@@ -601,13 +627,14 @@ const listProvidersCommand = new Command('list-providers')
601
627
 
602
628
  const listConsumersCommand = new Command('list-consumers')
603
629
  .description('List published consumer contracts')
604
- .requiredOption('--org <key>', 'Organization key')
630
+ .option('--org <key>', 'Organization key')
605
631
  .option('--consumer <name>', 'Filter by consumer service name')
606
632
  .option('--provider <name>', 'Filter by provider service name')
607
- .option('--json', 'Output raw JSON')
608
- .option('--server <url>', 'SpecShield server URL')
633
+ .option('--json', 'Output raw JSON')
634
+ .option('--server <url>', 'SpecShield server URL')
609
635
  .option('--api-token <token>', 'API token')
610
636
  .action(async (opts) => {
637
+ withProjectDefaults(opts, 'list-consumers');
611
638
  const token = await resolveApiToken(opts);
612
639
  requireToken(token);
613
640