specshield 2.0.1 → 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.
@@ -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 {
@@ -51,6 +70,26 @@ function hr() {
51
70
  return chalk.gray(' ─────────────────────────────────────────────────────');
52
71
  }
53
72
 
73
+ /**
74
+ * Flatten a verification result's mismatches.
75
+ * Current backend returns `resultJson` (JSON string of [{endpoint,status,mismatches:[...]}]).
76
+ * Tolerate older shapes that put a flat array on `issues` or `mismatches`.
77
+ */
78
+ function flattenMismatches(result) {
79
+ if (!result) return [];
80
+ if (Array.isArray(result.issues)) return result.issues;
81
+ if (Array.isArray(result.mismatches)) return result.mismatches;
82
+ if (typeof result.resultJson === 'string' && result.resultJson.length > 0) {
83
+ try {
84
+ const parsed = JSON.parse(result.resultJson);
85
+ if (Array.isArray(parsed)) {
86
+ return parsed.flatMap(ep => Array.isArray(ep.mismatches) ? ep.mismatches : []);
87
+ }
88
+ } catch { /* fall through */ }
89
+ }
90
+ return [];
91
+ }
92
+
54
93
  /** Strip ANSI escape codes for length measurement */
55
94
  function stripAnsi(str) {
56
95
  return str.replace(/\[[0-9;]*m/g, '');
@@ -79,16 +118,17 @@ function printTable(headers, rows) {
79
118
 
80
119
  const publishProviderCommand = new Command('publish-provider')
81
120
  .description('Publish a provider OpenAPI spec to the BDCT registry')
82
- .requiredOption('--spec <path>', 'Path to provider spec file (YAML or JSON)')
83
- .requiredOption('--provider <name>', 'Provider service name')
84
- .requiredOption('--version <ver>', 'Provider version tag')
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')
85
125
  .option('--env <environment>', 'Environment label (e.g. staging, production)')
86
- .option('--org <key>', 'Organization key')
87
126
  .option('--branch <branch>', 'Git branch name')
88
- .option('--json', 'Output raw JSON')
89
- .option('--server <url>', 'SpecShield server URL')
127
+ .option('--json', 'Output raw JSON')
128
+ .option('--server <url>', 'SpecShield server URL')
90
129
  .option('--api-token <token>', 'API token (overrides env / stored config)')
91
130
  .action(async (opts) => {
131
+ withProjectDefaults(opts, 'publish-provider');
92
132
  const token = await resolveApiToken(opts);
93
133
  requireToken(token);
94
134
 
@@ -110,12 +150,12 @@ const publishProviderCommand = new Command('publish-provider')
110
150
 
111
151
  try {
112
152
  const result = await publishProviderSpec(opts.server, token, {
113
- provider: opts.provider,
114
- version: opts.version,
153
+ orgKey: opts.org,
154
+ providerName: opts.provider,
155
+ version: opts.version,
115
156
  specContent,
116
- env: opts.env || null,
117
- orgKey: opts.org || null,
118
- branch: opts.branch || null,
157
+ environment: opts.env || null,
158
+ branch: opts.branch || null,
119
159
  });
120
160
  if (spinner) spinner.stop();
121
161
 
@@ -131,7 +171,11 @@ const publishProviderCommand = new Command('publish-provider')
131
171
  process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
132
172
  process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
133
173
  if (opts.env) process.stdout.write(` Environment : ${opts.env}\n`);
134
- if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
174
+ const publishedAt = result.createdAt || result.publishedAt;
175
+ if (publishedAt) process.stdout.write(` Published At: ${fmtDate(publishedAt)}\n`);
176
+ if (typeof result.verificationsTriggered === 'number') {
177
+ process.stdout.write(` Verifications triggered: ${chalk.cyan(result.verificationsTriggered)}\n`);
178
+ }
135
179
  process.stdout.write('\n');
136
180
  } catch (err) {
137
181
  if (spinner) spinner.fail('Publish failed');
@@ -144,16 +188,17 @@ const publishProviderCommand = new Command('publish-provider')
144
188
 
145
189
  const publishConsumerCommand = new Command('publish-consumer')
146
190
  .description('Publish a consumer contract to the BDCT registry')
147
- .requiredOption('--contract <path>', 'Path to consumer contract file (OpenAPI YAML/JSON or Pact JSON)')
148
- .requiredOption('--consumer <name>', 'Consumer service name')
149
- .requiredOption('--provider <name>', 'Provider service name')
150
- .requiredOption('--version <ver>', 'Consumer version tag')
151
- .option('--org <key>', 'Organization key')
152
- .option('--format <fmt>', 'Contract format: OPENAPI | PACT', 'OPENAPI')
153
- .option('--json', 'Output raw JSON')
154
- .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')
155
199
  .option('--api-token <token>', 'API token (overrides env / stored config)')
156
200
  .action(async (opts) => {
201
+ withProjectDefaults(opts, 'publish-consumer');
157
202
  const token = await resolveApiToken(opts);
158
203
  requireToken(token);
159
204
 
@@ -175,12 +220,12 @@ const publishConsumerCommand = new Command('publish-consumer')
175
220
 
176
221
  try {
177
222
  const result = await publishConsumerContract(opts.server, token, {
178
- consumer: opts.consumer,
179
- provider: opts.provider,
223
+ orgKey: opts.org,
224
+ consumerName: opts.consumer,
225
+ providerName: opts.provider,
180
226
  version: opts.version,
181
227
  contractContent,
182
- orgKey: opts.org || null,
183
- format: opts.format || 'OPENAPI',
228
+ contractFormat: opts.format || 'OPENAPI',
184
229
  });
185
230
  if (spinner) spinner.stop();
186
231
 
@@ -197,7 +242,11 @@ const publishConsumerCommand = new Command('publish-consumer')
197
242
  process.stdout.write(` Provider : ${chalk.white(opts.provider)}\n`);
198
243
  process.stdout.write(` Version : ${chalk.cyan(opts.version)}\n`);
199
244
  process.stdout.write(` Format : ${opts.format || 'OPENAPI'}\n`);
200
- if (result.publishedAt) process.stdout.write(` Published At: ${fmtDate(result.publishedAt)}\n`);
245
+ const consumerPublishedAt = result.createdAt || result.publishedAt;
246
+ if (consumerPublishedAt) process.stdout.write(` Published At: ${fmtDate(consumerPublishedAt)}\n`);
247
+ if (typeof result.verificationsTriggered === 'number') {
248
+ process.stdout.write(` Verifications triggered: ${chalk.cyan(result.verificationsTriggered)}\n`);
249
+ }
201
250
  process.stdout.write('\n');
202
251
  process.stdout.write(chalk.gray(` ➜ Run: specshield bdct verify --consumer ${opts.consumer} --provider ${opts.provider}\n`));
203
252
  process.stdout.write('\n');
@@ -212,16 +261,17 @@ const publishConsumerCommand = new Command('publish-consumer')
212
261
 
213
262
  const verifyCommand = new Command('verify')
214
263
  .description('Verify consumer-provider contract compatibility')
215
- .requiredOption('--consumer <name>', 'Consumer service name')
216
- .requiredOption('--provider <name>', 'Provider service name')
217
- .option('--consumer-version <ver>', 'Consumer version to verify')
218
- .option('--provider-version <ver>', 'Provider version to verify against')
219
- .option('--env <environment>', 'Environment label')
220
- .option('--org <key>', 'Organization key')
221
- .option('--json', 'Output raw JSON')
222
- .option('--server <url>', 'SpecShield server URL')
223
- .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')
224
273
  .action(async (opts) => {
274
+ withProjectDefaults(opts, 'verify');
225
275
  const token = await resolveApiToken(opts);
226
276
  requireToken(token);
227
277
 
@@ -229,23 +279,23 @@ const verifyCommand = new Command('verify')
229
279
 
230
280
  try {
231
281
  const result = await verify(opts.server, token, {
232
- consumer: opts.consumer,
233
- provider: opts.provider,
234
- consumerVersion: opts.consumerVersion || null,
235
- providerVersion: opts.providerVersion || null,
236
- env: opts.env || null,
237
- orgKey: opts.org || null,
282
+ orgKey: opts.org,
283
+ consumerName: opts.consumer,
284
+ consumerVersion: opts.consumerVersion,
285
+ providerName: opts.provider,
286
+ providerVersion: opts.providerVersion,
287
+ environment: opts.env || null,
238
288
  });
239
289
  if (spinner) spinner.stop();
240
290
 
291
+ const status = String(result.status || result.result || '').toUpperCase();
292
+ const success = status === 'COMPATIBLE';
293
+
241
294
  if (opts.json) {
242
295
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
243
- return;
296
+ process.exit(success ? 0 : 1);
244
297
  }
245
298
 
246
- const status = String(result.status || result.result || '').toUpperCase();
247
- const success = status === 'COMPATIBLE';
248
-
249
299
  process.stdout.write('\n');
250
300
  if (success) {
251
301
  process.stdout.write(chalk.green.bold(' ✔ COMPATIBLE') + '\n');
@@ -262,14 +312,26 @@ const verifyCommand = new Command('verify')
262
312
  process.stdout.write(` Verified At : ${fmtDate(result.verifiedAt || result.completedAt)}\n`);
263
313
  }
264
314
 
265
- const issues = result.issues || result.mismatches || [];
315
+ // Backend returns resultJson as a JSON string of [{endpoint, status, mismatches:[...]}].
316
+ // Older shapes also accepted: top-level issues / mismatches arrays.
317
+ const issues = flattenMismatches(result);
266
318
  if (issues.length > 0) {
267
319
  process.stdout.write('\n');
268
320
  process.stdout.write(chalk.red.bold(' Issues') + '\n');
269
321
  process.stdout.write(hr() + '\n');
270
322
  for (const issue of issues) {
271
- process.stdout.write(` ${chalk.red('●')} ${chalk.bold(issue.type || issue.mismatchType || 'MISMATCH')} at ${chalk.gray(issue.path || '$')}\n`);
272
- if (issue.message) process.stdout.write(` ${chalk.gray(issue.message)}\n`);
323
+ const sev = (issue.severity || 'ERROR').toUpperCase();
324
+ const marker = sev === 'WARNING' ? chalk.yellow('⚠') : chalk.red('●');
325
+ const type = issue.type || issue.mismatchType || 'MISMATCH';
326
+ const loc = issue.field || issue.path || issue.endpoint || '$';
327
+ process.stdout.write(` ${marker} ${chalk.bold(type)} at ${chalk.gray(loc)}\n`);
328
+ if (issue.consumerExpects && issue.providerProvides) {
329
+ process.stdout.write(` ${chalk.gray(`consumer: ${issue.consumerExpects}, provider: ${issue.providerProvides}`)}\n`);
330
+ } else if (issue.consumerExpects) {
331
+ process.stdout.write(` ${chalk.gray(`expected: ${issue.consumerExpects}`)}\n`);
332
+ } else if (issue.message) {
333
+ process.stdout.write(` ${chalk.gray(issue.message)}\n`);
334
+ }
273
335
  }
274
336
  process.stdout.write('\n');
275
337
  }
@@ -287,14 +349,15 @@ const verifyCommand = new Command('verify')
287
349
 
288
350
  const canIDeployCommand = new Command('can-i-deploy')
289
351
  .description('Check if a service version is safe to deploy')
290
- .requiredOption('--service <name>', 'Service name (consumer or provider)')
291
- .requiredOption('--version <ver>', 'Service version to check')
352
+ .option('--service <name>', 'Service name (consumer or provider)')
353
+ .option('--version <ver>', 'Service version to check')
354
+ .option('--org <key>', 'Organization key')
292
355
  .option('--env <environment>', 'Target environment (e.g. qa, staging, production)')
293
- .option('--org <key>', 'Organization key')
294
- .option('--json', 'Output raw JSON')
295
- .option('--server <url>', 'SpecShield server URL')
356
+ .option('--json', 'Output raw JSON')
357
+ .option('--server <url>', 'SpecShield server URL')
296
358
  .option('--api-token <token>', 'API token')
297
359
  .action(async (opts) => {
360
+ withProjectDefaults(opts, 'can-i-deploy');
298
361
  const token = await resolveApiToken(opts);
299
362
  requireToken(token);
300
363
 
@@ -302,21 +365,21 @@ const canIDeployCommand = new Command('can-i-deploy')
302
365
 
303
366
  try {
304
367
  const result = await canIDeploy(opts.server, token, {
368
+ org: opts.org,
305
369
  service: opts.service,
306
370
  version: opts.version,
307
371
  env: opts.env || null,
308
- org: opts.org || null,
309
372
  });
310
373
  if (spinner) spinner.stop();
311
374
 
375
+ const deployable = result.deployable ?? result.allowed ?? false;
376
+ const envLabel = opts.env ? ` in ${opts.env}` : '';
377
+
312
378
  if (opts.json) {
313
379
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
314
- return;
380
+ process.exit(deployable ? 0 : 1);
315
381
  }
316
382
 
317
- const deployable = result.deployable ?? result.allowed ?? false;
318
- const envLabel = opts.env ? ` in ${opts.env}` : '';
319
-
320
383
  process.stdout.write('\n');
321
384
  if (deployable) {
322
385
  process.stdout.write(chalk.green.bold(' ✔ PASS') + chalk.white(`: ${opts.service} v${opts.version} is deployable${envLabel}\n`));
@@ -360,7 +423,7 @@ const canIDeployCommand = new Command('can-i-deploy')
360
423
 
361
424
  const listCommand = new Command('list')
362
425
  .description('List BDCT verification history')
363
- .option('--org <key>', 'Filter by organization key')
426
+ .option('--org <key>', 'Organization key')
364
427
  .option('--consumer <name>', 'Filter by consumer service name')
365
428
  .option('--provider <name>', 'Filter by provider service name')
366
429
  .option('--env <environment>', 'Filter by environment')
@@ -370,6 +433,7 @@ const listCommand = new Command('list')
370
433
  .option('--server <url>', 'SpecShield server URL')
371
434
  .option('--api-token <token>', 'API token')
372
435
  .action(async (opts) => {
436
+ withProjectDefaults(opts, 'list');
373
437
  const token = await resolveApiToken(opts);
374
438
  requireToken(token);
375
439
 
@@ -439,6 +503,7 @@ const matrixCommand = new Command('matrix')
439
503
  .option('--server <url>', 'SpecShield server URL')
440
504
  .option('--api-token <token>', 'API token')
441
505
  .action(async (opts) => {
506
+ withProjectDefaults(opts, 'matrix');
442
507
  const token = await resolveApiToken(opts);
443
508
  requireToken(token);
444
509
 
@@ -456,7 +521,8 @@ const matrixCommand = new Command('matrix')
456
521
  return;
457
522
  }
458
523
 
459
- // Expected matrix shape: { consumers: string[], providers: string[], cells: { [consumer]: { [provider]: string } } }
524
+ // Backend matrix shape: { consumers: string[], providers: string[],
525
+ // cells: { "<consumer>__<provider>": "<STATUS>" } }
460
526
  const consumers = matrix.consumers || [];
461
527
  const providers = matrix.providers || [];
462
528
  const cells = matrix.cells || {};
@@ -475,7 +541,7 @@ const matrixCommand = new Command('matrix')
475
541
  const headers = ['Consumer \\ Provider', ...providers];
476
542
  const rows = consumers.map(consumer => {
477
543
  const providerCells = providers.map(provider => {
478
- const status = (cells[consumer] && cells[consumer][provider]) || 'UNKNOWN';
544
+ const status = cells[`${consumer}__${provider}`] || 'UNKNOWN';
479
545
  return compatBadge(status);
480
546
  });
481
547
  return [chalk.white(consumer), ...providerCells];
@@ -497,15 +563,13 @@ const matrixCommand = new Command('matrix')
497
563
 
498
564
  const listProvidersCommand = new Command('list-providers')
499
565
  .description('List published provider specs')
500
- .option('--org <key>', 'Filter by organization key')
566
+ .option('--org <key>', 'Organization key')
501
567
  .option('--provider <name>', 'Filter by provider service name')
502
- .option('--env <environment>', 'Filter by environment')
503
- .option('--page <n>', 'Page number (0-based)', '0')
504
- .option('--size <n>', 'Page size', '20')
505
- .option('--json', 'Output raw JSON')
506
- .option('--server <url>', 'SpecShield server URL')
568
+ .option('--json', 'Output raw JSON')
569
+ .option('--server <url>', 'SpecShield server URL')
507
570
  .option('--api-token <token>', 'API token')
508
571
  .action(async (opts) => {
572
+ withProjectDefaults(opts, 'list-providers');
509
573
  const token = await resolveApiToken(opts);
510
574
  requireToken(token);
511
575
 
@@ -515,9 +579,6 @@ const listProvidersCommand = new Command('list-providers')
515
579
  const page = await listProviderSpecs(opts.server, token, {
516
580
  org: opts.org,
517
581
  provider: opts.provider,
518
- env: opts.env,
519
- page: parseInt(opts.page, 10) || 0,
520
- size: parseInt(opts.size, 10) || 20,
521
582
  });
522
583
  if (spinner) spinner.stop();
523
584
 
@@ -543,11 +604,11 @@ const listProvidersCommand = new Command('list-providers')
543
604
  ['ID', 'Provider', 'Version', 'Environment', 'Branch', 'Published At'],
544
605
  items.map(s => [
545
606
  chalk.cyan(String(s.id ?? '—')),
546
- s.provider || s.providerName || '—',
607
+ s.providerName || s.provider || '—',
547
608
  chalk.cyan(s.version || '—'),
548
- s.env || s.environment || '—',
609
+ s.environment || s.env || '—',
549
610
  chalk.gray(s.branch || '—'),
550
- fmtDate(s.publishedAt),
611
+ fmtDate(s.createdAt || s.publishedAt),
551
612
  ])
552
613
  );
553
614
 
@@ -566,15 +627,14 @@ const listProvidersCommand = new Command('list-providers')
566
627
 
567
628
  const listConsumersCommand = new Command('list-consumers')
568
629
  .description('List published consumer contracts')
569
- .option('--org <key>', 'Filter by organization key')
630
+ .option('--org <key>', 'Organization key')
570
631
  .option('--consumer <name>', 'Filter by consumer service name')
571
632
  .option('--provider <name>', 'Filter by provider service name')
572
- .option('--page <n>', 'Page number (0-based)', '0')
573
- .option('--size <n>', 'Page size', '20')
574
- .option('--json', 'Output raw JSON')
575
- .option('--server <url>', 'SpecShield server URL')
633
+ .option('--json', 'Output raw JSON')
634
+ .option('--server <url>', 'SpecShield server URL')
576
635
  .option('--api-token <token>', 'API token')
577
636
  .action(async (opts) => {
637
+ withProjectDefaults(opts, 'list-consumers');
578
638
  const token = await resolveApiToken(opts);
579
639
  requireToken(token);
580
640
 
@@ -585,8 +645,6 @@ const listConsumersCommand = new Command('list-consumers')
585
645
  org: opts.org,
586
646
  consumer: opts.consumer,
587
647
  provider: opts.provider,
588
- page: parseInt(opts.page, 10) || 0,
589
- size: parseInt(opts.size, 10) || 20,
590
648
  });
591
649
  if (spinner) spinner.stop();
592
650
 
@@ -612,11 +670,11 @@ const listConsumersCommand = new Command('list-consumers')
612
670
  ['ID', 'Consumer', 'Provider', 'Version', 'Format', 'Published At'],
613
671
  items.map(c => [
614
672
  chalk.cyan(String(c.id ?? '—')),
615
- c.consumer || c.consumerName || '—',
616
- c.provider || c.providerName || '—',
673
+ c.consumerName || c.consumer || '—',
674
+ c.providerName || c.provider || '—',
617
675
  chalk.cyan(c.version || '—'),
618
- c.format || '—',
619
- fmtDate(c.publishedAt),
676
+ c.contractFormat || c.format || '—',
677
+ fmtDate(c.createdAt || c.publishedAt),
620
678
  ])
621
679
  );
622
680