@curl-runner/cli 1.16.0 → 1.16.2

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.
Files changed (40) hide show
  1. package/package.json +2 -2
  2. package/src/ci-exit.test.ts +0 -216
  3. package/src/cli.ts +0 -1351
  4. package/src/commands/upgrade.ts +0 -262
  5. package/src/diff/baseline-manager.test.ts +0 -181
  6. package/src/diff/baseline-manager.ts +0 -266
  7. package/src/diff/diff-formatter.ts +0 -316
  8. package/src/diff/index.ts +0 -3
  9. package/src/diff/response-differ.test.ts +0 -330
  10. package/src/diff/response-differ.ts +0 -489
  11. package/src/executor/max-concurrency.test.ts +0 -139
  12. package/src/executor/profile-executor.test.ts +0 -132
  13. package/src/executor/profile-executor.ts +0 -167
  14. package/src/executor/request-executor.ts +0 -663
  15. package/src/parser/yaml.test.ts +0 -480
  16. package/src/parser/yaml.ts +0 -271
  17. package/src/snapshot/index.ts +0 -3
  18. package/src/snapshot/snapshot-differ.test.ts +0 -358
  19. package/src/snapshot/snapshot-differ.ts +0 -296
  20. package/src/snapshot/snapshot-formatter.ts +0 -170
  21. package/src/snapshot/snapshot-manager.test.ts +0 -204
  22. package/src/snapshot/snapshot-manager.ts +0 -342
  23. package/src/types/bun-yaml.d.ts +0 -11
  24. package/src/types/config.ts +0 -638
  25. package/src/utils/colors.ts +0 -30
  26. package/src/utils/condition-evaluator.test.ts +0 -415
  27. package/src/utils/condition-evaluator.ts +0 -327
  28. package/src/utils/curl-builder.test.ts +0 -165
  29. package/src/utils/curl-builder.ts +0 -209
  30. package/src/utils/installation-detector.test.ts +0 -52
  31. package/src/utils/installation-detector.ts +0 -123
  32. package/src/utils/logger.ts +0 -856
  33. package/src/utils/response-store.test.ts +0 -213
  34. package/src/utils/response-store.ts +0 -108
  35. package/src/utils/stats.test.ts +0 -161
  36. package/src/utils/stats.ts +0 -151
  37. package/src/utils/version-checker.ts +0 -158
  38. package/src/version.ts +0 -43
  39. package/src/watcher/file-watcher.test.ts +0 -186
  40. package/src/watcher/file-watcher.ts +0 -140
package/src/cli.ts DELETED
@@ -1,1351 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { Glob } from 'bun';
4
- import { showUpgradeHelp, UpgradeCommand } from './commands/upgrade';
5
- import { BaselineManager, DiffFormatter, DiffOrchestrator } from './diff';
6
- import { ProfileExecutor } from './executor/profile-executor';
7
- import { RequestExecutor } from './executor/request-executor';
8
- import { YamlParser } from './parser/yaml';
9
- import type {
10
- DiffConfig,
11
- ExecutionResult,
12
- ExecutionSummary,
13
- GlobalConfig,
14
- GlobalDiffConfig,
15
- ProfileConfig,
16
- RequestConfig,
17
- WatchConfig,
18
- } from './types/config';
19
- import { Logger } from './utils/logger';
20
- import { exportToCSV, exportToJSON } from './utils/stats';
21
- import { VersionChecker } from './utils/version-checker';
22
- import { getVersion } from './version';
23
- import { FileWatcher } from './watcher/file-watcher';
24
-
25
- class CurlRunnerCLI {
26
- private logger = new Logger();
27
-
28
- private async loadConfigFile(): Promise<Partial<GlobalConfig>> {
29
- const configFiles = [
30
- 'curl-runner.yaml',
31
- 'curl-runner.yml',
32
- '.curl-runner.yaml',
33
- '.curl-runner.yml',
34
- ];
35
-
36
- for (const filename of configFiles) {
37
- try {
38
- const file = Bun.file(filename);
39
- if (await file.exists()) {
40
- const yamlContent = await YamlParser.parseFile(filename);
41
- // Extract global config from the YAML file
42
- const config = yamlContent.global || {};
43
- this.logger.logInfo(`Loaded configuration from ${filename}`);
44
- return config as Partial<GlobalConfig>;
45
- }
46
- } catch (error) {
47
- this.logger.logWarning(`Failed to load configuration from ${filename}: ${error}`);
48
- }
49
- }
50
-
51
- return {};
52
- }
53
-
54
- private loadEnvironmentVariables(): Partial<GlobalConfig> {
55
- const envConfig: Partial<GlobalConfig> = {};
56
-
57
- // Load environment variables
58
- if (process.env.CURL_RUNNER_TIMEOUT) {
59
- envConfig.defaults = {
60
- ...envConfig.defaults,
61
- timeout: Number.parseInt(process.env.CURL_RUNNER_TIMEOUT, 10),
62
- };
63
- }
64
-
65
- if (process.env.CURL_RUNNER_RETRIES) {
66
- envConfig.defaults = {
67
- ...envConfig.defaults,
68
- retry: {
69
- ...envConfig.defaults?.retry,
70
- count: Number.parseInt(process.env.CURL_RUNNER_RETRIES, 10),
71
- },
72
- };
73
- }
74
-
75
- if (process.env.CURL_RUNNER_RETRY_DELAY) {
76
- envConfig.defaults = {
77
- ...envConfig.defaults,
78
- retry: {
79
- ...envConfig.defaults?.retry,
80
- delay: Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY, 10),
81
- },
82
- };
83
- }
84
-
85
- if (process.env.CURL_RUNNER_VERBOSE) {
86
- envConfig.output = {
87
- ...envConfig.output,
88
- verbose: process.env.CURL_RUNNER_VERBOSE.toLowerCase() === 'true',
89
- };
90
- }
91
-
92
- if (process.env.CURL_RUNNER_EXECUTION) {
93
- envConfig.execution = process.env.CURL_RUNNER_EXECUTION as 'sequential' | 'parallel';
94
- }
95
-
96
- if (process.env.CURL_RUNNER_CONTINUE_ON_ERROR) {
97
- envConfig.continueOnError =
98
- process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase() === 'true';
99
- }
100
-
101
- if (process.env.CURL_RUNNER_MAX_CONCURRENCY) {
102
- const maxConcurrency = Number.parseInt(process.env.CURL_RUNNER_MAX_CONCURRENCY, 10);
103
- if (maxConcurrency > 0) {
104
- envConfig.maxConcurrency = maxConcurrency;
105
- }
106
- }
107
-
108
- if (process.env.CURL_RUNNER_OUTPUT_FORMAT) {
109
- const format = process.env.CURL_RUNNER_OUTPUT_FORMAT;
110
- if (['json', 'pretty', 'raw'].includes(format)) {
111
- envConfig.output = { ...envConfig.output, format: format as 'json' | 'pretty' | 'raw' };
112
- }
113
- }
114
-
115
- if (process.env.CURL_RUNNER_PRETTY_LEVEL) {
116
- const level = process.env.CURL_RUNNER_PRETTY_LEVEL;
117
- if (['minimal', 'standard', 'detailed'].includes(level)) {
118
- envConfig.output = {
119
- ...envConfig.output,
120
- prettyLevel: level as 'minimal' | 'standard' | 'detailed',
121
- };
122
- }
123
- }
124
-
125
- if (process.env.CURL_RUNNER_OUTPUT_FILE) {
126
- envConfig.output = { ...envConfig.output, saveToFile: process.env.CURL_RUNNER_OUTPUT_FILE };
127
- }
128
-
129
- // CI exit code configuration
130
- if (process.env.CURL_RUNNER_STRICT_EXIT) {
131
- envConfig.ci = {
132
- ...envConfig.ci,
133
- strictExit: process.env.CURL_RUNNER_STRICT_EXIT.toLowerCase() === 'true',
134
- };
135
- }
136
-
137
- if (process.env.CURL_RUNNER_FAIL_ON) {
138
- envConfig.ci = {
139
- ...envConfig.ci,
140
- failOn: Number.parseInt(process.env.CURL_RUNNER_FAIL_ON, 10),
141
- };
142
- }
143
-
144
- if (process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE) {
145
- const percentage = Number.parseFloat(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE);
146
- if (percentage >= 0 && percentage <= 100) {
147
- envConfig.ci = {
148
- ...envConfig.ci,
149
- failOnPercentage: percentage,
150
- };
151
- }
152
- }
153
-
154
- // Watch mode configuration
155
- if (process.env.CURL_RUNNER_WATCH) {
156
- envConfig.watch = {
157
- ...envConfig.watch,
158
- enabled: process.env.CURL_RUNNER_WATCH.toLowerCase() === 'true',
159
- };
160
- }
161
-
162
- if (process.env.CURL_RUNNER_WATCH_DEBOUNCE) {
163
- envConfig.watch = {
164
- ...envConfig.watch,
165
- debounce: Number.parseInt(process.env.CURL_RUNNER_WATCH_DEBOUNCE, 10),
166
- };
167
- }
168
-
169
- if (process.env.CURL_RUNNER_WATCH_CLEAR) {
170
- envConfig.watch = {
171
- ...envConfig.watch,
172
- clear: process.env.CURL_RUNNER_WATCH_CLEAR.toLowerCase() !== 'false',
173
- };
174
- }
175
-
176
- // Profile mode configuration
177
- if (process.env.CURL_RUNNER_PROFILE) {
178
- const iterations = Number.parseInt(process.env.CURL_RUNNER_PROFILE, 10);
179
- if (iterations > 0) {
180
- envConfig.profile = {
181
- ...envConfig.profile,
182
- iterations,
183
- };
184
- }
185
- }
186
-
187
- if (process.env.CURL_RUNNER_PROFILE_WARMUP) {
188
- envConfig.profile = {
189
- ...envConfig.profile,
190
- iterations: envConfig.profile?.iterations ?? 10,
191
- warmup: Number.parseInt(process.env.CURL_RUNNER_PROFILE_WARMUP, 10),
192
- };
193
- }
194
-
195
- if (process.env.CURL_RUNNER_PROFILE_CONCURRENCY) {
196
- envConfig.profile = {
197
- ...envConfig.profile,
198
- iterations: envConfig.profile?.iterations ?? 10,
199
- concurrency: Number.parseInt(process.env.CURL_RUNNER_PROFILE_CONCURRENCY, 10),
200
- };
201
- }
202
-
203
- if (process.env.CURL_RUNNER_PROFILE_HISTOGRAM) {
204
- envConfig.profile = {
205
- ...envConfig.profile,
206
- iterations: envConfig.profile?.iterations ?? 10,
207
- histogram: process.env.CURL_RUNNER_PROFILE_HISTOGRAM.toLowerCase() === 'true',
208
- };
209
- }
210
-
211
- if (process.env.CURL_RUNNER_PROFILE_EXPORT) {
212
- envConfig.profile = {
213
- ...envConfig.profile,
214
- iterations: envConfig.profile?.iterations ?? 10,
215
- exportFile: process.env.CURL_RUNNER_PROFILE_EXPORT,
216
- };
217
- }
218
-
219
- // Snapshot configuration
220
- if (process.env.CURL_RUNNER_SNAPSHOT) {
221
- envConfig.snapshot = {
222
- ...envConfig.snapshot,
223
- enabled: process.env.CURL_RUNNER_SNAPSHOT.toLowerCase() === 'true',
224
- };
225
- }
226
-
227
- if (process.env.CURL_RUNNER_SNAPSHOT_UPDATE) {
228
- const mode = process.env.CURL_RUNNER_SNAPSHOT_UPDATE.toLowerCase();
229
- if (['none', 'all', 'failing'].includes(mode)) {
230
- envConfig.snapshot = {
231
- ...envConfig.snapshot,
232
- updateMode: mode as 'none' | 'all' | 'failing',
233
- };
234
- }
235
- }
236
-
237
- if (process.env.CURL_RUNNER_SNAPSHOT_DIR) {
238
- envConfig.snapshot = {
239
- ...envConfig.snapshot,
240
- dir: process.env.CURL_RUNNER_SNAPSHOT_DIR,
241
- };
242
- }
243
-
244
- if (process.env.CURL_RUNNER_SNAPSHOT_CI) {
245
- envConfig.snapshot = {
246
- ...envConfig.snapshot,
247
- ci: process.env.CURL_RUNNER_SNAPSHOT_CI.toLowerCase() === 'true',
248
- };
249
- }
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
-
297
- return envConfig;
298
- }
299
-
300
- async run(args: string[]): Promise<void> {
301
- try {
302
- const { files, options } = this.parseArguments(args);
303
-
304
- // Check for updates in the background (non-blocking)
305
- if (!options.version && !options.help) {
306
- const versionChecker = new VersionChecker();
307
- versionChecker.checkForUpdates().catch(() => {
308
- // Silently ignore any errors
309
- });
310
- }
311
-
312
- if (options.help) {
313
- this.showHelp();
314
- return;
315
- }
316
-
317
- if (options.version) {
318
- console.log(`curl-runner v${getVersion()}`);
319
- return;
320
- }
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
-
339
- // Load configuration from environment variables, config file, then CLI options
340
- const envConfig = this.loadEnvironmentVariables();
341
- const configFile = await this.loadConfigFile();
342
-
343
- const yamlFiles = await this.findYamlFiles(files, options);
344
-
345
- if (yamlFiles.length === 0) {
346
- this.logger.logError('No YAML files found');
347
- process.exit(1);
348
- }
349
-
350
- this.logger.logInfo(`Found ${yamlFiles.length} YAML file(s)`);
351
-
352
- let globalConfig: GlobalConfig = this.mergeGlobalConfigs(envConfig, configFile);
353
- const allRequests: RequestConfig[] = [];
354
-
355
- // Group requests by file to show clear file separations in output
356
- const fileGroups: Array<{ file: string; requests: RequestConfig[]; config?: GlobalConfig }> =
357
- [];
358
-
359
- for (const file of yamlFiles) {
360
- this.logger.logInfo(`Processing: ${file}`);
361
- const { requests, config } = await this.processYamlFile(file);
362
-
363
- // Associate each request with its source file's output configuration and filename
364
- const fileOutputConfig = config?.output || {};
365
- const requestsWithSourceConfig = requests.map((request) => ({
366
- ...request,
367
- sourceOutputConfig: fileOutputConfig,
368
- sourceFile: file,
369
- }));
370
-
371
- // Only merge non-output global configs (execution, continueOnError, variables, defaults)
372
- if (config) {
373
- const { ...nonOutputConfig } = config;
374
- globalConfig = this.mergeGlobalConfigs(globalConfig, nonOutputConfig);
375
- }
376
-
377
- fileGroups.push({ file, requests: requestsWithSourceConfig, config });
378
- allRequests.push(...requestsWithSourceConfig);
379
- }
380
-
381
- if (options.execution) {
382
- globalConfig.execution = options.execution as 'sequential' | 'parallel';
383
- }
384
- if (options.maxConcurrent !== undefined) {
385
- globalConfig.maxConcurrency = options.maxConcurrent as number;
386
- }
387
- if (options.continueOnError !== undefined) {
388
- globalConfig.continueOnError = options.continueOnError as boolean;
389
- }
390
- if (options.verbose !== undefined) {
391
- globalConfig.output = { ...globalConfig.output, verbose: options.verbose as boolean };
392
- }
393
- if (options.quiet !== undefined) {
394
- globalConfig.output = { ...globalConfig.output, verbose: false };
395
- }
396
- if (options.output) {
397
- globalConfig.output = { ...globalConfig.output, saveToFile: options.output as string };
398
- }
399
- if (options.outputFormat) {
400
- globalConfig.output = {
401
- ...globalConfig.output,
402
- format: options.outputFormat as 'json' | 'pretty' | 'raw',
403
- };
404
- }
405
- if (options.prettyLevel) {
406
- globalConfig.output = {
407
- ...globalConfig.output,
408
- prettyLevel: options.prettyLevel as 'minimal' | 'standard' | 'detailed',
409
- };
410
- }
411
- if (options.showHeaders !== undefined) {
412
- globalConfig.output = {
413
- ...globalConfig.output,
414
- showHeaders: options.showHeaders as boolean,
415
- };
416
- }
417
- if (options.showBody !== undefined) {
418
- globalConfig.output = { ...globalConfig.output, showBody: options.showBody as boolean };
419
- }
420
- if (options.showMetrics !== undefined) {
421
- globalConfig.output = {
422
- ...globalConfig.output,
423
- showMetrics: options.showMetrics as boolean,
424
- };
425
- }
426
-
427
- // Apply timeout and retry settings to defaults
428
- if (options.timeout) {
429
- globalConfig.defaults = { ...globalConfig.defaults, timeout: options.timeout as number };
430
- }
431
- if (options.retries || options.noRetry) {
432
- const retryCount = options.noRetry ? 0 : (options.retries as number) || 0;
433
- globalConfig.defaults = {
434
- ...globalConfig.defaults,
435
- retry: {
436
- ...globalConfig.defaults?.retry,
437
- count: retryCount,
438
- },
439
- };
440
- }
441
- if (options.retryDelay) {
442
- globalConfig.defaults = {
443
- ...globalConfig.defaults,
444
- retry: {
445
- ...globalConfig.defaults?.retry,
446
- delay: options.retryDelay as number,
447
- },
448
- };
449
- }
450
-
451
- // Apply CI exit code options
452
- if (options.strictExit !== undefined) {
453
- globalConfig.ci = { ...globalConfig.ci, strictExit: options.strictExit as boolean };
454
- }
455
- if (options.failOn !== undefined) {
456
- globalConfig.ci = { ...globalConfig.ci, failOn: options.failOn as number };
457
- }
458
- if (options.failOnPercentage !== undefined) {
459
- globalConfig.ci = {
460
- ...globalConfig.ci,
461
- failOnPercentage: options.failOnPercentage as number,
462
- };
463
- }
464
-
465
- // Apply snapshot options
466
- if (options.snapshot !== undefined) {
467
- globalConfig.snapshot = {
468
- ...globalConfig.snapshot,
469
- enabled: options.snapshot as boolean,
470
- };
471
- }
472
- if (options.snapshotUpdate !== undefined) {
473
- globalConfig.snapshot = {
474
- ...globalConfig.snapshot,
475
- enabled: true,
476
- updateMode: options.snapshotUpdate as 'none' | 'all' | 'failing',
477
- };
478
- }
479
- if (options.snapshotDir !== undefined) {
480
- globalConfig.snapshot = {
481
- ...globalConfig.snapshot,
482
- dir: options.snapshotDir as string,
483
- };
484
- }
485
- if (options.snapshotCi !== undefined) {
486
- globalConfig.snapshot = {
487
- ...globalConfig.snapshot,
488
- ci: options.snapshotCi as boolean,
489
- };
490
- }
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
-
532
- if (allRequests.length === 0) {
533
- this.logger.logError('No requests found in YAML files');
534
- process.exit(1);
535
- }
536
-
537
- // Check if watch mode is enabled
538
- const watchEnabled = options.watch || globalConfig.watch?.enabled;
539
-
540
- // Check if profile mode is enabled (mutually exclusive with watch mode)
541
- const profileIterations =
542
- (options.profile as number | undefined) ?? globalConfig.profile?.iterations;
543
- const profileEnabled = profileIterations && profileIterations > 0;
544
-
545
- if (watchEnabled && profileEnabled) {
546
- this.logger.logError('Profile mode and watch mode cannot be used together');
547
- process.exit(1);
548
- }
549
-
550
- if (profileEnabled) {
551
- // Profile mode - run requests multiple times for latency stats
552
- const profileConfig: ProfileConfig = {
553
- iterations: profileIterations,
554
- warmup:
555
- (options.profileWarmup as number | undefined) ?? globalConfig.profile?.warmup ?? 1,
556
- concurrency:
557
- (options.profileConcurrency as number | undefined) ??
558
- globalConfig.profile?.concurrency ??
559
- 1,
560
- histogram:
561
- (options.profileHistogram as boolean | undefined) ??
562
- globalConfig.profile?.histogram ??
563
- false,
564
- exportFile:
565
- (options.profileExport as string | undefined) ?? globalConfig.profile?.exportFile,
566
- };
567
-
568
- await this.executeProfileMode(allRequests, globalConfig, profileConfig);
569
- } else if (watchEnabled) {
570
- // Build watch config from options and global config
571
- const watchConfig: WatchConfig = {
572
- enabled: true,
573
- debounce:
574
- (options.watchDebounce as number | undefined) ?? globalConfig.watch?.debounce ?? 300,
575
- clear: (options.watchClear as boolean | undefined) ?? globalConfig.watch?.clear ?? true,
576
- };
577
-
578
- const watcher = new FileWatcher({
579
- files: yamlFiles,
580
- config: watchConfig,
581
- logger: this.logger,
582
- onRun: async () => {
583
- await this.executeRequests(yamlFiles, globalConfig);
584
- },
585
- });
586
-
587
- await watcher.start();
588
- } else {
589
- // Normal execution mode
590
- const summary = await this.executeRequests(yamlFiles, globalConfig);
591
- const exitCode = this.determineExitCode(summary, globalConfig);
592
- process.exit(exitCode);
593
- }
594
- } catch (error) {
595
- this.logger.logError(error instanceof Error ? error.message : String(error));
596
- process.exit(1);
597
- }
598
- }
599
-
600
- private async executeProfileMode(
601
- requests: RequestConfig[],
602
- globalConfig: GlobalConfig,
603
- profileConfig: ProfileConfig,
604
- ): Promise<void> {
605
- const profileExecutor = new ProfileExecutor(globalConfig, profileConfig);
606
- const results = await profileExecutor.profileRequests(requests);
607
-
608
- this.logger.logProfileSummary(results);
609
-
610
- // Export results if requested
611
- if (profileConfig.exportFile) {
612
- const exportData: string[] = [];
613
- const isCSV = profileConfig.exportFile.endsWith('.csv');
614
-
615
- for (const result of results) {
616
- const name = result.request.name || result.request.url;
617
- if (isCSV) {
618
- exportData.push(exportToCSV(result.stats, name));
619
- } else {
620
- exportData.push(exportToJSON(result.stats, name));
621
- }
622
- }
623
-
624
- const content = isCSV ? exportData.join('\n\n') : `[${exportData.join(',\n')}]`;
625
- await Bun.write(profileConfig.exportFile, content);
626
- this.logger.logInfo(`Profile results exported to ${profileConfig.exportFile}`);
627
- }
628
-
629
- // Exit with code 1 if failure rate is high
630
- const totalFailures = results.reduce((sum, r) => sum + r.stats.failures, 0);
631
- const totalIterations = results.reduce(
632
- (sum, r) => sum + r.stats.iterations + r.stats.warmup,
633
- 0,
634
- );
635
-
636
- if (totalFailures > 0 && totalFailures / totalIterations > 0.5) {
637
- process.exit(1);
638
- }
639
-
640
- process.exit(0);
641
- }
642
-
643
- private async executeRequests(
644
- yamlFiles: string[],
645
- globalConfig: GlobalConfig,
646
- ): Promise<ExecutionSummary> {
647
- // Process YAML files and collect requests
648
- const fileGroups: Array<{ file: string; requests: RequestConfig[]; config?: GlobalConfig }> =
649
- [];
650
- const allRequests: RequestConfig[] = [];
651
-
652
- for (const file of yamlFiles) {
653
- const { requests, config } = await this.processYamlFile(file);
654
-
655
- const fileOutputConfig = config?.output || {};
656
- const requestsWithSourceConfig = requests.map((request) => ({
657
- ...request,
658
- sourceOutputConfig: fileOutputConfig,
659
- sourceFile: file,
660
- }));
661
-
662
- fileGroups.push({ file, requests: requestsWithSourceConfig, config });
663
- allRequests.push(...requestsWithSourceConfig);
664
- }
665
-
666
- const executor = new RequestExecutor(globalConfig);
667
- let summary: ExecutionSummary;
668
-
669
- // If multiple files, execute them with file separators for clarity
670
- if (fileGroups.length > 1) {
671
- const allResults: ExecutionResult[] = [];
672
- let totalDuration = 0;
673
-
674
- for (let i = 0; i < fileGroups.length; i++) {
675
- const group = fileGroups[i];
676
-
677
- // Show file header for better organization
678
- this.logger.logFileHeader(group.file, group.requests.length);
679
-
680
- const fileSummary = await executor.execute(group.requests);
681
- allResults.push(...fileSummary.results);
682
- totalDuration += fileSummary.duration;
683
-
684
- // Add spacing between files (except for the last one)
685
- if (i < fileGroups.length - 1) {
686
- console.log();
687
- }
688
- }
689
-
690
- // Create combined summary
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;
694
-
695
- summary = {
696
- total: allResults.length,
697
- successful,
698
- failed,
699
- skipped,
700
- duration: totalDuration,
701
- results: allResults,
702
- };
703
-
704
- // Show final summary
705
- this.logger.logSummary(summary, true);
706
- } else {
707
- // Single file - use normal execution
708
- summary = await executor.execute(allRequests);
709
- }
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
-
716
- return summary;
717
- }
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
-
843
- private parseArguments(args: string[]): { files: string[]; options: Record<string, unknown> } {
844
- const options: Record<string, unknown> = {};
845
- const files: string[] = [];
846
-
847
- for (let i = 0; i < args.length; i++) {
848
- const arg = args[i];
849
-
850
- if (arg.startsWith('--')) {
851
- const key = arg.slice(2);
852
- const nextArg = args[i + 1];
853
-
854
- if (key === 'help' || key === 'version') {
855
- options[key] = true;
856
- } else if (key === 'no-retry') {
857
- options.noRetry = true;
858
- } else if (key === 'quiet') {
859
- options.quiet = true;
860
- } else if (key === 'show-headers') {
861
- options.showHeaders = true;
862
- } else if (key === 'show-body') {
863
- options.showBody = true;
864
- } else if (key === 'show-metrics') {
865
- options.showMetrics = true;
866
- } else if (key === 'strict-exit') {
867
- options.strictExit = true;
868
- } else if (key === 'watch') {
869
- options.watch = true;
870
- } else if (key === 'watch-clear') {
871
- options.watchClear = true;
872
- } else if (key === 'no-watch-clear') {
873
- options.watchClear = false;
874
- } else if (key === 'profile-histogram') {
875
- options.profileHistogram = true;
876
- } else if (key === 'snapshot') {
877
- options.snapshot = true;
878
- } else if (key === 'update-snapshots') {
879
- options.snapshotUpdate = 'all';
880
- } else if (key === 'update-failing') {
881
- options.snapshotUpdate = 'failing';
882
- } else if (key === 'ci-snapshot') {
883
- options.snapshotCi = true;
884
- } else if (key === 'diff') {
885
- options.diff = true;
886
- } else if (key === 'diff-save') {
887
- options.diffSave = true;
888
- } else if (nextArg && !nextArg.startsWith('--')) {
889
- if (key === 'continue-on-error') {
890
- options.continueOnError = nextArg === 'true';
891
- } else if (key === 'verbose') {
892
- options.verbose = nextArg === 'true';
893
- } else if (key === 'timeout') {
894
- options.timeout = Number.parseInt(nextArg, 10);
895
- } else if (key === 'retries') {
896
- options.retries = Number.parseInt(nextArg, 10);
897
- } else if (key === 'retry-delay') {
898
- options.retryDelay = Number.parseInt(nextArg, 10);
899
- } else if (key === 'max-concurrent') {
900
- const maxConcurrent = Number.parseInt(nextArg, 10);
901
- if (maxConcurrent > 0) {
902
- options.maxConcurrent = maxConcurrent;
903
- }
904
- } else if (key === 'fail-on') {
905
- options.failOn = Number.parseInt(nextArg, 10);
906
- } else if (key === 'fail-on-percentage') {
907
- const percentage = Number.parseFloat(nextArg);
908
- if (percentage >= 0 && percentage <= 100) {
909
- options.failOnPercentage = percentage;
910
- }
911
- } else if (key === 'output-format') {
912
- if (['json', 'pretty', 'raw'].includes(nextArg)) {
913
- options.outputFormat = nextArg;
914
- }
915
- } else if (key === 'pretty-level') {
916
- if (['minimal', 'standard', 'detailed'].includes(nextArg)) {
917
- options.prettyLevel = nextArg;
918
- }
919
- } else if (key === 'watch-debounce') {
920
- options.watchDebounce = Number.parseInt(nextArg, 10);
921
- } else if (key === 'profile') {
922
- options.profile = Number.parseInt(nextArg, 10);
923
- } else if (key === 'profile-warmup') {
924
- options.profileWarmup = Number.parseInt(nextArg, 10);
925
- } else if (key === 'profile-concurrency') {
926
- options.profileConcurrency = Number.parseInt(nextArg, 10);
927
- } else if (key === 'profile-export') {
928
- options.profileExport = nextArg;
929
- } else if (key === 'snapshot-dir') {
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
- }
941
- } else {
942
- options[key] = nextArg;
943
- }
944
- i++;
945
- } else {
946
- options[key] = true;
947
- }
948
- } else if (arg.startsWith('-')) {
949
- const flags = arg.slice(1);
950
- for (const flag of flags) {
951
- switch (flag) {
952
- case 'h':
953
- options.help = true;
954
- break;
955
- case 'v':
956
- options.verbose = true;
957
- break;
958
- case 'p':
959
- options.execution = 'parallel';
960
- break;
961
- case 'c':
962
- options.continueOnError = true;
963
- break;
964
- case 'q':
965
- options.quiet = true;
966
- break;
967
- case 'w':
968
- options.watch = true;
969
- break;
970
- case 's':
971
- options.snapshot = true;
972
- break;
973
- case 'u':
974
- options.snapshotUpdate = 'all';
975
- break;
976
- case 'd':
977
- options.diff = true;
978
- break;
979
- case 'o': {
980
- // Handle -o flag for output file
981
- const outputArg = args[i + 1];
982
- if (outputArg && !outputArg.startsWith('-')) {
983
- options.output = outputArg;
984
- i++;
985
- }
986
- break;
987
- }
988
- case 'P': {
989
- // Handle -P flag for profile mode
990
- const profileArg = args[i + 1];
991
- if (profileArg && !profileArg.startsWith('-')) {
992
- options.profile = Number.parseInt(profileArg, 10);
993
- i++;
994
- }
995
- break;
996
- }
997
- }
998
- }
999
- } else {
1000
- files.push(arg);
1001
- }
1002
- }
1003
-
1004
- return { files, options };
1005
- }
1006
-
1007
- private async findYamlFiles(
1008
- patterns: string[],
1009
- options: Record<string, unknown>,
1010
- ): Promise<string[]> {
1011
- const files: Set<string> = new Set();
1012
-
1013
- let searchPatterns: string[] = [];
1014
-
1015
- if (patterns.length === 0) {
1016
- searchPatterns = options.all ? ['**/*.yaml', '**/*.yml'] : ['*.yaml', '*.yml'];
1017
- } else {
1018
- // Check if patterns include directories
1019
- for (const pattern of patterns) {
1020
- try {
1021
- // Use Bun's file system API to check if it's a directory
1022
- const fs = await import('node:fs/promises');
1023
- const stat = await fs.stat(pattern);
1024
-
1025
- if (stat.isDirectory()) {
1026
- // Add glob patterns for all YAML files in this directory
1027
- searchPatterns.push(`${pattern}/*.yaml`, `${pattern}/*.yml`);
1028
- // If --all flag is set, search recursively
1029
- if (options.all) {
1030
- searchPatterns.push(`${pattern}/**/*.yaml`, `${pattern}/**/*.yml`);
1031
- }
1032
- } else if (stat.isFile()) {
1033
- // It's a file, add it directly
1034
- searchPatterns.push(pattern);
1035
- }
1036
- } catch {
1037
- // If stat fails, assume it's a glob pattern
1038
- searchPatterns.push(pattern);
1039
- }
1040
- }
1041
- }
1042
-
1043
- for (const pattern of searchPatterns) {
1044
- const globber = new Glob(pattern);
1045
- for await (const file of globber.scan('.')) {
1046
- // Only add files with .yaml or .yml extension
1047
- if (file.endsWith('.yaml') || file.endsWith('.yml')) {
1048
- files.add(file);
1049
- }
1050
- }
1051
- }
1052
-
1053
- return Array.from(files).sort();
1054
- }
1055
-
1056
- private async processYamlFile(
1057
- filepath: string,
1058
- ): Promise<{ requests: RequestConfig[]; config?: GlobalConfig }> {
1059
- const yamlContent = await YamlParser.parseFile(filepath);
1060
- const requests: RequestConfig[] = [];
1061
- let globalConfig: GlobalConfig | undefined;
1062
-
1063
- if (yamlContent.global) {
1064
- globalConfig = yamlContent.global;
1065
- }
1066
-
1067
- const variables = {
1068
- ...yamlContent.global?.variables,
1069
- ...yamlContent.collection?.variables,
1070
- };
1071
-
1072
- const defaults = {
1073
- ...yamlContent.global?.defaults,
1074
- ...yamlContent.collection?.defaults,
1075
- };
1076
-
1077
- if (yamlContent.request) {
1078
- const request = this.prepareRequest(yamlContent.request, variables, defaults);
1079
- requests.push(request);
1080
- }
1081
-
1082
- if (yamlContent.requests) {
1083
- for (const req of yamlContent.requests) {
1084
- const request = this.prepareRequest(req, variables, defaults);
1085
- requests.push(request);
1086
- }
1087
- }
1088
-
1089
- if (yamlContent.collection?.requests) {
1090
- for (const req of yamlContent.collection.requests) {
1091
- const request = this.prepareRequest(req, variables, defaults);
1092
- requests.push(request);
1093
- }
1094
- }
1095
-
1096
- return { requests, config: globalConfig };
1097
- }
1098
-
1099
- private prepareRequest(
1100
- request: RequestConfig,
1101
- variables: Record<string, string>,
1102
- defaults: Partial<RequestConfig>,
1103
- ): RequestConfig {
1104
- const interpolated = YamlParser.interpolateVariables(request, variables) as RequestConfig;
1105
- return YamlParser.mergeConfigs(defaults, interpolated);
1106
- }
1107
-
1108
- private mergeGlobalConfigs(base: GlobalConfig, override: GlobalConfig): GlobalConfig {
1109
- return {
1110
- ...base,
1111
- ...override,
1112
- variables: { ...base.variables, ...override.variables },
1113
- output: { ...base.output, ...override.output },
1114
- defaults: { ...base.defaults, ...override.defaults },
1115
- ci: { ...base.ci, ...override.ci },
1116
- watch: { ...base.watch, ...override.watch },
1117
- snapshot: { ...base.snapshot, ...override.snapshot },
1118
- diff: { ...base.diff, ...override.diff },
1119
- };
1120
- }
1121
-
1122
- /**
1123
- * Determines the appropriate exit code based on execution results and CI configuration.
1124
- *
1125
- * Exit code logic:
1126
- * - If strictExit is enabled: exit 1 if ANY failures occur
1127
- * - If failOn is set: exit 1 if failures exceed the threshold
1128
- * - If failOnPercentage is set: exit 1 if failure percentage exceeds the threshold
1129
- * - Default behavior: exit 1 only if failures exist AND continueOnError is false
1130
- *
1131
- * @param summary - The execution summary containing success/failure counts
1132
- * @param config - Global configuration including CI exit options
1133
- * @returns 0 for success, 1 for failure
1134
- */
1135
- private determineExitCode(summary: ExecutionSummary, config: GlobalConfig): number {
1136
- const { failed, total } = summary;
1137
- const ci = config.ci;
1138
-
1139
- // If no failures, always exit with 0
1140
- if (failed === 0) {
1141
- return 0;
1142
- }
1143
-
1144
- // Check CI exit code options
1145
- if (ci) {
1146
- // strictExit: exit 1 if ANY failures occur
1147
- if (ci.strictExit) {
1148
- return 1;
1149
- }
1150
-
1151
- // failOn: exit 1 if failures exceed the threshold
1152
- if (ci.failOn !== undefined && failed > ci.failOn) {
1153
- return 1;
1154
- }
1155
-
1156
- // failOnPercentage: exit 1 if failure percentage exceeds the threshold
1157
- if (ci.failOnPercentage !== undefined && total > 0) {
1158
- const failurePercentage = (failed / total) * 100;
1159
- if (failurePercentage > ci.failOnPercentage) {
1160
- return 1;
1161
- }
1162
- }
1163
-
1164
- // If any CI option is set but thresholds not exceeded, exit 0
1165
- if (ci.failOn !== undefined || ci.failOnPercentage !== undefined) {
1166
- return 0;
1167
- }
1168
- }
1169
-
1170
- // Default behavior: exit 1 if failures AND continueOnError is false
1171
- return !config.continueOnError ? 1 : 0;
1172
- }
1173
-
1174
- private showHelp(): void {
1175
- console.log(`
1176
- ${this.logger.color('🚀 CURL RUNNER', 'bright')}
1177
-
1178
- ${this.logger.color('USAGE:', 'yellow')}
1179
- curl-runner [files...] [options]
1180
-
1181
- ${this.logger.color('OPTIONS:', 'yellow')}
1182
- -h, --help Show this help message
1183
- -v, --verbose Enable verbose output
1184
- -q, --quiet Suppress non-error output
1185
- -p, --execution parallel Execute requests in parallel
1186
- --max-concurrent <n> Limit concurrent requests in parallel mode
1187
- -c, --continue-on-error Continue execution on errors
1188
- -o, --output <file> Save results to file
1189
- --all Find all YAML files recursively
1190
- --timeout <ms> Set request timeout in milliseconds
1191
- --retries <count> Set maximum retry attempts
1192
- --retry-delay <ms> Set delay between retries in milliseconds
1193
- --no-retry Disable retry mechanism
1194
- --output-format <format> Set output format (json|pretty|raw)
1195
- --pretty-level <level> Set pretty format level (minimal|standard|detailed)
1196
- --show-headers Include response headers in output
1197
- --show-body Include response body in output
1198
- --show-metrics Include performance metrics in output
1199
- --version Show version
1200
-
1201
- ${this.logger.color('WATCH MODE:', 'yellow')}
1202
- -w, --watch Watch files and re-run on changes
1203
- --watch-debounce <ms> Debounce delay for watch mode (default: 300)
1204
- --no-watch-clear Don't clear screen between watch runs
1205
-
1206
- ${this.logger.color('PROFILE MODE:', 'yellow')}
1207
- -P, --profile <n> Run each request N times for latency stats
1208
- --profile-warmup <n> Warmup iterations to exclude from stats (default: 1)
1209
- --profile-concurrency <n> Concurrent iterations (default: 1 = sequential)
1210
- --profile-histogram Show ASCII histogram of latency distribution
1211
- --profile-export <file> Export raw timings to file (.json or .csv)
1212
-
1213
- ${this.logger.color('CI/CD OPTIONS:', 'yellow')}
1214
- --strict-exit Exit with code 1 if any validation fails (for CI/CD)
1215
- --fail-on <count> Exit with code 1 if failures exceed this count
1216
- --fail-on-percentage <pct> Exit with code 1 if failure percentage exceeds this value
1217
-
1218
- ${this.logger.color('SNAPSHOT OPTIONS:', 'yellow')}
1219
- -s, --snapshot Enable snapshot testing
1220
- -u, --update-snapshots Update all snapshots
1221
- --update-failing Update only failing snapshots
1222
- --snapshot-dir <dir> Custom snapshot directory (default: __snapshots__)
1223
- --ci-snapshot Fail if snapshot is missing (CI mode)
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
-
1242
- ${this.logger.color('EXAMPLES:', 'yellow')}
1243
- # Run all YAML files in current directory
1244
- curl-runner
1245
-
1246
- # Run specific file
1247
- curl-runner api-tests.yaml
1248
-
1249
- # Run all files in a directory
1250
- curl-runner examples/
1251
-
1252
- # Run all files in multiple directories
1253
- curl-runner tests/ examples/
1254
-
1255
- # Run all files recursively in parallel
1256
- curl-runner --all -p
1257
-
1258
- # Run in parallel with max 5 concurrent requests
1259
- curl-runner -p --max-concurrent 5 tests.yaml
1260
-
1261
- # Run directory recursively
1262
- curl-runner --all examples/
1263
-
1264
- # Run with verbose output and continue on errors
1265
- curl-runner tests/*.yaml -vc
1266
-
1267
- # Run with minimal pretty output (only status and errors)
1268
- curl-runner --output-format pretty --pretty-level minimal test.yaml
1269
-
1270
- # Run with detailed pretty output (show all information)
1271
- curl-runner --output-format pretty --pretty-level detailed test.yaml
1272
-
1273
- # CI/CD: Fail if any validation fails (strict mode)
1274
- curl-runner tests/ --strict-exit
1275
-
1276
- # CI/CD: Run all tests but fail if any validation fails
1277
- curl-runner tests/ --continue-on-error --strict-exit
1278
-
1279
- # CI/CD: Allow up to 2 failures
1280
- curl-runner tests/ --fail-on 2
1281
-
1282
- # CI/CD: Allow up to 10% failures
1283
- curl-runner tests/ --fail-on-percentage 10
1284
-
1285
- # Watch mode - re-run on file changes
1286
- curl-runner api.yaml --watch
1287
-
1288
- # Watch with custom debounce
1289
- curl-runner tests/ -w --watch-debounce 500
1290
-
1291
- # Profile mode - run request 100 times for latency stats
1292
- curl-runner api.yaml -P 100
1293
-
1294
- # Profile with 5 warmup iterations and histogram
1295
- curl-runner api.yaml --profile 50 --profile-warmup 5 --profile-histogram
1296
-
1297
- # Profile with concurrent iterations and export
1298
- curl-runner api.yaml -P 100 --profile-concurrency 10 --profile-export results.json
1299
-
1300
- # Snapshot testing - save and compare responses
1301
- curl-runner api.yaml --snapshot
1302
-
1303
- # Update all snapshots
1304
- curl-runner api.yaml -su
1305
-
1306
- # CI mode - fail if snapshot missing
1307
- curl-runner api.yaml --snapshot --ci-snapshot
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
-
1324
- ${this.logger.color('YAML STRUCTURE:', 'yellow')}
1325
- Single request:
1326
- request:
1327
- url: https://api.example.com
1328
- method: GET
1329
-
1330
- Multiple requests:
1331
- requests:
1332
- - url: https://api.example.com/users
1333
- method: GET
1334
- - url: https://api.example.com/posts
1335
- method: POST
1336
- body: { title: "Test" }
1337
-
1338
- With global config:
1339
- global:
1340
- execution: parallel
1341
- variables:
1342
- BASE_URL: https://api.example.com
1343
- requests:
1344
- - url: \${BASE_URL}/users
1345
- method: GET
1346
- `);
1347
- }
1348
- }
1349
-
1350
- const cli = new CurlRunnerCLI();
1351
- cli.run(process.argv.slice(2));