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