@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
@@ -1,856 +0,0 @@
1
- import { SnapshotFormatter } from '../snapshot/snapshot-formatter';
2
- import type {
3
- ExecutionResult,
4
- ExecutionSummary,
5
- GlobalConfig,
6
- ProfileResult,
7
- RequestConfig,
8
- } from '../types/config';
9
- import { generateHistogram } from './stats';
10
-
11
- interface TreeNode {
12
- label: string;
13
- value?: string;
14
- children?: TreeNode[];
15
- color?: string;
16
- }
17
-
18
- class TreeRenderer {
19
- private colors: Record<string, string>;
20
-
21
- constructor(colors: Record<string, string>) {
22
- this.colors = colors;
23
- }
24
-
25
- private color(text: string, colorName: string): string {
26
- if (!colorName || !this.colors[colorName]) {
27
- return text;
28
- }
29
- return `${this.colors[colorName]}${text}${this.colors.reset}`;
30
- }
31
-
32
- render(nodes: TreeNode[], basePrefix: string = ' '): void {
33
- nodes.forEach((node, index) => {
34
- const isLast = index === nodes.length - 1;
35
- const prefix = isLast ? `${basePrefix}└─` : `${basePrefix}├─`;
36
-
37
- if (node.label && node.value) {
38
- // Regular labeled node with value
39
- const displayValue = node.color ? this.color(node.value, node.color) : node.value;
40
-
41
- // Handle multiline values (like Response Body)
42
- const lines = displayValue.split('\n');
43
- if (lines.length === 1) {
44
- console.log(`${prefix} ${node.label}: ${displayValue}`);
45
- } else {
46
- console.log(`${prefix} ${node.label}:`);
47
- const contentPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
48
- lines.forEach((line) => {
49
- console.log(`${contentPrefix}${line}`);
50
- });
51
- }
52
- } else if (node.label && !node.value) {
53
- // Section header (like "Headers:" or "Metrics:")
54
- console.log(`${prefix} ${node.label}:`);
55
- } else if (!node.label && node.value) {
56
- // Content line without label (like response body lines)
57
- const continuationPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
58
- console.log(`${continuationPrefix}${node.value}`);
59
- }
60
-
61
- if (node.children && node.children.length > 0) {
62
- const childPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
63
- this.render(node.children, childPrefix);
64
- }
65
- });
66
- }
67
- }
68
-
69
- export class Logger {
70
- private config: GlobalConfig['output'];
71
-
72
- private readonly colors = {
73
- reset: '\x1b[0m',
74
- bright: '\x1b[1m',
75
- dim: '\x1b[2m',
76
- underscore: '\x1b[4m',
77
-
78
- black: '\x1b[30m',
79
- red: '\x1b[31m',
80
- green: '\x1b[32m',
81
- yellow: '\x1b[33m',
82
- blue: '\x1b[34m',
83
- magenta: '\x1b[35m',
84
- cyan: '\x1b[36m',
85
- white: '\x1b[37m',
86
-
87
- bgBlack: '\x1b[40m',
88
- bgRed: '\x1b[41m',
89
- bgGreen: '\x1b[42m',
90
- bgYellow: '\x1b[43m',
91
- bgBlue: '\x1b[44m',
92
- bgMagenta: '\x1b[45m',
93
- bgCyan: '\x1b[46m',
94
- bgWhite: '\x1b[47m',
95
- };
96
-
97
- constructor(config: GlobalConfig['output'] = {}) {
98
- this.config = {
99
- verbose: false,
100
- showHeaders: false,
101
- showBody: true,
102
- showMetrics: false,
103
- format: 'pretty',
104
- prettyLevel: 'minimal',
105
- ...config,
106
- };
107
- }
108
-
109
- color(text: string, color: keyof typeof this.colors): string {
110
- return `${this.colors[color]}${text}${this.colors.reset}`;
111
- }
112
-
113
- private getShortFilename(filePath: string): string {
114
- return filePath.replace(/.*\//, '').replace('.yaml', '');
115
- }
116
-
117
- private shouldShowOutput(): boolean {
118
- if (this.config.format === 'raw') {
119
- return false;
120
- }
121
- if (this.config.format === 'pretty') {
122
- return true; // Pretty format should always show output
123
- }
124
- return this.config.verbose !== false; // For other formats, respect verbose flag
125
- }
126
-
127
- private shouldShowHeaders(): boolean {
128
- if (this.config.format !== 'pretty') {
129
- return this.config.showHeaders || false;
130
- }
131
-
132
- const level = this.config.prettyLevel || 'standard';
133
- switch (level) {
134
- case 'minimal':
135
- return false;
136
- case 'standard':
137
- return this.config.showHeaders || false;
138
- case 'detailed':
139
- return true;
140
- default:
141
- return this.config.showHeaders || false;
142
- }
143
- }
144
-
145
- private shouldShowBody(): boolean {
146
- if (this.config.format !== 'pretty') {
147
- return this.config.showBody !== false;
148
- }
149
-
150
- const level = this.config.prettyLevel || 'standard';
151
- switch (level) {
152
- case 'minimal':
153
- return false; // Minimal never shows body
154
- case 'standard':
155
- return this.config.showBody !== false;
156
- case 'detailed':
157
- return true; // Detailed always shows body
158
- default:
159
- return this.config.showBody !== false;
160
- }
161
- }
162
-
163
- private shouldShowMetrics(): boolean {
164
- if (this.config.format !== 'pretty') {
165
- return this.config.showMetrics || false;
166
- }
167
-
168
- const level = this.config.prettyLevel || 'standard';
169
- switch (level) {
170
- case 'minimal':
171
- return false; // Minimal never shows metrics
172
- case 'standard':
173
- return this.config.showMetrics || false;
174
- case 'detailed':
175
- return true; // Detailed always shows metrics
176
- default:
177
- return this.config.showMetrics || false;
178
- }
179
- }
180
-
181
- private shouldShowRequestDetails(): boolean {
182
- if (this.config.format !== 'pretty') {
183
- return this.config.verbose || false;
184
- }
185
-
186
- const level = this.config.prettyLevel || 'standard';
187
- switch (level) {
188
- case 'minimal':
189
- return false;
190
- case 'standard':
191
- return this.config.verbose || false;
192
- case 'detailed':
193
- return true;
194
- default:
195
- return this.config.verbose || false;
196
- }
197
- }
198
-
199
- private shouldShowSeparators(): boolean {
200
- if (this.config.format !== 'pretty') {
201
- return true;
202
- }
203
-
204
- const level = this.config.prettyLevel || 'standard';
205
- switch (level) {
206
- case 'minimal':
207
- return false;
208
- case 'standard':
209
- return true;
210
- case 'detailed':
211
- return true;
212
- default:
213
- return true;
214
- }
215
- }
216
-
217
- private colorStatusCode(statusStr: string): string {
218
- // For expected status codes in validation errors, use yellow to distinguish from red actual values
219
- return this.color(statusStr, 'yellow');
220
- }
221
-
222
- private logValidationErrors(errorString: string): void {
223
- // Check if this is a validation error with multiple parts (separated by ';')
224
- const errors = errorString.split('; ');
225
-
226
- if (errors.length === 1) {
227
- // Single error - check if it's a status error for special formatting
228
- const trimmedError = errors[0].trim();
229
- const statusMatch = trimmedError.match(/^Expected status (.+?), got (.+)$/);
230
- if (statusMatch) {
231
- const [, expected, actual] = statusMatch;
232
- const expectedStatus = this.colorStatusCode(expected.replace(' or ', '|'));
233
- const actualStatus = this.color(actual, 'red'); // Always red for incorrect actual values
234
- console.log(
235
- ` ${this.color('✗', 'red')} ${this.color('Error:', 'red')} Expected ${this.color('status', 'yellow')} ${expectedStatus}, got ${actualStatus}`,
236
- );
237
- } else {
238
- console.log(` ${this.color('✗', 'red')} ${this.color('Error:', 'red')} ${trimmedError}`);
239
- }
240
- } else {
241
- // Multiple validation errors - show them nicely formatted
242
- console.log(` ${this.color('✗', 'red')} ${this.color('Validation Errors:', 'red')}`);
243
- for (const error of errors) {
244
- const trimmedError = error.trim();
245
- if (trimmedError) {
246
- // Parse different error formats for better formatting
247
- if (trimmedError.startsWith('Expected ')) {
248
- // Format 1: "Expected status 201, got 200"
249
- const statusMatch = trimmedError.match(/^Expected status (.+?), got (.+)$/);
250
- if (statusMatch) {
251
- const [, expected, actual] = statusMatch;
252
- const expectedStatus = this.colorStatusCode(expected.replace(' or ', '|'));
253
- const actualStatus = this.color(actual, 'red'); // Always red for incorrect actual values
254
- console.log(
255
- ` ${this.color('•', 'red')} ${this.color('status', 'yellow')}: expected ${expectedStatus}, got ${actualStatus}`,
256
- );
257
- } else {
258
- // Format 2: "Expected field to be value, got value"
259
- const fieldMatch = trimmedError.match(/^Expected (.+?) to be (.+?), got (.+)$/);
260
- if (fieldMatch) {
261
- const [, field, expected, actual] = fieldMatch;
262
- console.log(
263
- ` ${this.color('•', 'red')} ${this.color(field, 'yellow')}: expected ${this.color(expected, 'green')}, got ${this.color(actual, 'red')}`,
264
- );
265
- } else {
266
- console.log(` ${this.color('•', 'red')} ${trimmedError}`);
267
- }
268
- }
269
- } else {
270
- console.log(` ${this.color('•', 'red')} ${trimmedError}`);
271
- }
272
- }
273
- }
274
- }
275
- }
276
-
277
- private formatJson(data: unknown): string {
278
- if (this.config.format === 'raw') {
279
- return typeof data === 'string' ? data : JSON.stringify(data);
280
- }
281
- if (this.config.format === 'json') {
282
- return JSON.stringify(data);
283
- }
284
- return JSON.stringify(data, null, 2);
285
- }
286
-
287
- private formatDuration(ms: number): string {
288
- if (ms < 1000) {
289
- return `${ms.toFixed(0)}ms`;
290
- }
291
- return `${(ms / 1000).toFixed(2)}s`;
292
- }
293
-
294
- private formatSize(bytes: number | undefined): string {
295
- if (!bytes) {
296
- return '0 B';
297
- }
298
- const sizes = ['B', 'KB', 'MB', 'GB'];
299
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
300
- return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
301
- }
302
-
303
- logExecutionStart(count: number, mode: string): void {
304
- if (!this.shouldShowOutput()) {
305
- return;
306
- }
307
-
308
- if (this.shouldShowSeparators()) {
309
- console.log(); // Add spacing before the execution header
310
- console.log(this.color(`Executing ${count} request(s) in ${mode} mode`, 'dim'));
311
- console.log();
312
- } else {
313
- // For minimal format, still add spacing after processing info
314
- console.log();
315
- }
316
- }
317
-
318
- logRequestStart(_config: RequestConfig, _index: number): void {
319
- // In the new format, we show everything in logRequestComplete
320
- // This method is kept for compatibility but simplified
321
- return;
322
- }
323
-
324
- logCommand(command: string): void {
325
- if (this.shouldShowRequestDetails()) {
326
- console.log(this.color(' Command:', 'dim'));
327
- console.log(this.color(` ${command}`, 'dim'));
328
- }
329
- }
330
-
331
- logRetry(attempt: number, maxRetries: number): void {
332
- console.log(this.color(` ↻ Retry ${attempt}/${maxRetries}...`, 'yellow'));
333
- }
334
-
335
- logRequestComplete(result: ExecutionResult): void {
336
- // Handle raw format output - only show response body
337
- if (this.config.format === 'raw') {
338
- if (result.success && this.config.showBody && result.body) {
339
- const bodyStr = this.formatJson(result.body);
340
- console.log(bodyStr);
341
- }
342
- return;
343
- }
344
-
345
- // Handle JSON format output - structured JSON only
346
- if (this.config.format === 'json') {
347
- const jsonResult = {
348
- request: {
349
- name: result.request.name,
350
- url: result.request.url,
351
- method: result.request.method || 'GET',
352
- },
353
- success: result.success,
354
- status: result.status,
355
- ...(this.shouldShowHeaders() && result.headers ? { headers: result.headers } : {}),
356
- ...(this.shouldShowBody() && result.body ? { body: result.body } : {}),
357
- ...(result.error ? { error: result.error } : {}),
358
- ...(this.shouldShowMetrics() && result.metrics ? { metrics: result.metrics } : {}),
359
- };
360
- console.log(JSON.stringify(jsonResult, null, 2));
361
- return;
362
- }
363
-
364
- // Pretty format output (default behavior)
365
- if (!this.shouldShowOutput()) {
366
- return;
367
- }
368
-
369
- const level = this.config.prettyLevel || 'minimal';
370
- const statusColor = result.success ? 'green' : 'red';
371
- const statusIcon = result.success ? '✓' : 'x';
372
- const name = result.request.name || 'Request';
373
-
374
- if (level === 'minimal') {
375
- // Minimal format: clean tree structure but compact
376
- const fileTag = result.request.sourceFile
377
- ? this.getShortFilename(result.request.sourceFile)
378
- : 'inline';
379
- console.log(
380
- `${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')} [${fileTag}]`,
381
- );
382
-
383
- const treeNodes: TreeNode[] = [];
384
- const renderer = new TreeRenderer(this.colors);
385
-
386
- treeNodes.push({
387
- label: result.request.method || 'GET',
388
- value: result.request.url,
389
- color: 'blue',
390
- });
391
-
392
- const statusText = result.status ? `${result.status}` : 'ERROR';
393
- treeNodes.push({
394
- label: `${statusIcon} Status`,
395
- value: statusText,
396
- color: statusColor,
397
- });
398
-
399
- if (result.metrics) {
400
- const durationSize = `${this.formatDuration(result.metrics.duration)} | ${this.formatSize(result.metrics.size)}`;
401
- treeNodes.push({
402
- label: 'Duration',
403
- value: durationSize,
404
- color: 'cyan',
405
- });
406
- }
407
-
408
- renderer.render(treeNodes);
409
-
410
- if (result.error) {
411
- console.log();
412
- this.logValidationErrors(result.error);
413
- }
414
-
415
- // Show snapshot result
416
- if (result.snapshotResult) {
417
- console.log();
418
- this.logSnapshotResult(result.request.name || 'Request', result.snapshotResult);
419
- }
420
-
421
- console.log();
422
- return;
423
- }
424
-
425
- // Standard and detailed formats: use clean tree structure
426
- console.log(`${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')}`);
427
-
428
- // Build tree structure
429
- const treeNodes: TreeNode[] = [];
430
- const renderer = new TreeRenderer(this.colors);
431
-
432
- // Main info nodes
433
- treeNodes.push({ label: 'URL', value: result.request.url, color: 'blue' });
434
- treeNodes.push({ label: 'Method', value: result.request.method || 'GET', color: 'yellow' });
435
- treeNodes.push({
436
- label: 'Status',
437
- value: String(result.status || 'ERROR'),
438
- color: statusColor,
439
- });
440
-
441
- if (result.metrics) {
442
- treeNodes.push({
443
- label: 'Duration',
444
- value: this.formatDuration(result.metrics.duration),
445
- color: 'cyan',
446
- });
447
- }
448
-
449
- // Add headers section if needed
450
- if (this.shouldShowHeaders() && result.headers && Object.keys(result.headers).length > 0) {
451
- const headerChildren: TreeNode[] = Object.entries(result.headers).map(([key, value]) => ({
452
- label: this.color(key, 'dim'),
453
- value: String(value),
454
- }));
455
-
456
- treeNodes.push({
457
- label: 'Headers',
458
- children: headerChildren,
459
- });
460
- }
461
-
462
- // Add body section if needed
463
- if (this.shouldShowBody() && result.body) {
464
- const bodyStr = this.formatJson(result.body);
465
- const lines = bodyStr.split('\n');
466
- const maxLines = this.shouldShowRequestDetails() ? Infinity : 10;
467
- const bodyLines = lines.slice(0, maxLines);
468
-
469
- if (lines.length > maxLines) {
470
- bodyLines.push(this.color(`... (${lines.length - maxLines} more lines)`, 'dim'));
471
- }
472
-
473
- treeNodes.push({
474
- label: 'Response Body',
475
- value: bodyLines.join('\n'),
476
- });
477
- }
478
-
479
- // Add detailed metrics section if needed
480
- if (this.shouldShowMetrics() && result.metrics && level === 'detailed') {
481
- const metrics = result.metrics;
482
- const metricChildren: TreeNode[] = [];
483
-
484
- metricChildren.push({
485
- label: 'Request Duration',
486
- value: this.formatDuration(metrics.duration),
487
- color: 'cyan',
488
- });
489
-
490
- if (metrics.size !== undefined) {
491
- metricChildren.push({
492
- label: 'Response Size',
493
- value: this.formatSize(metrics.size),
494
- color: 'cyan',
495
- });
496
- }
497
-
498
- if (metrics.dnsLookup) {
499
- metricChildren.push({
500
- label: 'DNS Lookup',
501
- value: this.formatDuration(metrics.dnsLookup),
502
- color: 'cyan',
503
- });
504
- }
505
-
506
- if (metrics.tcpConnection) {
507
- metricChildren.push({
508
- label: 'TCP Connection',
509
- value: this.formatDuration(metrics.tcpConnection),
510
- color: 'cyan',
511
- });
512
- }
513
-
514
- if (metrics.tlsHandshake) {
515
- metricChildren.push({
516
- label: 'TLS Handshake',
517
- value: this.formatDuration(metrics.tlsHandshake),
518
- color: 'cyan',
519
- });
520
- }
521
-
522
- if (metrics.firstByte) {
523
- metricChildren.push({
524
- label: 'Time to First Byte',
525
- value: this.formatDuration(metrics.firstByte),
526
- color: 'cyan',
527
- });
528
- }
529
-
530
- treeNodes.push({
531
- label: 'Metrics',
532
- children: metricChildren,
533
- });
534
- }
535
-
536
- // Render the tree
537
- renderer.render(treeNodes);
538
-
539
- if (result.error) {
540
- console.log();
541
- this.logValidationErrors(result.error);
542
- }
543
-
544
- // Show snapshot result
545
- if (result.snapshotResult) {
546
- console.log();
547
- this.logSnapshotResult(result.request.name || 'Request', result.snapshotResult);
548
- }
549
-
550
- console.log();
551
- }
552
-
553
- /**
554
- * Logs snapshot comparison result.
555
- */
556
- private logSnapshotResult(requestName: string, result: ExecutionResult['snapshotResult']): void {
557
- if (!result) {
558
- return;
559
- }
560
-
561
- const formatter = new SnapshotFormatter();
562
- console.log(formatter.formatResult(requestName, result));
563
- }
564
-
565
- logSummary(summary: ExecutionSummary, isGlobal: boolean = false): void {
566
- // For raw format, don't show summary
567
- if (this.config.format === 'raw') {
568
- return;
569
- }
570
-
571
- // For JSON format, output structured summary
572
- if (this.config.format === 'json') {
573
- const jsonSummary = {
574
- summary: {
575
- total: summary.total,
576
- successful: summary.successful,
577
- failed: summary.failed,
578
- skipped: summary.skipped,
579
- duration: summary.duration,
580
- },
581
- results: summary.results.map((result) => ({
582
- request: {
583
- name: result.request.name,
584
- url: result.request.url,
585
- method: result.request.method || 'GET',
586
- },
587
- success: result.success,
588
- status: result.status,
589
- ...(this.shouldShowHeaders() && result.headers ? { headers: result.headers } : {}),
590
- ...(this.shouldShowBody() && result.body ? { body: result.body } : {}),
591
- ...(result.error ? { error: result.error } : {}),
592
- ...(this.shouldShowMetrics() && result.metrics ? { metrics: result.metrics } : {}),
593
- })),
594
- };
595
- console.log(JSON.stringify(jsonSummary, null, 2));
596
- return;
597
- }
598
-
599
- // Pretty format summary (default behavior)
600
- if (!this.shouldShowOutput()) {
601
- return;
602
- }
603
-
604
- const level = this.config.prettyLevel || 'minimal';
605
-
606
- // Add spacing for global summary
607
- if (isGlobal) {
608
- console.log(); // Extra spacing before global summary
609
- }
610
-
611
- if (level === 'minimal') {
612
- // Simple one-line summary for minimal, similar to docs example
613
- const statusColor = summary.failed === 0 ? 'green' : 'red';
614
- const skippedText = summary.skipped > 0 ? `, ${summary.skipped} skipped` : '';
615
- const successText =
616
- summary.failed === 0
617
- ? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully${skippedText}`
618
- : `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed${skippedText}`;
619
-
620
- const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
621
- console.log(`${summaryPrefix}: ${this.color(successText, statusColor)}`);
622
- return;
623
- }
624
-
625
- // Compact summary for standard/detailed - much simpler
626
- const _successRate = ((summary.successful / summary.total) * 100).toFixed(1);
627
- const statusColor = summary.failed === 0 ? 'green' : 'red';
628
- const skippedText = summary.skipped > 0 ? `, ${summary.skipped} skipped` : '';
629
- const successText =
630
- summary.failed === 0
631
- ? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully${skippedText}`
632
- : `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed${skippedText}`;
633
-
634
- const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
635
- console.log();
636
- console.log(
637
- `${summaryPrefix}: ${this.color(successText, statusColor)} (${this.color(this.formatDuration(summary.duration), 'cyan')})`,
638
- );
639
-
640
- if (summary.failed > 0 && this.shouldShowRequestDetails()) {
641
- summary.results
642
- .filter((r) => !r.success)
643
- .forEach((r) => {
644
- const name = r.request.name || r.request.url;
645
- console.log(` ${this.color('•', 'red')} ${name}: ${r.error}`);
646
- });
647
- }
648
- }
649
-
650
- logError(message: string): void {
651
- console.error(this.color(`✗ ${message}`, 'red'));
652
- }
653
-
654
- logWarning(message: string): void {
655
- console.warn(this.color(`⚠ ${message}`, 'yellow'));
656
- }
657
-
658
- logInfo(message: string): void {
659
- console.log(this.color(`ℹ ${message}`, 'blue'));
660
- }
661
-
662
- logSuccess(message: string): void {
663
- console.log(this.color(`✓ ${message}`, 'green'));
664
- }
665
-
666
- logSkipped(config: RequestConfig, index: number, reason?: string): void {
667
- if (!this.shouldShowOutput()) {
668
- return;
669
- }
670
-
671
- const name = config.name || `Request ${index}`;
672
-
673
- if (this.config.format === 'json') {
674
- const jsonResult = {
675
- request: {
676
- name: config.name,
677
- url: config.url,
678
- method: config.method || 'GET',
679
- },
680
- skipped: true,
681
- reason: reason || 'condition not met',
682
- };
683
- console.log(JSON.stringify(jsonResult, null, 2));
684
- return;
685
- }
686
-
687
- // Pretty format
688
- console.log(
689
- `${this.color('⊘', 'yellow')} ${this.color(name, 'bright')} ${this.color('[SKIP]', 'yellow')}`,
690
- );
691
-
692
- if (reason) {
693
- const treeNodes: TreeNode[] = [{ label: 'Reason', value: reason, color: 'yellow' }];
694
- const renderer = new TreeRenderer(this.colors);
695
- renderer.render(treeNodes);
696
- }
697
-
698
- console.log();
699
- }
700
-
701
- logFileHeader(fileName: string, requestCount: number): void {
702
- if (!this.shouldShowOutput() || this.config.format !== 'pretty') {
703
- return;
704
- }
705
-
706
- const shortName = fileName.replace(/.*\//, '').replace('.yaml', '');
707
- console.log();
708
- console.log(
709
- this.color(`▶ ${shortName}.yaml`, 'bright') +
710
- this.color(` (${requestCount} request${requestCount === 1 ? '' : 's'})`, 'dim'),
711
- );
712
- }
713
-
714
- logWatch(files: string[]): void {
715
- console.log();
716
- console.log(
717
- `${this.color('Watching for changes...', 'cyan')} ${this.color('(press Ctrl+C to stop)', 'dim')}`,
718
- );
719
- const fileList = files.length <= 3 ? files.join(', ') : `${files.length} files`;
720
- console.log(this.color(` Files: ${fileList}`, 'dim'));
721
- console.log();
722
- }
723
-
724
- logWatchReady(): void {
725
- console.log();
726
- console.log(this.color('Watching for changes...', 'cyan'));
727
- }
728
-
729
- logFileChanged(filename: string): void {
730
- const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false });
731
- console.log(this.color('-'.repeat(50), 'dim'));
732
- console.log(
733
- `${this.color(`[${timestamp}]`, 'dim')} File changed: ${this.color(filename, 'yellow')}`,
734
- );
735
- console.log();
736
- }
737
-
738
- logProfileStart(
739
- requestName: string,
740
- iterations: number,
741
- warmup: number,
742
- concurrency: number,
743
- ): void {
744
- if (!this.shouldShowOutput()) {
745
- return;
746
- }
747
-
748
- console.log();
749
- console.log(`${this.color('⚡ PROFILING', 'magenta')} ${this.color(requestName, 'bright')}`);
750
- console.log(
751
- this.color(
752
- ` ${iterations} iterations, ${warmup} warmup, concurrency: ${concurrency}`,
753
- 'dim',
754
- ),
755
- );
756
- }
757
-
758
- logProfileResult(result: ProfileResult, showHistogram: boolean): void {
759
- const { stats, request } = result;
760
- const name = request.name || request.url;
761
-
762
- if (this.config.format === 'json') {
763
- console.log(
764
- JSON.stringify({
765
- request: { name, url: request.url, method: request.method || 'GET' },
766
- stats: {
767
- iterations: stats.iterations,
768
- warmup: stats.warmup,
769
- failures: stats.failures,
770
- failureRate: stats.failureRate,
771
- min: stats.min,
772
- max: stats.max,
773
- mean: stats.mean,
774
- median: stats.median,
775
- p50: stats.p50,
776
- p95: stats.p95,
777
- p99: stats.p99,
778
- stdDev: stats.stdDev,
779
- },
780
- }),
781
- );
782
- return;
783
- }
784
-
785
- if (this.config.format === 'raw') {
786
- // Raw format: just print the key stats
787
- console.log(`${stats.p50}\t${stats.p95}\t${stats.p99}\t${stats.mean}`);
788
- return;
789
- }
790
-
791
- // Pretty format
792
- console.log();
793
- const statusIcon = stats.failures === 0 ? this.color('✓', 'green') : this.color('⚠', 'yellow');
794
- console.log(`${statusIcon} ${this.color(name, 'bright')}`);
795
-
796
- // Latency stats table
797
- console.log(this.color(' ┌─────────────────────────────────────┐', 'dim'));
798
- console.log(
799
- ` │ ${this.color('p50', 'cyan')} ${this.formatLatency(stats.p50).padStart(10)} │ ${this.color('min', 'dim')} ${this.formatLatency(stats.min).padStart(10)} │`,
800
- );
801
- console.log(
802
- ` │ ${this.color('p95', 'yellow')} ${this.formatLatency(stats.p95).padStart(10)} │ ${this.color('max', 'dim')} ${this.formatLatency(stats.max).padStart(10)} │`,
803
- );
804
- console.log(
805
- ` │ ${this.color('p99', 'red')} ${this.formatLatency(stats.p99).padStart(10)} │ ${this.color('mean', 'dim')} ${this.formatLatency(stats.mean).padStart(10)} │`,
806
- );
807
- console.log(this.color(' └─────────────────────────────────────┘', 'dim'));
808
-
809
- // Additional stats
810
- console.log(
811
- this.color(
812
- ` σ ${stats.stdDev.toFixed(2)}ms | ${stats.iterations} samples | ${stats.failures} failures (${stats.failureRate}%)`,
813
- 'dim',
814
- ),
815
- );
816
-
817
- // Optional histogram
818
- if (showHistogram && stats.timings.length > 0) {
819
- console.log();
820
- console.log(this.color(' Distribution:', 'dim'));
821
- const histogramLines = generateHistogram(stats.timings, 8, 30);
822
- for (const line of histogramLines) {
823
- console.log(` ${this.color(line, 'dim')}`);
824
- }
825
- }
826
- }
827
-
828
- private formatLatency(ms: number): string {
829
- if (ms < 1) {
830
- return `${(ms * 1000).toFixed(0)}µs`;
831
- }
832
- if (ms < 1000) {
833
- return `${ms.toFixed(1)}ms`;
834
- }
835
- return `${(ms / 1000).toFixed(2)}s`;
836
- }
837
-
838
- logProfileSummary(results: ProfileResult[]): void {
839
- if (!this.shouldShowOutput()) {
840
- return;
841
- }
842
-
843
- const totalIterations = results.reduce((sum, r) => sum + r.stats.iterations, 0);
844
- const totalFailures = results.reduce((sum, r) => sum + r.stats.failures, 0);
845
-
846
- console.log();
847
- console.log(this.color('─'.repeat(50), 'dim'));
848
- console.log(
849
- `${this.color('⚡ Profile Summary:', 'magenta')} ${results.length} request${results.length === 1 ? '' : 's'}, ${totalIterations} total iterations`,
850
- );
851
-
852
- if (totalFailures > 0) {
853
- console.log(this.color(` ${totalFailures} total failures`, 'yellow'));
854
- }
855
- }
856
- }