@curl-runner/cli 1.14.0 → 1.16.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curl-runner/cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "A powerful CLI tool for HTTP request management using YAML configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -14,6 +14,7 @@
14
14
  "format": "biome format --write .",
15
15
  "lint": "biome lint .",
16
16
  "check": "biome check --write .",
17
+ "typecheck": "tsc --noEmit",
17
18
  "test": "bun test"
18
19
  },
19
20
  "keywords": [
@@ -52,6 +52,7 @@ function createSummary(total: number, failed: number): ExecutionSummary {
52
52
  total,
53
53
  successful: total - failed,
54
54
  failed,
55
+ skipped: 0,
55
56
  duration: 1000,
56
57
  results: [],
57
58
  };
package/src/cli.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { Glob } from 'bun';
4
+ import { showUpgradeHelp, UpgradeCommand } from './commands/upgrade';
5
+ import { BaselineManager, DiffFormatter, DiffOrchestrator } from './diff';
4
6
  import { ProfileExecutor } from './executor/profile-executor';
5
7
  import { RequestExecutor } from './executor/request-executor';
6
8
  import { YamlParser } from './parser/yaml';
7
9
  import type {
10
+ DiffConfig,
8
11
  ExecutionResult,
9
12
  ExecutionSummary,
10
13
  GlobalConfig,
14
+ GlobalDiffConfig,
11
15
  ProfileConfig,
12
16
  RequestConfig,
13
17
  WatchConfig,
@@ -35,9 +39,9 @@ class CurlRunnerCLI {
35
39
  if (await file.exists()) {
36
40
  const yamlContent = await YamlParser.parseFile(filename);
37
41
  // Extract global config from the YAML file
38
- const config = yamlContent.global || yamlContent;
42
+ const config = yamlContent.global || {};
39
43
  this.logger.logInfo(`Loaded configuration from ${filename}`);
40
- return config;
44
+ return config as Partial<GlobalConfig>;
41
45
  }
42
46
  } catch (error) {
43
47
  this.logger.logWarning(`Failed to load configuration from ${filename}: ${error}`);
@@ -244,6 +248,52 @@ class CurlRunnerCLI {
244
248
  };
245
249
  }
246
250
 
251
+ // Diff configuration
252
+ if (process.env.CURL_RUNNER_DIFF) {
253
+ envConfig.diff = {
254
+ ...envConfig.diff,
255
+ enabled: process.env.CURL_RUNNER_DIFF.toLowerCase() === 'true',
256
+ };
257
+ }
258
+
259
+ if (process.env.CURL_RUNNER_DIFF_SAVE) {
260
+ envConfig.diff = {
261
+ ...envConfig.diff,
262
+ save: process.env.CURL_RUNNER_DIFF_SAVE.toLowerCase() === 'true',
263
+ };
264
+ }
265
+
266
+ if (process.env.CURL_RUNNER_DIFF_LABEL) {
267
+ envConfig.diff = {
268
+ ...envConfig.diff,
269
+ label: process.env.CURL_RUNNER_DIFF_LABEL,
270
+ };
271
+ }
272
+
273
+ if (process.env.CURL_RUNNER_DIFF_COMPARE) {
274
+ envConfig.diff = {
275
+ ...envConfig.diff,
276
+ compareWith: process.env.CURL_RUNNER_DIFF_COMPARE,
277
+ };
278
+ }
279
+
280
+ if (process.env.CURL_RUNNER_DIFF_DIR) {
281
+ envConfig.diff = {
282
+ ...envConfig.diff,
283
+ dir: process.env.CURL_RUNNER_DIFF_DIR,
284
+ };
285
+ }
286
+
287
+ if (process.env.CURL_RUNNER_DIFF_OUTPUT) {
288
+ const format = process.env.CURL_RUNNER_DIFF_OUTPUT.toLowerCase();
289
+ if (['terminal', 'json', 'markdown'].includes(format)) {
290
+ envConfig.diff = {
291
+ ...envConfig.diff,
292
+ outputFormat: format as 'terminal' | 'json' | 'markdown',
293
+ };
294
+ }
295
+ }
296
+
247
297
  return envConfig;
248
298
  }
249
299
 
@@ -269,6 +319,23 @@ class CurlRunnerCLI {
269
319
  return;
270
320
  }
271
321
 
322
+ // Handle upgrade subcommand: curl-runner upgrade [options]
323
+ if (args[0] === 'upgrade') {
324
+ if (args.includes('--help') || args.includes('-h')) {
325
+ showUpgradeHelp();
326
+ return;
327
+ }
328
+ const upgradeCmd = new UpgradeCommand();
329
+ await upgradeCmd.run(args.slice(1));
330
+ return;
331
+ }
332
+
333
+ // Handle diff subcommand: curl-runner diff <label1> <label2> [file]
334
+ if (args[0] === 'diff' && args.length >= 3) {
335
+ await this.executeDiffSubcommand(args.slice(1), options);
336
+ return;
337
+ }
338
+
272
339
  // Load configuration from environment variables, config file, then CLI options
273
340
  const envConfig = this.loadEnvironmentVariables();
274
341
  const configFile = await this.loadConfigFile();
@@ -318,16 +385,16 @@ class CurlRunnerCLI {
318
385
  globalConfig.maxConcurrency = options.maxConcurrent as number;
319
386
  }
320
387
  if (options.continueOnError !== undefined) {
321
- globalConfig.continueOnError = options.continueOnError;
388
+ globalConfig.continueOnError = options.continueOnError as boolean;
322
389
  }
323
390
  if (options.verbose !== undefined) {
324
- globalConfig.output = { ...globalConfig.output, verbose: options.verbose };
391
+ globalConfig.output = { ...globalConfig.output, verbose: options.verbose as boolean };
325
392
  }
326
393
  if (options.quiet !== undefined) {
327
394
  globalConfig.output = { ...globalConfig.output, verbose: false };
328
395
  }
329
396
  if (options.output) {
330
- globalConfig.output = { ...globalConfig.output, saveToFile: options.output };
397
+ globalConfig.output = { ...globalConfig.output, saveToFile: options.output as string };
331
398
  }
332
399
  if (options.outputFormat) {
333
400
  globalConfig.output = {
@@ -342,21 +409,27 @@ class CurlRunnerCLI {
342
409
  };
343
410
  }
344
411
  if (options.showHeaders !== undefined) {
345
- globalConfig.output = { ...globalConfig.output, showHeaders: options.showHeaders };
412
+ globalConfig.output = {
413
+ ...globalConfig.output,
414
+ showHeaders: options.showHeaders as boolean,
415
+ };
346
416
  }
347
417
  if (options.showBody !== undefined) {
348
- globalConfig.output = { ...globalConfig.output, showBody: options.showBody };
418
+ globalConfig.output = { ...globalConfig.output, showBody: options.showBody as boolean };
349
419
  }
350
420
  if (options.showMetrics !== undefined) {
351
- globalConfig.output = { ...globalConfig.output, showMetrics: options.showMetrics };
421
+ globalConfig.output = {
422
+ ...globalConfig.output,
423
+ showMetrics: options.showMetrics as boolean,
424
+ };
352
425
  }
353
426
 
354
427
  // Apply timeout and retry settings to defaults
355
428
  if (options.timeout) {
356
- globalConfig.defaults = { ...globalConfig.defaults, timeout: options.timeout };
429
+ globalConfig.defaults = { ...globalConfig.defaults, timeout: options.timeout as number };
357
430
  }
358
431
  if (options.retries || options.noRetry) {
359
- const retryCount = options.noRetry ? 0 : options.retries || 0;
432
+ const retryCount = options.noRetry ? 0 : (options.retries as number) || 0;
360
433
  globalConfig.defaults = {
361
434
  ...globalConfig.defaults,
362
435
  retry: {
@@ -370,7 +443,7 @@ class CurlRunnerCLI {
370
443
  ...globalConfig.defaults,
371
444
  retry: {
372
445
  ...globalConfig.defaults?.retry,
373
- delay: options.retryDelay,
446
+ delay: options.retryDelay as number,
374
447
  },
375
448
  };
376
449
  }
@@ -416,6 +489,46 @@ class CurlRunnerCLI {
416
489
  };
417
490
  }
418
491
 
492
+ // Apply diff options
493
+ if (options.diff !== undefined) {
494
+ globalConfig.diff = {
495
+ ...globalConfig.diff,
496
+ enabled: options.diff as boolean,
497
+ };
498
+ }
499
+ if (options.diffSave !== undefined) {
500
+ globalConfig.diff = {
501
+ ...globalConfig.diff,
502
+ enabled: true,
503
+ save: options.diffSave as boolean,
504
+ };
505
+ }
506
+ if (options.diffLabel !== undefined) {
507
+ globalConfig.diff = {
508
+ ...globalConfig.diff,
509
+ label: options.diffLabel as string,
510
+ };
511
+ }
512
+ if (options.diffCompare !== undefined) {
513
+ globalConfig.diff = {
514
+ ...globalConfig.diff,
515
+ enabled: true,
516
+ compareWith: options.diffCompare as string,
517
+ };
518
+ }
519
+ if (options.diffDir !== undefined) {
520
+ globalConfig.diff = {
521
+ ...globalConfig.diff,
522
+ dir: options.diffDir as string,
523
+ };
524
+ }
525
+ if (options.diffOutput !== undefined) {
526
+ globalConfig.diff = {
527
+ ...globalConfig.diff,
528
+ outputFormat: options.diffOutput as 'terminal' | 'json' | 'markdown',
529
+ };
530
+ }
531
+
419
532
  if (allRequests.length === 0) {
420
533
  this.logger.logError('No requests found in YAML files');
421
534
  process.exit(1);
@@ -575,13 +688,15 @@ class CurlRunnerCLI {
575
688
  }
576
689
 
577
690
  // Create combined summary
578
- const successful = allResults.filter((r) => r.success).length;
579
- const failed = allResults.filter((r) => !r.success).length;
691
+ const successful = allResults.filter((r) => r.success && !r.skipped).length;
692
+ const failed = allResults.filter((r) => !r.success && !r.skipped).length;
693
+ const skipped = allResults.filter((r) => r.skipped).length;
580
694
 
581
695
  summary = {
582
696
  total: allResults.length,
583
697
  successful,
584
698
  failed,
699
+ skipped,
585
700
  duration: totalDuration,
586
701
  results: allResults,
587
702
  };
@@ -593,9 +708,138 @@ class CurlRunnerCLI {
593
708
  summary = await executor.execute(allRequests);
594
709
  }
595
710
 
711
+ // Handle diff mode
712
+ if (globalConfig.diff?.enabled || globalConfig.diff?.save || globalConfig.diff?.compareWith) {
713
+ await this.handleDiffMode(yamlFiles[0], summary.results, globalConfig.diff);
714
+ }
715
+
596
716
  return summary;
597
717
  }
598
718
 
719
+ private async handleDiffMode(
720
+ yamlPath: string,
721
+ results: ExecutionResult[],
722
+ diffConfig: GlobalDiffConfig,
723
+ ): Promise<void> {
724
+ const orchestrator = new DiffOrchestrator(diffConfig);
725
+ const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
726
+ const config: DiffConfig = BaselineManager.mergeConfig(diffConfig, true) || {};
727
+
728
+ const currentLabel = diffConfig.label || 'current';
729
+ const compareLabel = diffConfig.compareWith;
730
+
731
+ // Save baseline if requested
732
+ if (diffConfig.save) {
733
+ await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
734
+ this.logger.logInfo(`Baseline saved as '${currentLabel}'`);
735
+ }
736
+
737
+ // Compare with baseline if requested
738
+ if (compareLabel) {
739
+ const diffSummary = await orchestrator.compareWithBaseline(
740
+ yamlPath,
741
+ results,
742
+ currentLabel,
743
+ compareLabel,
744
+ config,
745
+ );
746
+
747
+ // Check if baseline exists
748
+ if (diffSummary.newBaselines === diffSummary.totalRequests) {
749
+ this.logger.logWarning(
750
+ `No baseline '${compareLabel}' found. Saving current run as baseline.`,
751
+ );
752
+ await orchestrator.saveBaseline(yamlPath, compareLabel, results, config);
753
+ return;
754
+ }
755
+
756
+ const output = formatter.formatSummary(diffSummary, compareLabel, currentLabel);
757
+ console.log(output);
758
+
759
+ // Save current as baseline if configured
760
+ if (diffConfig.save) {
761
+ await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
762
+ }
763
+ } else if (diffConfig.enabled && !diffConfig.save) {
764
+ // Auto-detect: list available baselines or save first baseline
765
+ const labels = await orchestrator.listLabels(yamlPath);
766
+
767
+ if (labels.length === 0) {
768
+ // No baselines exist - save current as default baseline
769
+ await orchestrator.saveBaseline(yamlPath, 'baseline', results, config);
770
+ this.logger.logInfo(`No baselines found. Saved current run as 'baseline'.`);
771
+ } else if (labels.length === 1) {
772
+ // One baseline exists - compare against it
773
+ const diffSummary = await orchestrator.compareWithBaseline(
774
+ yamlPath,
775
+ results,
776
+ currentLabel,
777
+ labels[0],
778
+ config,
779
+ );
780
+ const output = formatter.formatSummary(diffSummary, labels[0], currentLabel);
781
+ console.log(output);
782
+ } else {
783
+ // Multiple baselines - list them
784
+ this.logger.logInfo(`Available baselines: ${labels.join(', ')}`);
785
+ this.logger.logInfo(`Use --diff-compare <label> to compare against a specific baseline.`);
786
+ }
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Executes the diff subcommand to compare two stored baselines.
792
+ * Usage: curl-runner diff <label1> <label2> [file.yaml]
793
+ */
794
+ private async executeDiffSubcommand(
795
+ args: string[],
796
+ options: Record<string, unknown>,
797
+ ): Promise<void> {
798
+ const label1 = args[0];
799
+ const label2 = args[1];
800
+ let yamlFile = args[2];
801
+
802
+ // Find YAML file if not specified
803
+ if (!yamlFile) {
804
+ const yamlFiles = await this.findYamlFiles([], options);
805
+ if (yamlFiles.length === 0) {
806
+ this.logger.logError(
807
+ 'No YAML files found. Specify a file: curl-runner diff <label1> <label2> <file.yaml>',
808
+ );
809
+ process.exit(1);
810
+ }
811
+ if (yamlFiles.length > 1) {
812
+ this.logger.logError('Multiple YAML files found. Specify which file to use.');
813
+ process.exit(1);
814
+ }
815
+ yamlFile = yamlFiles[0];
816
+ }
817
+
818
+ const diffConfig: GlobalDiffConfig = {
819
+ dir: (options.diffDir as string) || '__baselines__',
820
+ outputFormat: (options.diffOutput as 'terminal' | 'json' | 'markdown') || 'terminal',
821
+ };
822
+
823
+ const orchestrator = new DiffOrchestrator(diffConfig);
824
+ const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
825
+ const config: DiffConfig = { exclude: [], match: {} };
826
+
827
+ try {
828
+ const diffSummary = await orchestrator.compareTwoBaselines(yamlFile, label1, label2, config);
829
+ const output = formatter.formatSummary(diffSummary, label1, label2);
830
+ console.log(output);
831
+
832
+ // Exit with code 1 if differences found
833
+ if (diffSummary.changed > 0) {
834
+ process.exit(1);
835
+ }
836
+ process.exit(0);
837
+ } catch (error) {
838
+ this.logger.logError(error instanceof Error ? error.message : String(error));
839
+ process.exit(1);
840
+ }
841
+ }
842
+
599
843
  private parseArguments(args: string[]): { files: string[]; options: Record<string, unknown> } {
600
844
  const options: Record<string, unknown> = {};
601
845
  const files: string[] = [];
@@ -637,6 +881,10 @@ class CurlRunnerCLI {
637
881
  options.snapshotUpdate = 'failing';
638
882
  } else if (key === 'ci-snapshot') {
639
883
  options.snapshotCi = true;
884
+ } else if (key === 'diff') {
885
+ options.diff = true;
886
+ } else if (key === 'diff-save') {
887
+ options.diffSave = true;
640
888
  } else if (nextArg && !nextArg.startsWith('--')) {
641
889
  if (key === 'continue-on-error') {
642
890
  options.continueOnError = nextArg === 'true';
@@ -680,6 +928,16 @@ class CurlRunnerCLI {
680
928
  options.profileExport = nextArg;
681
929
  } else if (key === 'snapshot-dir') {
682
930
  options.snapshotDir = nextArg;
931
+ } else if (key === 'diff-label') {
932
+ options.diffLabel = nextArg;
933
+ } else if (key === 'diff-compare') {
934
+ options.diffCompare = nextArg;
935
+ } else if (key === 'diff-dir') {
936
+ options.diffDir = nextArg;
937
+ } else if (key === 'diff-output') {
938
+ if (['terminal', 'json', 'markdown'].includes(nextArg)) {
939
+ options.diffOutput = nextArg;
940
+ }
683
941
  } else {
684
942
  options[key] = nextArg;
685
943
  }
@@ -715,6 +973,9 @@ class CurlRunnerCLI {
715
973
  case 'u':
716
974
  options.snapshotUpdate = 'all';
717
975
  break;
976
+ case 'd':
977
+ options.diff = true;
978
+ break;
718
979
  case 'o': {
719
980
  // Handle -o flag for output file
720
981
  const outputArg = args[i + 1];
@@ -840,7 +1101,7 @@ class CurlRunnerCLI {
840
1101
  variables: Record<string, string>,
841
1102
  defaults: Partial<RequestConfig>,
842
1103
  ): RequestConfig {
843
- const interpolated = YamlParser.interpolateVariables(request, variables);
1104
+ const interpolated = YamlParser.interpolateVariables(request, variables) as RequestConfig;
844
1105
  return YamlParser.mergeConfigs(defaults, interpolated);
845
1106
  }
846
1107
 
@@ -854,6 +1115,7 @@ class CurlRunnerCLI {
854
1115
  ci: { ...base.ci, ...override.ci },
855
1116
  watch: { ...base.watch, ...override.watch },
856
1117
  snapshot: { ...base.snapshot, ...override.snapshot },
1118
+ diff: { ...base.diff, ...override.diff },
857
1119
  };
858
1120
  }
859
1121
 
@@ -960,6 +1222,23 @@ ${this.logger.color('SNAPSHOT OPTIONS:', 'yellow')}
960
1222
  --snapshot-dir <dir> Custom snapshot directory (default: __snapshots__)
961
1223
  --ci-snapshot Fail if snapshot is missing (CI mode)
962
1224
 
1225
+ ${this.logger.color('DIFF OPTIONS:', 'yellow')}
1226
+ -d, --diff Enable response diffing (compare with baseline)
1227
+ --diff-save Save current run as baseline
1228
+ --diff-label <name> Label for current run (e.g., 'staging', 'v1.0')
1229
+ --diff-compare <label> Compare against this baseline label
1230
+ --diff-dir <dir> Baseline storage directory (default: __baselines__)
1231
+ --diff-output <format> Output format (terminal|json|markdown)
1232
+
1233
+ ${this.logger.color('DIFF SUBCOMMAND:', 'yellow')}
1234
+ curl-runner diff <label1> <label2> [file.yaml]
1235
+ Compare two stored baselines without making requests
1236
+
1237
+ ${this.logger.color('UPGRADE:', 'yellow')}
1238
+ curl-runner upgrade Upgrade to latest version (auto-detects install method)
1239
+ curl-runner upgrade --dry-run Preview upgrade command without executing
1240
+ curl-runner upgrade --force Force reinstall even if up to date
1241
+
963
1242
  ${this.logger.color('EXAMPLES:', 'yellow')}
964
1243
  # Run all YAML files in current directory
965
1244
  curl-runner
@@ -1027,6 +1306,21 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
1027
1306
  # CI mode - fail if snapshot missing
1028
1307
  curl-runner api.yaml --snapshot --ci-snapshot
1029
1308
 
1309
+ # Response diffing - save baseline for staging
1310
+ curl-runner api.yaml --diff-save --diff-label staging
1311
+
1312
+ # Compare current run against staging baseline
1313
+ curl-runner api.yaml --diff --diff-compare staging
1314
+
1315
+ # Compare staging vs production baselines (offline)
1316
+ curl-runner diff staging production api.yaml
1317
+
1318
+ # Auto-diff: creates baseline on first run, compares on subsequent runs
1319
+ curl-runner api.yaml --diff
1320
+
1321
+ # Diff with JSON output for CI
1322
+ curl-runner api.yaml --diff --diff-compare staging --diff-output json
1323
+
1030
1324
  ${this.logger.color('YAML STRUCTURE:', 'yellow')}
1031
1325
  Single request:
1032
1326
  request: