@curl-runner/cli 1.0.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.
- package/README.md +518 -0
- package/package.json +43 -0
- package/src/cli.ts +562 -0
- package/src/executor/request-executor.ts +490 -0
- package/src/parser/yaml.ts +99 -0
- package/src/types/config.ts +106 -0
- package/src/utils/curl-builder.ts +152 -0
- package/src/utils/logger.ts +501 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Glob } from 'bun';
|
|
4
|
+
import { RequestExecutor } from './executor/request-executor';
|
|
5
|
+
import { YamlParser } from './parser/yaml';
|
|
6
|
+
import type { GlobalConfig, RequestConfig } from './types/config';
|
|
7
|
+
import { Logger } from './utils/logger';
|
|
8
|
+
|
|
9
|
+
class CurlRunnerCLI {
|
|
10
|
+
private logger = new Logger();
|
|
11
|
+
|
|
12
|
+
private async loadConfigFile(): Promise<Partial<GlobalConfig>> {
|
|
13
|
+
const configFiles = [
|
|
14
|
+
'curl-runner.yaml',
|
|
15
|
+
'curl-runner.yml',
|
|
16
|
+
'.curl-runner.yaml',
|
|
17
|
+
'.curl-runner.yml',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const filename of configFiles) {
|
|
21
|
+
try {
|
|
22
|
+
const file = Bun.file(filename);
|
|
23
|
+
if (await file.exists()) {
|
|
24
|
+
const yamlContent = await YamlParser.parseFile(filename);
|
|
25
|
+
// Extract global config from the YAML file
|
|
26
|
+
const config = yamlContent.global || yamlContent;
|
|
27
|
+
this.logger.logInfo(`Loaded configuration from ${filename}`);
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
this.logger.logWarning(`Failed to load configuration from ${filename}: ${error}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private loadEnvironmentVariables(): Partial<GlobalConfig> {
|
|
39
|
+
const envConfig: Partial<GlobalConfig> = {};
|
|
40
|
+
|
|
41
|
+
// Load environment variables
|
|
42
|
+
if (process.env.CURL_RUNNER_TIMEOUT) {
|
|
43
|
+
envConfig.defaults = {
|
|
44
|
+
...envConfig.defaults,
|
|
45
|
+
timeout: Number.parseInt(process.env.CURL_RUNNER_TIMEOUT, 10),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.env.CURL_RUNNER_RETRIES) {
|
|
50
|
+
envConfig.defaults = {
|
|
51
|
+
...envConfig.defaults,
|
|
52
|
+
retry: {
|
|
53
|
+
...envConfig.defaults?.retry,
|
|
54
|
+
count: Number.parseInt(process.env.CURL_RUNNER_RETRIES, 10),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.env.CURL_RUNNER_RETRY_DELAY) {
|
|
60
|
+
envConfig.defaults = {
|
|
61
|
+
...envConfig.defaults,
|
|
62
|
+
retry: {
|
|
63
|
+
...envConfig.defaults?.retry,
|
|
64
|
+
delay: Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY, 10),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.env.CURL_RUNNER_VERBOSE) {
|
|
70
|
+
envConfig.output = {
|
|
71
|
+
...envConfig.output,
|
|
72
|
+
verbose: process.env.CURL_RUNNER_VERBOSE.toLowerCase() === 'true',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.env.CURL_RUNNER_EXECUTION) {
|
|
77
|
+
envConfig.execution = process.env.CURL_RUNNER_EXECUTION as 'sequential' | 'parallel';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (process.env.CURL_RUNNER_CONTINUE_ON_ERROR) {
|
|
81
|
+
envConfig.continueOnError =
|
|
82
|
+
process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase() === 'true';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.env.CURL_RUNNER_OUTPUT_FORMAT) {
|
|
86
|
+
const format = process.env.CURL_RUNNER_OUTPUT_FORMAT;
|
|
87
|
+
if (['json', 'pretty', 'raw'].includes(format)) {
|
|
88
|
+
envConfig.output = { ...envConfig.output, format: format as 'json' | 'pretty' | 'raw' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (process.env.CURL_RUNNER_PRETTY_LEVEL) {
|
|
93
|
+
const level = process.env.CURL_RUNNER_PRETTY_LEVEL;
|
|
94
|
+
if (['minimal', 'standard', 'detailed'].includes(level)) {
|
|
95
|
+
envConfig.output = {
|
|
96
|
+
...envConfig.output,
|
|
97
|
+
prettyLevel: level as 'minimal' | 'standard' | 'detailed',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (process.env.CURL_RUNNER_OUTPUT_FILE) {
|
|
103
|
+
envConfig.output = { ...envConfig.output, saveToFile: process.env.CURL_RUNNER_OUTPUT_FILE };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return envConfig;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async run(args: string[]): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
const { files, options } = this.parseArguments(args);
|
|
112
|
+
|
|
113
|
+
if (options.help) {
|
|
114
|
+
this.showHelp();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.version) {
|
|
119
|
+
console.log('curl-runner v1.0.0');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load configuration from environment variables, config file, then CLI options
|
|
124
|
+
const envConfig = this.loadEnvironmentVariables();
|
|
125
|
+
const configFile = await this.loadConfigFile();
|
|
126
|
+
|
|
127
|
+
const yamlFiles = await this.findYamlFiles(files, options);
|
|
128
|
+
|
|
129
|
+
if (yamlFiles.length === 0) {
|
|
130
|
+
this.logger.logError('No YAML files found');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.logger.logInfo(`Found ${yamlFiles.length} YAML file(s)`);
|
|
135
|
+
|
|
136
|
+
let globalConfig: GlobalConfig = this.mergeGlobalConfigs(envConfig, configFile);
|
|
137
|
+
const allRequests: RequestConfig[] = [];
|
|
138
|
+
|
|
139
|
+
// Group requests by file to show clear file separations in output
|
|
140
|
+
const fileGroups: Array<{ file: string; requests: RequestConfig[]; config?: GlobalConfig }> =
|
|
141
|
+
[];
|
|
142
|
+
|
|
143
|
+
for (const file of yamlFiles) {
|
|
144
|
+
this.logger.logInfo(`Processing: ${file}`);
|
|
145
|
+
const { requests, config } = await this.processYamlFile(file);
|
|
146
|
+
|
|
147
|
+
// Associate each request with its source file's output configuration and filename
|
|
148
|
+
const fileOutputConfig = config?.output || {};
|
|
149
|
+
const requestsWithSourceConfig = requests.map((request) => ({
|
|
150
|
+
...request,
|
|
151
|
+
sourceOutputConfig: fileOutputConfig,
|
|
152
|
+
sourceFile: file,
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
// Only merge non-output global configs (execution, continueOnError, variables, defaults)
|
|
156
|
+
if (config) {
|
|
157
|
+
const { ...nonOutputConfig } = config;
|
|
158
|
+
globalConfig = this.mergeGlobalConfigs(globalConfig, nonOutputConfig);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fileGroups.push({ file, requests: requestsWithSourceConfig, config });
|
|
162
|
+
allRequests.push(...requestsWithSourceConfig);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (options.execution) {
|
|
166
|
+
globalConfig.execution = options.execution as 'sequential' | 'parallel';
|
|
167
|
+
}
|
|
168
|
+
if (options.continueOnError !== undefined) {
|
|
169
|
+
globalConfig.continueOnError = options.continueOnError;
|
|
170
|
+
}
|
|
171
|
+
if (options.verbose !== undefined) {
|
|
172
|
+
globalConfig.output = { ...globalConfig.output, verbose: options.verbose };
|
|
173
|
+
}
|
|
174
|
+
if (options.quiet !== undefined) {
|
|
175
|
+
globalConfig.output = { ...globalConfig.output, verbose: false };
|
|
176
|
+
}
|
|
177
|
+
if (options.output) {
|
|
178
|
+
globalConfig.output = { ...globalConfig.output, saveToFile: options.output };
|
|
179
|
+
}
|
|
180
|
+
if (options.outputFormat) {
|
|
181
|
+
globalConfig.output = {
|
|
182
|
+
...globalConfig.output,
|
|
183
|
+
format: options.outputFormat as 'json' | 'pretty' | 'raw',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (options.prettyLevel) {
|
|
187
|
+
globalConfig.output = {
|
|
188
|
+
...globalConfig.output,
|
|
189
|
+
prettyLevel: options.prettyLevel as 'minimal' | 'standard' | 'detailed',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (options.showHeaders !== undefined) {
|
|
193
|
+
globalConfig.output = { ...globalConfig.output, showHeaders: options.showHeaders };
|
|
194
|
+
}
|
|
195
|
+
if (options.showBody !== undefined) {
|
|
196
|
+
globalConfig.output = { ...globalConfig.output, showBody: options.showBody };
|
|
197
|
+
}
|
|
198
|
+
if (options.showMetrics !== undefined) {
|
|
199
|
+
globalConfig.output = { ...globalConfig.output, showMetrics: options.showMetrics };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Apply timeout and retry settings to defaults
|
|
203
|
+
if (options.timeout) {
|
|
204
|
+
globalConfig.defaults = { ...globalConfig.defaults, timeout: options.timeout };
|
|
205
|
+
}
|
|
206
|
+
if (options.retries || options.noRetry) {
|
|
207
|
+
const retryCount = options.noRetry ? 0 : options.retries || 0;
|
|
208
|
+
globalConfig.defaults = {
|
|
209
|
+
...globalConfig.defaults,
|
|
210
|
+
retry: {
|
|
211
|
+
...globalConfig.defaults?.retry,
|
|
212
|
+
count: retryCount,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (options.retryDelay) {
|
|
217
|
+
globalConfig.defaults = {
|
|
218
|
+
...globalConfig.defaults,
|
|
219
|
+
retry: {
|
|
220
|
+
...globalConfig.defaults?.retry,
|
|
221
|
+
delay: options.retryDelay,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (allRequests.length === 0) {
|
|
227
|
+
this.logger.logError('No requests found in YAML files');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const executor = new RequestExecutor(globalConfig);
|
|
232
|
+
let summary: ExecutionSummary;
|
|
233
|
+
|
|
234
|
+
// If multiple files, execute them with file separators for clarity
|
|
235
|
+
if (fileGroups.length > 1) {
|
|
236
|
+
const allResults: ExecutionResult[] = [];
|
|
237
|
+
let totalDuration = 0;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < fileGroups.length; i++) {
|
|
240
|
+
const group = fileGroups[i];
|
|
241
|
+
|
|
242
|
+
// Show file header for better organization
|
|
243
|
+
this.logger.logFileHeader(group.file, group.requests.length);
|
|
244
|
+
|
|
245
|
+
const fileSummary = await executor.execute(group.requests);
|
|
246
|
+
allResults.push(...fileSummary.results);
|
|
247
|
+
totalDuration += fileSummary.duration;
|
|
248
|
+
|
|
249
|
+
// Don't show individual file summaries for cleaner output
|
|
250
|
+
|
|
251
|
+
// Add spacing between files (except for the last one)
|
|
252
|
+
if (i < fileGroups.length - 1) {
|
|
253
|
+
console.log();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create combined summary
|
|
258
|
+
const successful = allResults.filter((r) => r.success).length;
|
|
259
|
+
const failed = allResults.filter((r) => !r.success).length;
|
|
260
|
+
|
|
261
|
+
summary = {
|
|
262
|
+
total: allResults.length,
|
|
263
|
+
successful,
|
|
264
|
+
failed,
|
|
265
|
+
duration: totalDuration,
|
|
266
|
+
results: allResults,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Show final summary
|
|
270
|
+
executor.logger.logSummary(summary, true);
|
|
271
|
+
} else {
|
|
272
|
+
// Single file - use normal execution
|
|
273
|
+
summary = await executor.execute(allRequests);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
process.exit(summary.failed > 0 && !globalConfig.continueOnError ? 1 : 0);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.logger.logError(error instanceof Error ? error.message : String(error));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private parseArguments(args: string[]): { files: string[]; options: Record<string, unknown> } {
|
|
284
|
+
const options: Record<string, unknown> = {};
|
|
285
|
+
const files: string[] = [];
|
|
286
|
+
|
|
287
|
+
for (let i = 0; i < args.length; i++) {
|
|
288
|
+
const arg = args[i];
|
|
289
|
+
|
|
290
|
+
if (arg.startsWith('--')) {
|
|
291
|
+
const key = arg.slice(2);
|
|
292
|
+
const nextArg = args[i + 1];
|
|
293
|
+
|
|
294
|
+
if (key === 'help' || key === 'version') {
|
|
295
|
+
options[key] = true;
|
|
296
|
+
} else if (key === 'no-retry') {
|
|
297
|
+
options.noRetry = true;
|
|
298
|
+
} else if (key === 'quiet') {
|
|
299
|
+
options.quiet = true;
|
|
300
|
+
} else if (key === 'show-headers') {
|
|
301
|
+
options.showHeaders = true;
|
|
302
|
+
} else if (key === 'show-body') {
|
|
303
|
+
options.showBody = true;
|
|
304
|
+
} else if (key === 'show-metrics') {
|
|
305
|
+
options.showMetrics = true;
|
|
306
|
+
} else if (nextArg && !nextArg.startsWith('--')) {
|
|
307
|
+
if (key === 'continue-on-error') {
|
|
308
|
+
options.continueOnError = nextArg === 'true';
|
|
309
|
+
} else if (key === 'verbose') {
|
|
310
|
+
options.verbose = nextArg === 'true';
|
|
311
|
+
} else if (key === 'timeout') {
|
|
312
|
+
options.timeout = Number.parseInt(nextArg, 10);
|
|
313
|
+
} else if (key === 'retries') {
|
|
314
|
+
options.retries = Number.parseInt(nextArg, 10);
|
|
315
|
+
} else if (key === 'retry-delay') {
|
|
316
|
+
options.retryDelay = Number.parseInt(nextArg, 10);
|
|
317
|
+
} else if (key === 'output-format') {
|
|
318
|
+
if (['json', 'pretty', 'raw'].includes(nextArg)) {
|
|
319
|
+
options.outputFormat = nextArg;
|
|
320
|
+
}
|
|
321
|
+
} else if (key === 'pretty-level') {
|
|
322
|
+
if (['minimal', 'standard', 'detailed'].includes(nextArg)) {
|
|
323
|
+
options.prettyLevel = nextArg;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
options[key] = nextArg;
|
|
327
|
+
}
|
|
328
|
+
i++;
|
|
329
|
+
} else {
|
|
330
|
+
options[key] = true;
|
|
331
|
+
}
|
|
332
|
+
} else if (arg.startsWith('-')) {
|
|
333
|
+
const flags = arg.slice(1);
|
|
334
|
+
for (const flag of flags) {
|
|
335
|
+
switch (flag) {
|
|
336
|
+
case 'h':
|
|
337
|
+
options.help = true;
|
|
338
|
+
break;
|
|
339
|
+
case 'v':
|
|
340
|
+
options.verbose = true;
|
|
341
|
+
break;
|
|
342
|
+
case 'p':
|
|
343
|
+
options.execution = 'parallel';
|
|
344
|
+
break;
|
|
345
|
+
case 'c':
|
|
346
|
+
options.continueOnError = true;
|
|
347
|
+
break;
|
|
348
|
+
case 'q':
|
|
349
|
+
options.quiet = true;
|
|
350
|
+
break;
|
|
351
|
+
case 'o': {
|
|
352
|
+
// Handle -o flag for output file
|
|
353
|
+
const outputArg = args[i + 1];
|
|
354
|
+
if (outputArg && !outputArg.startsWith('-')) {
|
|
355
|
+
options.output = outputArg;
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
files.push(arg);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { files, options };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async findYamlFiles(
|
|
371
|
+
patterns: string[],
|
|
372
|
+
options: Record<string, unknown>,
|
|
373
|
+
): Promise<string[]> {
|
|
374
|
+
const files: Set<string> = new Set();
|
|
375
|
+
|
|
376
|
+
let searchPatterns: string[] = [];
|
|
377
|
+
|
|
378
|
+
if (patterns.length === 0) {
|
|
379
|
+
searchPatterns = options.all ? ['**/*.yaml', '**/*.yml'] : ['*.yaml', '*.yml'];
|
|
380
|
+
} else {
|
|
381
|
+
// Check if patterns include directories
|
|
382
|
+
for (const pattern of patterns) {
|
|
383
|
+
try {
|
|
384
|
+
// Use Bun's file system API to check if it's a directory
|
|
385
|
+
const fs = await import('node:fs/promises');
|
|
386
|
+
const stat = await fs.stat(pattern);
|
|
387
|
+
|
|
388
|
+
if (stat.isDirectory()) {
|
|
389
|
+
// Add glob patterns for all YAML files in this directory
|
|
390
|
+
searchPatterns.push(`${pattern}/*.yaml`, `${pattern}/*.yml`);
|
|
391
|
+
// If --all flag is set, search recursively
|
|
392
|
+
if (options.all) {
|
|
393
|
+
searchPatterns.push(`${pattern}/**/*.yaml`, `${pattern}/**/*.yml`);
|
|
394
|
+
}
|
|
395
|
+
} else if (stat.isFile()) {
|
|
396
|
+
// It's a file, add it directly
|
|
397
|
+
searchPatterns.push(pattern);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// If stat fails, assume it's a glob pattern
|
|
401
|
+
searchPatterns.push(pattern);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const pattern of searchPatterns) {
|
|
407
|
+
const globber = new Glob(pattern);
|
|
408
|
+
for await (const file of globber.scan('.')) {
|
|
409
|
+
// Only add files with .yaml or .yml extension
|
|
410
|
+
if (file.endsWith('.yaml') || file.endsWith('.yml')) {
|
|
411
|
+
files.add(file);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return Array.from(files).sort();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async processYamlFile(
|
|
420
|
+
filepath: string,
|
|
421
|
+
): Promise<{ requests: RequestConfig[]; config?: GlobalConfig }> {
|
|
422
|
+
const yamlContent = await YamlParser.parseFile(filepath);
|
|
423
|
+
const requests: RequestConfig[] = [];
|
|
424
|
+
let globalConfig: GlobalConfig | undefined;
|
|
425
|
+
|
|
426
|
+
if (yamlContent.global) {
|
|
427
|
+
globalConfig = yamlContent.global;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const variables = {
|
|
431
|
+
...yamlContent.global?.variables,
|
|
432
|
+
...yamlContent.collection?.variables,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const defaults = {
|
|
436
|
+
...yamlContent.global?.defaults,
|
|
437
|
+
...yamlContent.collection?.defaults,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (yamlContent.request) {
|
|
441
|
+
const request = this.prepareRequest(yamlContent.request, variables, defaults);
|
|
442
|
+
requests.push(request);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (yamlContent.requests) {
|
|
446
|
+
for (const req of yamlContent.requests) {
|
|
447
|
+
const request = this.prepareRequest(req, variables, defaults);
|
|
448
|
+
requests.push(request);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (yamlContent.collection?.requests) {
|
|
453
|
+
for (const req of yamlContent.collection.requests) {
|
|
454
|
+
const request = this.prepareRequest(req, variables, defaults);
|
|
455
|
+
requests.push(request);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return { requests, config: globalConfig };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private prepareRequest(
|
|
463
|
+
request: RequestConfig,
|
|
464
|
+
variables: Record<string, string>,
|
|
465
|
+
defaults: Partial<RequestConfig>,
|
|
466
|
+
): RequestConfig {
|
|
467
|
+
const interpolated = YamlParser.interpolateVariables(request, variables);
|
|
468
|
+
return YamlParser.mergeConfigs(defaults, interpolated);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private mergeGlobalConfigs(base: GlobalConfig, override: GlobalConfig): GlobalConfig {
|
|
472
|
+
return {
|
|
473
|
+
...base,
|
|
474
|
+
...override,
|
|
475
|
+
variables: { ...base.variables, ...override.variables },
|
|
476
|
+
output: { ...base.output, ...override.output },
|
|
477
|
+
defaults: { ...base.defaults, ...override.defaults },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private showHelp(): void {
|
|
482
|
+
console.log(`
|
|
483
|
+
${this.logger.color('🚀 CURL RUNNER', 'bright')}
|
|
484
|
+
|
|
485
|
+
${this.logger.color('USAGE:', 'yellow')}
|
|
486
|
+
curl-runner [files...] [options]
|
|
487
|
+
|
|
488
|
+
${this.logger.color('OPTIONS:', 'yellow')}
|
|
489
|
+
-h, --help Show this help message
|
|
490
|
+
-v, --verbose Enable verbose output
|
|
491
|
+
-q, --quiet Suppress non-error output
|
|
492
|
+
-p, --execution parallel Execute requests in parallel
|
|
493
|
+
-c, --continue-on-error Continue execution on errors
|
|
494
|
+
-o, --output <file> Save results to file
|
|
495
|
+
--all Find all YAML files recursively
|
|
496
|
+
--timeout <ms> Set request timeout in milliseconds
|
|
497
|
+
--retries <count> Set maximum retry attempts
|
|
498
|
+
--retry-delay <ms> Set delay between retries in milliseconds
|
|
499
|
+
--no-retry Disable retry mechanism
|
|
500
|
+
--output-format <format> Set output format (json|pretty|raw)
|
|
501
|
+
--pretty-level <level> Set pretty format level (minimal|standard|detailed)
|
|
502
|
+
--show-headers Include response headers in output
|
|
503
|
+
--show-body Include response body in output
|
|
504
|
+
--show-metrics Include performance metrics in output
|
|
505
|
+
--version Show version
|
|
506
|
+
|
|
507
|
+
${this.logger.color('EXAMPLES:', 'yellow')}
|
|
508
|
+
# Run all YAML files in current directory
|
|
509
|
+
curl-runner
|
|
510
|
+
|
|
511
|
+
# Run specific file
|
|
512
|
+
curl-runner api-tests.yaml
|
|
513
|
+
|
|
514
|
+
# Run all files in a directory
|
|
515
|
+
curl-runner examples/
|
|
516
|
+
|
|
517
|
+
# Run all files in multiple directories
|
|
518
|
+
curl-runner tests/ examples/
|
|
519
|
+
|
|
520
|
+
# Run all files recursively in parallel
|
|
521
|
+
curl-runner --all -p
|
|
522
|
+
|
|
523
|
+
# Run directory recursively
|
|
524
|
+
curl-runner --all examples/
|
|
525
|
+
|
|
526
|
+
# Run with verbose output and continue on errors
|
|
527
|
+
curl-runner tests/*.yaml -vc
|
|
528
|
+
|
|
529
|
+
# Run with minimal pretty output (only status and errors)
|
|
530
|
+
curl-runner --output-format pretty --pretty-level minimal test.yaml
|
|
531
|
+
|
|
532
|
+
# Run with detailed pretty output (show all information)
|
|
533
|
+
curl-runner --output-format pretty --pretty-level detailed test.yaml
|
|
534
|
+
|
|
535
|
+
${this.logger.color('YAML STRUCTURE:', 'yellow')}
|
|
536
|
+
Single request:
|
|
537
|
+
request:
|
|
538
|
+
url: https://api.example.com
|
|
539
|
+
method: GET
|
|
540
|
+
|
|
541
|
+
Multiple requests:
|
|
542
|
+
requests:
|
|
543
|
+
- url: https://api.example.com/users
|
|
544
|
+
method: GET
|
|
545
|
+
- url: https://api.example.com/posts
|
|
546
|
+
method: POST
|
|
547
|
+
body: { title: "Test" }
|
|
548
|
+
|
|
549
|
+
With global config:
|
|
550
|
+
global:
|
|
551
|
+
execution: parallel
|
|
552
|
+
variables:
|
|
553
|
+
BASE_URL: https://api.example.com
|
|
554
|
+
requests:
|
|
555
|
+
- url: \${BASE_URL}/users
|
|
556
|
+
method: GET
|
|
557
|
+
`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const cli = new CurlRunnerCLI();
|
|
562
|
+
cli.run(process.argv.slice(2));
|