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