@curl-runner/cli 1.2.0 → 1.5.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curl-runner/cli",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "A powerful CLI tool for HTTP request management using YAML configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -2,6 +2,8 @@ import { YamlParser } from '../parser/yaml';
2
2
  import type {
3
3
  ExecutionResult,
4
4
  ExecutionSummary,
5
+ FileAttachment,
6
+ FormFieldValue,
5
7
  GlobalConfig,
6
8
  JsonValue,
7
9
  RequestConfig,
@@ -28,6 +30,42 @@ export class RequestExecutor {
28
30
  };
29
31
  }
30
32
 
33
+ /**
34
+ * Checks if a form field value is a file attachment.
35
+ */
36
+ private isFileAttachment(value: FormFieldValue): value is FileAttachment {
37
+ return typeof value === 'object' && value !== null && 'file' in value;
38
+ }
39
+
40
+ /**
41
+ * Validates that all file attachments in formData exist.
42
+ * Returns an error message if any file is missing, or undefined if all files exist.
43
+ */
44
+ private async validateFileAttachments(config: RequestConfig): Promise<string | undefined> {
45
+ if (!config.formData) {
46
+ return undefined;
47
+ }
48
+
49
+ const missingFiles: string[] = [];
50
+
51
+ for (const [fieldName, fieldValue] of Object.entries(config.formData)) {
52
+ if (this.isFileAttachment(fieldValue)) {
53
+ const filePath = fieldValue.file;
54
+ const file = Bun.file(filePath);
55
+ const exists = await file.exists();
56
+ if (!exists) {
57
+ missingFiles.push(`${fieldName}: ${filePath}`);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (missingFiles.length > 0) {
63
+ return `File(s) not found: ${missingFiles.join(', ')}`;
64
+ }
65
+
66
+ return undefined;
67
+ }
68
+
31
69
  async executeRequest(config: RequestConfig, index: number = 0): Promise<ExecutionResult> {
32
70
  const startTime = performance.now();
33
71
 
@@ -37,6 +75,21 @@ export class RequestExecutor {
37
75
 
38
76
  requestLogger.logRequestStart(config, index);
39
77
 
78
+ // Validate file attachments exist before executing
79
+ const fileError = await this.validateFileAttachments(config);
80
+ if (fileError) {
81
+ const failedResult: ExecutionResult = {
82
+ request: config,
83
+ success: false,
84
+ error: fileError,
85
+ metrics: {
86
+ duration: performance.now() - startTime,
87
+ },
88
+ };
89
+ requestLogger.logRequestComplete(failedResult);
90
+ return failedResult;
91
+ }
92
+
40
93
  const command = CurlBuilder.buildCommand(config);
41
94
  requestLogger.logCommand(command);
42
95
 
@@ -174,3 +174,226 @@ describe('YamlParser.resolveVariable', () => {
174
174
  expect(result).toBe('store-value');
175
175
  });
176
176
  });
177
+
178
+ describe('YamlParser.resolveVariable with default values', () => {
179
+ test('should use default value when variable is not set', () => {
180
+ const result = YamlParser.resolveVariable('API_TIMEOUT:5000', {}, {});
181
+ expect(result).toBe('5000');
182
+ });
183
+
184
+ test('should use variable value when set, ignoring default', () => {
185
+ const variables = { API_TIMEOUT: '10000' };
186
+ const result = YamlParser.resolveVariable('API_TIMEOUT:5000', variables, {});
187
+ expect(result).toBe('10000');
188
+ });
189
+
190
+ test('should handle nested default with first variable set', () => {
191
+ const variables = { DATABASE_HOST: 'prod-db.example.com' };
192
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', variables, {});
193
+ expect(result).toBe('prod-db.example.com');
194
+ });
195
+
196
+ test('should handle nested default with second variable set', () => {
197
+ const variables = { DB_HOST: 'staging-db.example.com' };
198
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', variables, {});
199
+ expect(result).toBe('staging-db.example.com');
200
+ });
201
+
202
+ test('should use final fallback when no variables are set', () => {
203
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', {}, {});
204
+ expect(result).toBe('localhost');
205
+ });
206
+
207
+ test('should not confuse DATE: with default syntax', () => {
208
+ const result = YamlParser.resolveVariable('DATE:YYYY-MM-DD', {}, {});
209
+ // Should return a formatted date, not treat 'YYYY-MM-DD' as a default
210
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
211
+ });
212
+
213
+ test('should not confuse TIME: with default syntax', () => {
214
+ const result = YamlParser.resolveVariable('TIME:HH:mm:ss', {}, {});
215
+ // Should return a formatted time, not treat 'HH:mm:ss' as a default
216
+ expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
217
+ });
218
+
219
+ test('should handle default value with special characters', () => {
220
+ const result = YamlParser.resolveVariable('API_URL:https://api.example.com/v1', {}, {});
221
+ expect(result).toBe('https://api.example.com/v1');
222
+ });
223
+
224
+ test('should handle empty default value', () => {
225
+ const result = YamlParser.resolveVariable('OPTIONAL:', {}, {});
226
+ expect(result).toBe('');
227
+ });
228
+
229
+ test('should use store variable when available before default', () => {
230
+ const storeContext = { token: 'stored-token' };
231
+ // Note: store variables have a different syntax (store.X), but we test
232
+ // that the default mechanism doesn't interfere
233
+ const result = YamlParser.resolveVariable('AUTH_TOKEN:default-token', {}, storeContext);
234
+ expect(result).toBe('default-token');
235
+ });
236
+ });
237
+
238
+ describe('YamlParser.interpolateVariables with default values', () => {
239
+ test('should interpolate variable with default in string', () => {
240
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
241
+ const obj = { timeout: '${API_TIMEOUT:5000}' };
242
+ const result = YamlParser.interpolateVariables(obj, {});
243
+ expect(result).toEqual({ timeout: '5000' });
244
+ });
245
+
246
+ test('should interpolate variable with value set over default', () => {
247
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
248
+ const obj = { timeout: '${API_TIMEOUT:5000}' };
249
+ const variables = { API_TIMEOUT: '10000' };
250
+ const result = YamlParser.interpolateVariables(obj, variables);
251
+ expect(result).toEqual({ timeout: '10000' });
252
+ });
253
+
254
+ test('should handle multiple variables with defaults in one string', () => {
255
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
256
+ const obj = { url: '${BASE_URL:https://api.example.com}/${VERSION:v1}/users' };
257
+ const result = YamlParser.interpolateVariables(obj, {});
258
+ expect(result).toEqual({ url: 'https://api.example.com/v1/users' });
259
+ });
260
+
261
+ test('should mix defaults with regular variables', () => {
262
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
263
+ const obj = { url: '${BASE_URL}/api/${VERSION:v1}/users' };
264
+ const variables = { BASE_URL: 'https://prod.example.com' };
265
+ const result = YamlParser.interpolateVariables(obj, variables);
266
+ expect(result).toEqual({ url: 'https://prod.example.com/api/v1/users' });
267
+ });
268
+
269
+ test('should handle nested defaults in interpolation', () => {
270
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
271
+ const obj = { host: '${DATABASE_HOST:${DB_HOST:localhost}}' };
272
+ const result = YamlParser.interpolateVariables(obj, {});
273
+ expect(result).toEqual({ host: 'localhost' });
274
+ });
275
+
276
+ test('should preserve DATE: formatting syntax', () => {
277
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
278
+ const obj = { date: '${DATE:YYYY-MM-DD}' };
279
+ const result = YamlParser.interpolateVariables(obj, {}) as { date: string };
280
+ expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
281
+ });
282
+
283
+ test('should preserve TIME: formatting syntax', () => {
284
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
285
+ const obj = { time: '${TIME:HH:mm:ss}' };
286
+ const result = YamlParser.interpolateVariables(obj, {}) as { time: string };
287
+ expect(result.time).toMatch(/^\d{2}:\d{2}:\d{2}$/);
288
+ });
289
+ });
290
+
291
+ describe('YamlParser.resolveDynamicVariable', () => {
292
+ test('should resolve UUID to a valid full UUID', () => {
293
+ const result = YamlParser.resolveDynamicVariable('UUID');
294
+ expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
295
+ });
296
+
297
+ test('should resolve UUID:short to first 8 characters of a UUID', () => {
298
+ const result = YamlParser.resolveDynamicVariable('UUID:short');
299
+ expect(result).toMatch(/^[0-9a-f]{8}$/i);
300
+ expect(result).toHaveLength(8);
301
+ });
302
+
303
+ test('should resolve RANDOM:min-max to a number within range', () => {
304
+ const result = YamlParser.resolveDynamicVariable('RANDOM:1-100');
305
+ expect(result).not.toBeNull();
306
+ const num = Number(result);
307
+ expect(num).toBeGreaterThanOrEqual(1);
308
+ expect(num).toBeLessThanOrEqual(100);
309
+ });
310
+
311
+ test('should resolve RANDOM:min-max with large range', () => {
312
+ const result = YamlParser.resolveDynamicVariable('RANDOM:1-1000');
313
+ expect(result).not.toBeNull();
314
+ const num = Number(result);
315
+ expect(num).toBeGreaterThanOrEqual(1);
316
+ expect(num).toBeLessThanOrEqual(1000);
317
+ });
318
+
319
+ test('should resolve RANDOM:min-max with same min and max', () => {
320
+ const result = YamlParser.resolveDynamicVariable('RANDOM:5-5');
321
+ expect(result).toBe('5');
322
+ });
323
+
324
+ test('should resolve RANDOM:string:length to alphanumeric string of correct length', () => {
325
+ const result = YamlParser.resolveDynamicVariable('RANDOM:string:10');
326
+ expect(result).not.toBeNull();
327
+ expect(result).toHaveLength(10);
328
+ expect(result).toMatch(/^[A-Za-z0-9]+$/);
329
+ });
330
+
331
+ test('should resolve RANDOM:string:length with different lengths', () => {
332
+ const result5 = YamlParser.resolveDynamicVariable('RANDOM:string:5');
333
+ const result20 = YamlParser.resolveDynamicVariable('RANDOM:string:20');
334
+ expect(result5).toHaveLength(5);
335
+ expect(result20).toHaveLength(20);
336
+ expect(result5).toMatch(/^[A-Za-z0-9]+$/);
337
+ expect(result20).toMatch(/^[A-Za-z0-9]+$/);
338
+ });
339
+
340
+ test('should resolve TIMESTAMP to a numeric string', () => {
341
+ const result = YamlParser.resolveDynamicVariable('TIMESTAMP');
342
+ expect(result).toMatch(/^\d+$/);
343
+ });
344
+
345
+ test('should resolve CURRENT_TIME to a numeric string', () => {
346
+ const result = YamlParser.resolveDynamicVariable('CURRENT_TIME');
347
+ expect(result).toMatch(/^\d+$/);
348
+ });
349
+
350
+ test('should return null for unknown dynamic variable', () => {
351
+ const result = YamlParser.resolveDynamicVariable('UNKNOWN');
352
+ expect(result).toBeNull();
353
+ });
354
+ });
355
+
356
+ describe('YamlParser.interpolateVariables with new dynamic variables', () => {
357
+ test('should interpolate UUID:short in objects', () => {
358
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${UUID:short} interpolation
359
+ const obj = { id: '${UUID:short}' };
360
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
361
+ expect(result.id).toMatch(/^[0-9a-f]{8}$/i);
362
+ });
363
+
364
+ test('should interpolate RANDOM:min-max in objects', () => {
365
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${RANDOM:x-y} interpolation
366
+ const obj = { value: '${RANDOM:1-100}' };
367
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
368
+ const num = Number(result.value);
369
+ expect(num).toBeGreaterThanOrEqual(1);
370
+ expect(num).toBeLessThanOrEqual(100);
371
+ });
372
+
373
+ test('should interpolate RANDOM:string:length in objects', () => {
374
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${RANDOM:string:n} interpolation
375
+ const obj = { token: '${RANDOM:string:16}' };
376
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
377
+ expect(result.token).toHaveLength(16);
378
+ expect(result.token).toMatch(/^[A-Za-z0-9]+$/);
379
+ });
380
+
381
+ test('should interpolate multiple new dynamic variables in one object', () => {
382
+ const obj = {
383
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
384
+ sessionId: '${UUID:short}',
385
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
386
+ randomNum: '${RANDOM:1-1000}',
387
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
388
+ randomStr: '${RANDOM:string:10}',
389
+ };
390
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
391
+
392
+ expect(result.sessionId).toMatch(/^[0-9a-f]{8}$/i);
393
+ const num = Number(result.randomNum);
394
+ expect(num).toBeGreaterThanOrEqual(1);
395
+ expect(num).toBeLessThanOrEqual(1000);
396
+ expect(result.randomStr).toHaveLength(10);
397
+ expect(result.randomStr).toMatch(/^[A-Za-z0-9]+$/);
398
+ });
399
+ });
@@ -18,6 +18,8 @@ export class YamlParser {
18
18
  * - Static variables: ${VAR_NAME}
19
19
  * - Dynamic variables: ${UUID}, ${TIMESTAMP}, ${DATE:format}, ${TIME:format}
20
20
  * - Stored response values: ${store.variableName}
21
+ * - Default values: ${VAR_NAME:default} - uses 'default' if VAR_NAME is not found
22
+ * - Nested defaults: ${VAR1:${VAR2:fallback}} - tries VAR1, then VAR2, then 'fallback'
21
23
  *
22
24
  * @param obj - The object to interpolate
23
25
  * @param variables - Static variables map
@@ -29,19 +31,31 @@ export class YamlParser {
29
31
  storeContext?: ResponseStoreContext,
30
32
  ): unknown {
31
33
  if (typeof obj === 'string') {
32
- // Check if it's a single variable like ${VAR} (no other characters)
33
- const singleVarMatch = obj.match(/^\$\{([^}]+)\}$/);
34
- if (singleVarMatch) {
35
- const varName = singleVarMatch[1];
34
+ // Extract all variable references with proper brace matching
35
+ const extractedVars = YamlParser.extractVariables(obj);
36
+
37
+ if (extractedVars.length === 0) {
38
+ return obj;
39
+ }
40
+
41
+ // Check if it's a single variable that spans the entire string
42
+ if (extractedVars.length === 1 && extractedVars[0].start === 0 && extractedVars[0].end === obj.length) {
43
+ const varName = extractedVars[0].name;
36
44
  const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
37
45
  return resolvedValue !== null ? resolvedValue : obj;
38
46
  }
39
47
 
40
- // Handle multiple variables in the string using regex replacement
41
- return obj.replace(/\$\{([^}]+)\}/g, (match, varName) => {
42
- const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
43
- return resolvedValue !== null ? resolvedValue : match;
44
- });
48
+ // Handle multiple variables in the string
49
+ let result = '';
50
+ let lastEnd = 0;
51
+ for (const varRef of extractedVars) {
52
+ result += obj.slice(lastEnd, varRef.start);
53
+ const resolvedValue = YamlParser.resolveVariable(varRef.name, variables, storeContext);
54
+ result += resolvedValue !== null ? resolvedValue : obj.slice(varRef.start, varRef.end);
55
+ lastEnd = varRef.end;
56
+ }
57
+ result += obj.slice(lastEnd);
58
+ return result;
45
59
  }
46
60
 
47
61
  if (Array.isArray(obj)) {
@@ -61,7 +75,7 @@ export class YamlParser {
61
75
 
62
76
  /**
63
77
  * Resolves a single variable reference.
64
- * Priority: store context > dynamic variables > static variables
78
+ * Priority: store context > dynamic variables > static variables > default values
65
79
  */
66
80
  static resolveVariable(
67
81
  varName: string,
@@ -77,6 +91,39 @@ export class YamlParser {
77
91
  return null; // Store variable not found, return null to keep original
78
92
  }
79
93
 
94
+ // Check for default value syntax: ${VAR:default}
95
+ // Must check before dynamic variables to properly handle defaults
96
+ const colonIndex = varName.indexOf(':');
97
+ if (colonIndex !== -1) {
98
+ const actualVarName = varName.slice(0, colonIndex);
99
+ const defaultValue = varName.slice(colonIndex + 1);
100
+
101
+ // Don't confuse with DATE:, TIME:, UUID:, RANDOM: patterns
102
+ // These are reserved prefixes for dynamic variable generation
103
+ const reservedPrefixes = ['DATE', 'TIME', 'UUID', 'RANDOM'];
104
+ if (!reservedPrefixes.includes(actualVarName)) {
105
+ // Try to resolve the actual variable name
106
+ const resolved = YamlParser.resolveVariable(actualVarName, variables, storeContext);
107
+ if (resolved !== null) {
108
+ return resolved;
109
+ }
110
+ // Variable not found, use the default value
111
+ // The default value might itself be a variable reference like ${OTHER_VAR:fallback}
112
+ // Note: Due to the regex in interpolateVariables using [^}]+, nested braces
113
+ // get truncated (e.g., "${VAR:${OTHER:default}}" captures "VAR:${OTHER:default")
114
+ // So we check for both complete ${...} and truncated ${... patterns
115
+ if (defaultValue.startsWith('${')) {
116
+ // Handle both complete ${VAR} and truncated ${VAR (from nested braces)
117
+ const nestedVarName = defaultValue.endsWith('}')
118
+ ? defaultValue.slice(2, -1)
119
+ : defaultValue.slice(2);
120
+ const nestedResolved = YamlParser.resolveVariable(nestedVarName, variables, storeContext);
121
+ return nestedResolved !== null ? nestedResolved : defaultValue;
122
+ }
123
+ return defaultValue;
124
+ }
125
+ }
126
+
80
127
  // Check for dynamic variable
81
128
  const dynamicValue = YamlParser.resolveDynamicVariable(varName);
82
129
  if (dynamicValue !== null) {
@@ -97,6 +144,29 @@ export class YamlParser {
97
144
  return crypto.randomUUID();
98
145
  }
99
146
 
147
+ // UUID:short - first segment (8 chars) of a UUID
148
+ if (varName === 'UUID:short') {
149
+ return crypto.randomUUID().split('-')[0];
150
+ }
151
+
152
+ // RANDOM:min-max - random number in range
153
+ const randomRangeMatch = varName.match(/^RANDOM:(\d+)-(\d+)$/);
154
+ if (randomRangeMatch) {
155
+ const min = Number(randomRangeMatch[1]);
156
+ const max = Number(randomRangeMatch[2]);
157
+ return String(Math.floor(Math.random() * (max - min + 1)) + min);
158
+ }
159
+
160
+ // RANDOM:string:length - random alphanumeric string
161
+ const randomStringMatch = varName.match(/^RANDOM:string:(\d+)$/);
162
+ if (randomStringMatch) {
163
+ const length = Number(randomStringMatch[1]);
164
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
165
+ return Array.from({ length }, () =>
166
+ chars.charAt(Math.floor(Math.random() * chars.length)),
167
+ ).join('');
168
+ }
169
+
100
170
  // Current timestamp variations
101
171
  if (varName === 'CURRENT_TIME' || varName === 'TIMESTAMP') {
102
172
  return Date.now().toString();
@@ -133,6 +203,46 @@ export class YamlParser {
133
203
  return format.replace('HH', hours).replace('mm', minutes).replace('ss', seconds);
134
204
  }
135
205
 
206
+ /**
207
+ * Extracts variable references from a string, properly handling nested braces.
208
+ * For example, "${VAR:${OTHER:default}}" is extracted as a single variable reference.
209
+ */
210
+ static extractVariables(str: string): Array<{ start: number; end: number; name: string }> {
211
+ const variables: Array<{ start: number; end: number; name: string }> = [];
212
+ let i = 0;
213
+
214
+ while (i < str.length) {
215
+ // Look for ${
216
+ if (str[i] === '$' && str[i + 1] === '{') {
217
+ const start = i;
218
+ i += 2; // Skip past ${
219
+ let braceCount = 1;
220
+ const nameStart = i;
221
+
222
+ // Find the matching closing brace
223
+ while (i < str.length && braceCount > 0) {
224
+ if (str[i] === '{') {
225
+ braceCount++;
226
+ } else if (str[i] === '}') {
227
+ braceCount--;
228
+ }
229
+ i++;
230
+ }
231
+
232
+ if (braceCount === 0) {
233
+ // Found matching closing brace
234
+ const name = str.slice(nameStart, i - 1); // Exclude the closing }
235
+ variables.push({ start, end: i, name });
236
+ }
237
+ // If braceCount > 0, we have unmatched braces - skip this variable
238
+ } else {
239
+ i++;
240
+ }
241
+ }
242
+
243
+ return variables;
244
+ }
245
+
136
246
  static mergeConfigs(base: Partial<RequestConfig>, override: RequestConfig): RequestConfig {
137
247
  return {
138
248
  ...base,
@@ -4,6 +4,47 @@ export interface JsonObject {
4
4
  }
5
5
  export interface JsonArray extends Array<JsonValue> {}
6
6
 
7
+ /**
8
+ * Configuration for a file attachment in a form data request.
9
+ *
10
+ * Examples:
11
+ * - `{ file: "./image.png" }` - Simple file attachment
12
+ * - `{ file: "./doc.pdf", filename: "document.pdf" }` - With custom filename
13
+ * - `{ file: "./data.json", contentType: "application/json" }` - With explicit content type
14
+ */
15
+ export interface FileAttachment {
16
+ /** Path to the file (relative to YAML file or absolute) */
17
+ file: string;
18
+ /** Custom filename to send (defaults to actual filename) */
19
+ filename?: string;
20
+ /** Explicit content type (curl will auto-detect if not specified) */
21
+ contentType?: string;
22
+ }
23
+
24
+ /**
25
+ * A form field value can be a string, number, boolean, or a file attachment.
26
+ */
27
+ export type FormFieldValue = string | number | boolean | FileAttachment;
28
+
29
+ /**
30
+ * Configuration for form data (multipart/form-data) requests.
31
+ * Each key is a form field name, and the value can be a simple value or a file attachment.
32
+ *
33
+ * Examples:
34
+ * ```yaml
35
+ * formData:
36
+ * name: "John Doe"
37
+ * age: 30
38
+ * avatar:
39
+ * file: "./avatar.png"
40
+ * document:
41
+ * file: "./report.pdf"
42
+ * filename: "quarterly-report.pdf"
43
+ * contentType: "application/pdf"
44
+ * ```
45
+ */
46
+ export type FormDataConfig = Record<string, FormFieldValue>;
47
+
7
48
  /**
8
49
  * Configuration for storing response values as variables for subsequent requests.
9
50
  * Maps a variable name to a JSON path in the response.
@@ -24,6 +65,18 @@ export interface RequestConfig {
24
65
  params?: Record<string, string>;
25
66
  sourceFile?: string; // Source YAML file for better output organization
26
67
  body?: JsonValue;
68
+ /**
69
+ * Form data for multipart/form-data requests.
70
+ * Use this for file uploads or when you need to send form fields.
71
+ * Cannot be used together with 'body'.
72
+ *
73
+ * @example
74
+ * formData:
75
+ * username: "john"
76
+ * avatar:
77
+ * file: "./avatar.png"
78
+ */
79
+ formData?: FormDataConfig;
27
80
  timeout?: number;
28
81
  followRedirects?: boolean;
29
82
  maxRedirects?: number;
@@ -0,0 +1,165 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { CurlBuilder } from './curl-builder';
3
+
4
+ describe('CurlBuilder', () => {
5
+ describe('buildCommand', () => {
6
+ test('should build basic GET request', () => {
7
+ const command = CurlBuilder.buildCommand({
8
+ url: 'https://example.com/api',
9
+ method: 'GET',
10
+ });
11
+
12
+ expect(command).toContain('curl');
13
+ expect(command).toContain('-X GET');
14
+ expect(command).toContain('"https://example.com/api"');
15
+ });
16
+
17
+ test('should build POST request with JSON body', () => {
18
+ const command = CurlBuilder.buildCommand({
19
+ url: 'https://example.com/api',
20
+ method: 'POST',
21
+ body: { name: 'test' },
22
+ });
23
+
24
+ expect(command).toContain('-X POST');
25
+ expect(command).toContain('-d \'{"name":"test"}\'');
26
+ expect(command).toContain('Content-Type: application/json');
27
+ });
28
+
29
+ test('should build POST request with form data', () => {
30
+ const command = CurlBuilder.buildCommand({
31
+ url: 'https://example.com/upload',
32
+ method: 'POST',
33
+ formData: {
34
+ username: 'john',
35
+ age: 30,
36
+ },
37
+ });
38
+
39
+ expect(command).toContain('-X POST');
40
+ expect(command).toContain("-F 'username=john'");
41
+ expect(command).toContain("-F 'age=30'");
42
+ expect(command).not.toContain('-d');
43
+ });
44
+
45
+ test('should build POST request with file attachment', () => {
46
+ const command = CurlBuilder.buildCommand({
47
+ url: 'https://example.com/upload',
48
+ method: 'POST',
49
+ formData: {
50
+ document: {
51
+ file: './test.pdf',
52
+ },
53
+ },
54
+ });
55
+
56
+ expect(command).toContain("-F 'document=@./test.pdf'");
57
+ });
58
+
59
+ test('should build POST request with file attachment and custom filename', () => {
60
+ const command = CurlBuilder.buildCommand({
61
+ url: 'https://example.com/upload',
62
+ method: 'POST',
63
+ formData: {
64
+ document: {
65
+ file: './test.pdf',
66
+ filename: 'report.pdf',
67
+ },
68
+ },
69
+ });
70
+
71
+ expect(command).toContain("-F 'document=@./test.pdf;filename=report.pdf'");
72
+ });
73
+
74
+ test('should build POST request with file attachment and content type', () => {
75
+ const command = CurlBuilder.buildCommand({
76
+ url: 'https://example.com/upload',
77
+ method: 'POST',
78
+ formData: {
79
+ data: {
80
+ file: './data.json',
81
+ contentType: 'application/json',
82
+ },
83
+ },
84
+ });
85
+
86
+ expect(command).toContain("-F 'data=@./data.json;type=application/json'");
87
+ });
88
+
89
+ test('should build POST request with file attachment including all options', () => {
90
+ const command = CurlBuilder.buildCommand({
91
+ url: 'https://example.com/upload',
92
+ method: 'POST',
93
+ formData: {
94
+ document: {
95
+ file: './report.pdf',
96
+ filename: 'quarterly-report.pdf',
97
+ contentType: 'application/pdf',
98
+ },
99
+ },
100
+ });
101
+
102
+ expect(command).toContain(
103
+ "-F 'document=@./report.pdf;filename=quarterly-report.pdf;type=application/pdf'",
104
+ );
105
+ });
106
+
107
+ test('should build POST request with mixed form data and files', () => {
108
+ const command = CurlBuilder.buildCommand({
109
+ url: 'https://example.com/upload',
110
+ method: 'POST',
111
+ formData: {
112
+ title: 'My Document',
113
+ description: 'Test upload',
114
+ file: {
115
+ file: './document.pdf',
116
+ },
117
+ },
118
+ });
119
+
120
+ expect(command).toContain("-F 'title=My Document'");
121
+ expect(command).toContain("-F 'description=Test upload'");
122
+ expect(command).toContain("-F 'file=@./document.pdf'");
123
+ });
124
+
125
+ test('should escape single quotes in form field values', () => {
126
+ const command = CurlBuilder.buildCommand({
127
+ url: 'https://example.com/upload',
128
+ method: 'POST',
129
+ formData: {
130
+ message: "It's a test",
131
+ },
132
+ });
133
+
134
+ expect(command).toContain("-F 'message=It'\\''s a test'");
135
+ });
136
+
137
+ test('should prefer formData over body when both are present', () => {
138
+ const command = CurlBuilder.buildCommand({
139
+ url: 'https://example.com/api',
140
+ method: 'POST',
141
+ formData: {
142
+ field: 'value',
143
+ },
144
+ body: { name: 'test' },
145
+ });
146
+
147
+ expect(command).toContain("-F 'field=value'");
148
+ expect(command).not.toContain('-d');
149
+ });
150
+
151
+ test('should handle boolean form field values', () => {
152
+ const command = CurlBuilder.buildCommand({
153
+ url: 'https://example.com/api',
154
+ method: 'POST',
155
+ formData: {
156
+ active: true,
157
+ disabled: false,
158
+ },
159
+ });
160
+
161
+ expect(command).toContain("-F 'active=true'");
162
+ expect(command).toContain("-F 'disabled=false'");
163
+ });
164
+ });
165
+ });
@@ -1,4 +1,4 @@
1
- import type { RequestConfig } from '../types/config';
1
+ import type { FileAttachment, FormFieldValue, RequestConfig } from '../types/config';
2
2
 
3
3
  interface CurlMetrics {
4
4
  response_code?: number;
@@ -11,6 +11,20 @@ interface CurlMetrics {
11
11
  time_starttransfer?: number;
12
12
  }
13
13
 
14
+ /**
15
+ * Checks if a form field value is a file attachment.
16
+ */
17
+ function isFileAttachment(value: FormFieldValue): value is FileAttachment {
18
+ return typeof value === 'object' && value !== null && 'file' in value;
19
+ }
20
+
21
+ /**
22
+ * Escapes a string value for use in curl -F flag.
23
+ */
24
+ function escapeFormValue(value: string): string {
25
+ return value.replace(/'/g, "'\\''");
26
+ }
27
+
14
28
  // Using class for organization, but could be refactored to functions
15
29
  export class CurlBuilder {
16
30
  static buildCommand(config: RequestConfig): string {
@@ -34,7 +48,26 @@ export class CurlBuilder {
34
48
  }
35
49
  }
36
50
 
37
- if (config.body) {
51
+ if (config.formData) {
52
+ // Use -F flags for multipart/form-data
53
+ for (const [fieldName, fieldValue] of Object.entries(config.formData)) {
54
+ if (isFileAttachment(fieldValue)) {
55
+ // File attachment: -F "field=@filepath;filename=name;type=mimetype"
56
+ let fileSpec = `@${fieldValue.file}`;
57
+ if (fieldValue.filename) {
58
+ fileSpec += `;filename=${fieldValue.filename}`;
59
+ }
60
+ if (fieldValue.contentType) {
61
+ fileSpec += `;type=${fieldValue.contentType}`;
62
+ }
63
+ parts.push('-F', `'${fieldName}=${escapeFormValue(fileSpec)}'`);
64
+ } else {
65
+ // Regular form field: -F "field=value"
66
+ const strValue = String(fieldValue);
67
+ parts.push('-F', `'${fieldName}=${escapeFormValue(strValue)}'`);
68
+ }
69
+ }
70
+ } else if (config.body) {
38
71
  const bodyStr = typeof config.body === 'string' ? config.body : JSON.stringify(config.body);
39
72
  parts.push('-d', `'${bodyStr.replace(/'/g, "'\\''")}'`);
40
73
 
package/dist/cli.js DELETED
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env bun
2
- // @bun
3
- var v=Object.create;var{getPrototypeOf:x,defineProperty:q,getOwnPropertyNames:y}=Object;var f=Object.prototype.hasOwnProperty;var g=($,K,Q)=>{Q=$!=null?v(x($)):{};let Z=K||!$||!$.__esModule?q(Q,"default",{value:$,enumerable:!0}):Q;for(let X of y($))if(!f.call(Z,X))q(Z,X,{get:()=>$[X],enumerable:!0});return Z};var E=import.meta.require;var{Glob:i}=globalThis.Bun;var{YAML:N}=globalThis.Bun;class H{static async parseFile($){let Q=await Bun.file($).text();return N.parse(Q)}static parse($){return N.parse($)}static interpolateVariables($,K,Q){if(typeof $==="string"){let Z=$.match(/^\$\{([^}]+)\}$/);if(Z){let X=Z[1],z=H.resolveVariable(X,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(X,z)=>{let W=H.resolveVariable(z,K,Q);return W!==null?W:X})}if(Array.isArray($))return $.map((Z)=>H.interpolateVariables(Z,K,Q));if($&&typeof $==="object"){let Z={};for(let[X,z]of Object.entries($))Z[X]=H.interpolateVariables(z,K,Q);return Z}return $}static resolveVariable($,K,Q){if($.startsWith("store.")&&Q){let X=$.slice(6);if(X in Q)return Q[X];return null}let Z=H.resolveDynamicVariable($);if(Z!==null)return Z;if($ in K)return K[$];return null}static resolveDynamicVariable($){if($==="UUID")return crypto.randomUUID();if($==="CURRENT_TIME"||$==="TIMESTAMP")return Date.now().toString();if($.startsWith("DATE:")){let K=$.slice(5);return H.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return H.formatTime(new Date,K)}return null}static formatDate($,K){let Q=$.getFullYear(),Z=String($.getMonth()+1).padStart(2,"0"),X=String($.getDate()).padStart(2,"0");return K.replace("YYYY",Q.toString()).replace("MM",Z).replace("DD",X)}static formatTime($,K){let Q=String($.getHours()).padStart(2,"0"),Z=String($.getMinutes()).padStart(2,"0"),X=String($.getSeconds()).padStart(2,"0");return K.replace("HH",Q).replace("mm",Z).replace("ss",X)}static mergeConfigs($,K){return{...$,...K,headers:{...$.headers,...K.headers},params:{...$.params,...K.params},variables:{...$.variables,...K.variables}}}}class S{static buildCommand($){let K=["curl"];if(K.push("-X",$.method||"GET"),K.push("-w",'"\\n__CURL_METRICS_START__%{json}__CURL_METRICS_END__"'),$.headers)for(let[Z,X]of Object.entries($.headers))K.push("-H",`"${Z}: ${X}"`);if($.auth){if($.auth.type==="basic"&&$.auth.username&&$.auth.password)K.push("-u",`"${$.auth.username}:${$.auth.password}"`);else if($.auth.type==="bearer"&&$.auth.token)K.push("-H",`"Authorization: Bearer ${$.auth.token}"`)}if($.body){let Z=typeof $.body==="string"?$.body:JSON.stringify($.body);if(K.push("-d",`'${Z.replace(/'/g,"'\\''")}'`),!$.headers?.["Content-Type"])K.push("-H",'"Content-Type: application/json"')}if($.timeout)K.push("--max-time",$.timeout.toString());if($.followRedirects!==!1){if(K.push("-L"),$.maxRedirects)K.push("--max-redirs",$.maxRedirects.toString())}if($.proxy)K.push("-x",$.proxy);if($.insecure)K.push("-k");if($.output)K.push("-o",$.output);K.push("-s","-S");let Q=$.url;if($.params&&Object.keys($.params).length>0){let Z=new URLSearchParams($.params).toString();Q+=(Q.includes("?")?"&":"?")+Z}return K.push(`"${Q}"`),K.join(" ")}static async executeCurl($){try{let K=Bun.spawn(["sh","-c",$],{stdout:"pipe",stderr:"pipe"}),Q=await new Response(K.stdout).text(),Z=await new Response(K.stderr).text();if(await K.exited,K.exitCode!==0&&!Q)return{success:!1,error:Z||`Command failed with exit code ${K.exitCode}`};let X=Q,z={},W=Q.match(/__CURL_METRICS_START__(.+?)__CURL_METRICS_END__/);if(W){X=Q.replace(/__CURL_METRICS_START__.+?__CURL_METRICS_END__/,"").trim();try{z=JSON.parse(W[1])}catch(U){}}let J={};if(z.response_code){let U=Z.split(`
4
- `).filter((w)=>w.includes(":"));for(let w of U){let[D,..._]=w.split(":");if(D&&_.length>0)J[D.trim()]=_.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:J,body:X,metrics:{duration:(z.time_total||0)*1000,size:z.size_download,dnsLookup:(z.time_namelookup||0)*1000,tcpConnection:(z.time_connect||0)*1000,tlsHandshake:(z.time_appconnect||0)*1000,firstByte:(z.time_starttransfer||0)*1000,download:(z.time_total||0)*1000}}}catch(K){return{success:!1,error:K instanceof Error?K.message:String(K)}}}}class A{colors;constructor($){this.colors=$}color($,K){if(!K||!this.colors[K])return $;return`${this.colors[K]}${$}${this.colors.reset}`}render($,K=" "){$.forEach((Q,Z)=>{let X=Z===$.length-1,z=X?`${K}\u2514\u2500`:`${K}\u251C\u2500`;if(Q.label&&Q.value){let W=Q.color?this.color(Q.value,Q.color):Q.value,J=W.split(`
5
- `);if(J.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let U=X?`${K} `:`${K}\u2502 `;J.forEach((w)=>{console.log(`${U}${w}`)})}}else if(Q.label&&!Q.value)console.log(`${z} ${Q.label}:`);else if(!Q.label&&Q.value){let W=X?`${K} `:`${K}\u2502 `;console.log(`${W}${Q.value}`)}if(Q.children&&Q.children.length>0){let W=X?`${K} `:`${K}\u2502 `;this.render(Q.children,W)}})}}class M{config;colors={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};constructor($={}){this.config={verbose:!1,showHeaders:!1,showBody:!0,showMetrics:!1,format:"pretty",prettyLevel:"minimal",...$}}color($,K){return`${this.colors[K]}${$}${this.colors.reset}`}getShortFilename($){return $.replace(/.*\//,"").replace(".yaml","")}shouldShowOutput(){if(this.config.format==="raw")return!1;if(this.config.format==="pretty")return!0;return this.config.verbose!==!1}shouldShowHeaders(){if(this.config.format!=="pretty")return this.config.showHeaders||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showHeaders||!1;case"detailed":return!0;default:return this.config.showHeaders||!1}}shouldShowBody(){if(this.config.format!=="pretty")return this.config.showBody!==!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showBody!==!1;case"detailed":return!0;default:return this.config.showBody!==!1}}shouldShowMetrics(){if(this.config.format!=="pretty")return this.config.showMetrics||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.showMetrics||!1;case"detailed":return!0;default:return this.config.showMetrics||!1}}shouldShowRequestDetails(){if(this.config.format!=="pretty")return this.config.verbose||!1;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return this.config.verbose||!1;case"detailed":return!0;default:return this.config.verbose||!1}}shouldShowSeparators(){if(this.config.format!=="pretty")return!0;switch(this.config.prettyLevel||"standard"){case"minimal":return!1;case"standard":return!0;case"detailed":return!0;default:return!0}}colorStatusCode($){return this.color($,"yellow")}logValidationErrors($){let K=$.split("; ");if(K.length===1){let Q=K[0].trim(),Z=Q.match(/^Expected status (.+?), got (.+)$/);if(Z){let[,X,z]=Z,W=this.colorStatusCode(X.replace(" or ","|")),J=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${J}`)}else console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} ${Q}`)}else{console.log(` ${this.color("\u2717","red")} ${this.color("Validation Errors:","red")}`);for(let Q of K){let Z=Q.trim();if(Z)if(Z.startsWith("Expected ")){let X=Z.match(/^Expected status (.+?), got (.+)$/);if(X){let[,z,W]=X,J=this.colorStatusCode(z.replace(" or ","|")),U=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${J}, got ${U}`)}else{let z=Z.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,J,U]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(J,"green")}, got ${this.color(U,"red")}`)}else console.log(` ${this.color("\u2022","red")} ${Z}`)}}else console.log(` ${this.color("\u2022","red")} ${Z}`)}}}formatJson($){if(this.config.format==="raw")return typeof $==="string"?$:JSON.stringify($);if(this.config.format==="json")return JSON.stringify($);return JSON.stringify($,null,2)}formatDuration($){if($<1000)return`${$.toFixed(0)}ms`;return`${($/1000).toFixed(2)}s`}formatSize($){if(!$)return"0 B";let K=["B","KB","MB","GB"],Q=Math.floor(Math.log($)/Math.log(1024));return`${($/1024**Q).toFixed(2)} ${K[Q]}`}logExecutionStart($,K){if(!this.shouldShowOutput())return;if(this.shouldShowSeparators())console.log(),console.log(this.color(`Executing ${$} request(s) in ${K} mode`,"dim")),console.log();else console.log()}logRequestStart($,K){return}logCommand($){if(this.shouldShowRequestDetails())console.log(this.color(" Command:","dim")),console.log(this.color(` ${$}`,"dim"))}logRetry($,K){console.log(this.color(` \u21BB Retry ${$}/${K}...`,"yellow"))}logRequestComplete($){if(this.config.format==="raw"){if($.success&&this.config.showBody&&$.body){let J=this.formatJson($.body);console.log(J)}return}if(this.config.format==="json"){let J={request:{name:$.request.name,url:$.request.url,method:$.request.method||"GET"},success:$.success,status:$.status,...this.shouldShowHeaders()&&$.headers?{headers:$.headers}:{},...this.shouldShowBody()&&$.body?{body:$.body}:{},...$.error?{error:$.error}:{},...this.shouldShowMetrics()&&$.metrics?{metrics:$.metrics}:{}};console.log(JSON.stringify(J,null,2));return}if(!this.shouldShowOutput())return;let K=this.config.prettyLevel||"minimal",Q=$.success?"green":"red",Z=$.success?"\u2713":"x",X=$.request.name||"Request";if(K==="minimal"){let J=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(Z,Q)} ${this.color(X,"bright")} [${J}]`);let U=[],w=new A(this.colors);U.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let D=$.status?`${$.status}`:"ERROR";if(U.push({label:`${Z} Status`,value:D,color:Q}),$.metrics){let _=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;U.push({label:"Duration",value:_,color:"cyan"})}if(w.render(U),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(Z,Q)} ${this.color(X,"bright")}`);let z=[],W=new A(this.colors);if(z.push({label:"URL",value:$.request.url,color:"blue"}),z.push({label:"Method",value:$.request.method||"GET",color:"yellow"}),z.push({label:"Status",value:String($.status||"ERROR"),color:Q}),$.metrics)z.push({label:"Duration",value:this.formatDuration($.metrics.duration),color:"cyan"});if(this.shouldShowHeaders()&&$.headers&&Object.keys($.headers).length>0){let J=Object.entries($.headers).map(([U,w])=>({label:this.color(U,"dim"),value:String(w)}));z.push({label:"Headers",children:J})}if(this.shouldShowBody()&&$.body){let U=this.formatJson($.body).split(`
6
- `),w=this.shouldShowRequestDetails()?1/0:10,D=U.slice(0,w);if(U.length>w)D.push(this.color(`... (${U.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:D.join(`
7
- `)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let J=$.metrics,U=[];if(U.push({label:"Request Duration",value:this.formatDuration(J.duration),color:"cyan"}),J.size!==void 0)U.push({label:"Response Size",value:this.formatSize(J.size),color:"cyan"});if(J.dnsLookup)U.push({label:"DNS Lookup",value:this.formatDuration(J.dnsLookup),color:"cyan"});if(J.tcpConnection)U.push({label:"TCP Connection",value:this.formatDuration(J.tcpConnection),color:"cyan"});if(J.tlsHandshake)U.push({label:"TLS Handshake",value:this.formatDuration(J.tlsHandshake),color:"cyan"});if(J.firstByte)U.push({label:"Time to First Byte",value:this.formatDuration(J.firstByte),color:"cyan"});z.push({label:"Metrics",children:U})}if(W.render(z),$.error)console.log(),this.logValidationErrors($.error);console.log()}logSummary($,K=!1){if(this.config.format==="raw")return;if(this.config.format==="json"){let J={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((U)=>({request:{name:U.request.name,url:U.request.url,method:U.request.method||"GET"},success:U.success,status:U.status,...this.shouldShowHeaders()&&U.headers?{headers:U.headers}:{},...this.shouldShowBody()&&U.body?{body:U.body}:{},...U.error?{error:U.error}:{},...this.shouldShowMetrics()&&U.metrics?{metrics:U.metrics}:{}}))};console.log(JSON.stringify(J,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let J=$.failed===0?"green":"red",U=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`;console.log(`${K?"\u25C6 Global Summary":"Summary"}: ${this.color(U,J)}`);return}let Z=($.successful/$.total*100).toFixed(1),X=$.failed===0?"green":"red",z=$.failed===0?`${$.total} request${$.total===1?"":"s"} completed successfully`:`${$.successful}/${$.total} request${$.total===1?"":"s"} completed, ${$.failed} failed`,W=K?"\u25C6 Global Summary":"Summary";if(console.log(),console.log(`${W}: ${this.color(z,X)} (${this.color(this.formatDuration($.duration),"cyan")})`),$.failed>0&&this.shouldShowRequestDetails())$.results.filter((J)=>!J.success).forEach((J)=>{let U=J.request.name||J.request.url;console.log(` ${this.color("\u2022","red")} ${U}: ${J.error}`)})}logError($){console.error(this.color(`\u2717 ${$}`,"red"))}logWarning($){console.warn(this.color(`\u26A0 ${$}`,"yellow"))}logInfo($){console.log(this.color(`\u2139 ${$}`,"blue"))}logSuccess($){console.log(this.color(`\u2713 ${$}`,"green"))}logFileHeader($,K){if(!this.shouldShowOutput()||this.config.format!=="pretty")return;let Q=$.replace(/.*\//,"").replace(".yaml","");console.log(),console.log(this.color(`\u25B6 ${Q}.yaml`,"bright")+this.color(` (${K} request${K===1?"":"s"})`,"dim"))}}function p($,K){let Q=K.split("."),Z=$;for(let X of Q){if(Z===null||Z===void 0)return;if(typeof Z!=="object")return;let z=X.match(/^(\w+)\[(\d+)\]$/);if(z){let[,W,J]=z,U=Number.parseInt(J,10);if(Z=Z[W],Array.isArray(Z))Z=Z[U];else return}else if(/^\d+$/.test(X)&&Array.isArray(Z))Z=Z[Number.parseInt(X,10)];else Z=Z[X]}return Z}function d($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function P($,K){let Q={},Z={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[X,z]of Object.entries(K)){let W=p(Z,z);Q[X]=d(W)}return Q}function h(){return{}}class L{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}async executeRequest($,K=0){let Q=performance.now(),Z=this.mergeOutputConfig($),X=new M(Z);X.logRequestStart($,K);let z=S.buildCommand($);X.logCommand(z);let W=0,J,U=($.retry?.count||0)+1;while(W<U){if(W>0){if(X.logRetry(W,U-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let D=await S.executeCurl(z);if(D.success){let _=D.body;try{if(D.headers?.["content-type"]?.includes("application/json")||_&&(_.trim().startsWith("{")||_.trim().startsWith("[")))_=JSON.parse(_)}catch(I){}let F={request:$,success:!0,status:D.status,headers:D.headers,body:_,metrics:{...D.metrics,duration:performance.now()-Q}};if($.expect){let I=this.validateResponse(F,$.expect);if(!I.success)F.success=!1,F.error=I.error}return X.logRequestComplete(F),F}J=D.error,W++}let w={request:$,success:!1,error:J,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(w),w}validateResponse($,K){if(!K)return{success:!0};let Q=[];if(K.status!==void 0){let X=Array.isArray(K.status)?K.status:[K.status];if(!X.includes($.status||0))Q.push(`Expected status ${X.join(" or ")}, got ${$.status}`)}if(K.headers)for(let[X,z]of Object.entries(K.headers)){let W=$.headers?.[X]||$.headers?.[X.toLowerCase()];if(W!==z)Q.push(`Expected header ${X}="${z}", got "${W}"`)}if(K.body!==void 0){let X=this.validateBodyProperties($.body,K.body,"");if(X.length>0)Q.push(...X)}if(K.responseTime!==void 0&&$.metrics){let X=$.metrics.duration;if(!this.validateRangePattern(X,K.responseTime))Q.push(`Expected response time to match ${K.responseTime}ms, got ${X.toFixed(2)}ms`)}let Z=Q.length>0;if(K.failure===!0){if(Z)return{success:!1,error:Q.join("; ")};let X=$.status||0;if(X>=400)return{success:!0};else return{success:!1,error:`Expected request to fail (4xx/5xx) but got status ${X}`}}else if(Z)return{success:!1,error:Q.join("; ")};else return{success:!0}}validateBodyProperties($,K,Q){let Z=[];if(typeof K!=="object"||K===null){let X=this.validateValue($,K,Q||"body");if(!X.isValid)Z.push(X.error);return Z}if(Array.isArray(K)){let X=this.validateValue($,K,Q||"body");if(!X.isValid)Z.push(X.error);return Z}for(let[X,z]of Object.entries(K)){let W=Q?`${Q}.${X}`:X,J;if(Array.isArray($)&&this.isArraySelector(X))J=this.getArrayValue($,X);else J=$?.[X];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let U=this.validateBodyProperties(J,z,W);Z.push(...U)}else{let U=this.validateValue(J,z,W);if(!U.isValid)Z.push(U.error)}}return Z}validateValue($,K,Q){if(K==="*")return{isValid:!0};if(Array.isArray(K)){if(!K.some((X)=>{if(X==="*")return!0;if(typeof X==="string"&&this.isRegexPattern(X))return this.validateRegexPattern($,X);if(typeof X==="string"&&this.isRangePattern(X))return this.validateRangePattern($,X);return $===X}))return{isValid:!1,error:`Expected ${Q} to match one of ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRegexPattern(K)){if(!this.validateRegexPattern($,K))return{isValid:!1,error:`Expected ${Q} to match pattern ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(typeof K==="string"&&this.isRangePattern(K)){if(!this.validateRangePattern($,K))return{isValid:!1,error:`Expected ${Q} to match range ${K}, got ${JSON.stringify($)}`};return{isValid:!0}}if(K==="null"||K===null){if($!==null)return{isValid:!1,error:`Expected ${Q} to be null, got ${JSON.stringify($)}`};return{isValid:!0}}if($!==K)return{isValid:!1,error:`Expected ${Q} to be ${JSON.stringify(K)}, got ${JSON.stringify($)}`};return{isValid:!0}}isRegexPattern($){return $.startsWith("^")||$.endsWith("$")||$.includes("\\d")||$.includes("\\w")||$.includes("\\s")||$.includes("[")||$.includes("*")||$.includes("+")||$.includes("?")}validateRegexPattern($,K){let Q=String($);try{return new RegExp(K).test(Q)}catch{return!1}}isRangePattern($){return/^(>=?|<=?|>|<)\s*[\d.-]+(\s*,\s*(>=?|<=?|>|<)\s*[\d.-]+)*$/.test($)}validateRangePattern($,K){let Q=Number($);if(Number.isNaN(Q))return!1;let Z=K.match(/^([\d.-]+)\s*-\s*([\d.-]+)$/);if(Z){let z=Number(Z[1]),W=Number(Z[2]);return Q>=z&&Q<=W}return K.split(",").map((z)=>z.trim()).every((z)=>{let W=z.match(/^(>=?|<=?|>|<)\s*([\d.-]+)$/);if(!W)return!1;let J=W[1],U=Number(W[2]);switch(J){case">":return Q>U;case">=":return Q>=U;case"<":return Q<U;case"<=":return Q<=U;default:return!1}})}isArraySelector($){return/^\[.*\]$/.test($)||$==="*"||$.startsWith("slice(")}getArrayValue($,K){if(K==="*")return $;if(K.startsWith("[")&&K.endsWith("]")){let Q=K.slice(1,-1);if(Q==="*")return $;let Z=Number(Q);if(!Number.isNaN(Z))return Z>=0?$[Z]:$[$.length+Z]}if(K.startsWith("slice(")){let Q=K.match(/slice\((\d+)(?:,(\d+))?\)/);if(Q){let Z=Number(Q[1]),X=Q[2]?Number(Q[2]):void 0;return $.slice(Z,X)}}return}async executeSequential($){let K=performance.now(),Q=[],Z=h();for(let X=0;X<$.length;X++){let z=this.interpolateStoreVariables($[X],Z),W=await this.executeRequest(z,X+1);if(Q.push(W),W.success&&z.store){let J=P(W,z.store);Object.assign(Z,J),this.logStoredValues(J)}if(!W.success&&!this.globalConfig.continueOnError){this.logger.logError("Stopping execution due to error");break}}return this.createSummary(Q,performance.now()-K)}interpolateStoreVariables($,K){if(Object.keys(K).length===0)return $;return H.interpolateVariables($,{},K)}logStoredValues($){if(Object.keys($).length===0)return;let K=Object.entries($);for(let[Q,Z]of K){let X=Z.length>50?`${Z.substring(0,50)}...`:Z;this.logger.logInfo(`Stored: ${Q} = "${X}"`)}}async executeParallel($){let K=performance.now(),Q=$.map((X,z)=>this.executeRequest(X,z+1)),Z=await Promise.all(Q);return this.createSummary(Z,performance.now()-K)}async execute($){this.logger.logExecutionStart($.length,this.globalConfig.execution||"sequential");let K=this.globalConfig.execution==="parallel"?await this.executeParallel($):await this.executeSequential($);if(this.logger.logSummary(K),this.globalConfig.output?.saveToFile)await this.saveSummaryToFile(K);return K}createSummary($,K){let Q=$.filter((X)=>X.success).length,Z=$.filter((X)=>!X.success).length;return{total:$.length,successful:Q,failed:Z,duration:K,results:$}}async saveSummaryToFile($){let K=this.globalConfig.output?.saveToFile;if(!K)return;let Q=JSON.stringify($,null,2);await Bun.write(K,Q),this.logger.logInfo(`Results saved to ${K}`)}}function j(){if(typeof BUILD_VERSION<"u")return BUILD_VERSION;if(process.env.CURL_RUNNER_VERSION)return process.env.CURL_RUNNER_VERSION;try{let $=["../package.json","./package.json","../../package.json"];for(let K of $)try{let Q=E(K);if(Q.name==="@curl-runner/cli"&&Q.version)return Q.version}catch{}return"0.0.0"}catch{return"0.0.0"}}var V={reset:"\x1B[0m",bright:"\x1B[1m",dim:"\x1B[2m",underscore:"\x1B[4m",black:"\x1B[30m",red:"\x1B[31m",green:"\x1B[32m",yellow:"\x1B[33m",blue:"\x1B[34m",magenta:"\x1B[35m",cyan:"\x1B[36m",white:"\x1B[37m",bgBlack:"\x1B[40m",bgRed:"\x1B[41m",bgGreen:"\x1B[42m",bgYellow:"\x1B[43m",bgBlue:"\x1B[44m",bgMagenta:"\x1B[45m",bgCyan:"\x1B[46m",bgWhite:"\x1B[47m"};function O($,K){return`${V[K]}${$}${V.reset}`}var b=`${process.env.HOME}/.curl-runner-version-cache.json`,m=86400000,c="https://registry.npmjs.org/@curl-runner/cli/latest";class R{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=j();if(K==="0.0.0")return;if(!$){let Z=await this.getCachedVersion();if(Z&&Date.now()-Z.lastCheck<m){this.compareVersions(K,Z.latestVersion);return}}let Q=await this.fetchLatestVersion();if(Q)await this.setCachedVersion(Q),this.compareVersions(K,Q)}catch{}}async fetchLatestVersion(){try{let $=await fetch(c,{signal:AbortSignal.timeout(3000)});if(!$.ok)return null;return(await $.json()).version}catch{return null}}compareVersions($,K){if(this.isNewerVersion($,K))console.log(),console.log(O("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E","yellow")),console.log(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\uD83D\uDCE6 New version available!","bright")+` ${O($,"red")} \u2192 ${O(K,"green")} `+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" Update with: "+O("npm install -g @curl-runner/cli","cyan")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" or: "+O("bun install -g @curl-runner/cli","cyan")+" "+O("\u2502","yellow")),console.log(O("\u2502","yellow")+" "+O("\u2502","yellow")),console.log(O("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F","yellow")),console.log()}isNewerVersion($,K){try{let Q=$.replace(/^v/,""),Z=K.replace(/^v/,""),X=Q.split(".").map(Number),z=Z.split(".").map(Number);for(let W=0;W<Math.max(X.length,z.length);W++){let J=X[W]||0,U=z[W]||0;if(U>J)return!0;if(U<J)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(b);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(b,JSON.stringify(K))}catch{}}}class C{logger=new M;async loadConfigFile(){let $=["curl-runner.yaml","curl-runner.yml",".curl-runner.yaml",".curl-runner.yml"];for(let K of $)try{if(await Bun.file(K).exists()){let Z=await H.parseFile(K),X=Z.global||Z;return this.logger.logInfo(`Loaded configuration from ${K}`),X}}catch(Q){this.logger.logWarning(`Failed to load configuration from ${K}: ${Q}`)}return{}}loadEnvironmentVariables(){let $={};if(process.env.CURL_RUNNER_TIMEOUT)$.defaults={...$.defaults,timeout:Number.parseInt(process.env.CURL_RUNNER_TIMEOUT,10)};if(process.env.CURL_RUNNER_RETRIES)$.defaults={...$.defaults,retry:{...$.defaults?.retry,count:Number.parseInt(process.env.CURL_RUNNER_RETRIES,10)}};if(process.env.CURL_RUNNER_RETRY_DELAY)$.defaults={...$.defaults,retry:{...$.defaults?.retry,delay:Number.parseInt(process.env.CURL_RUNNER_RETRY_DELAY,10)}};if(process.env.CURL_RUNNER_VERBOSE)$.output={...$.output,verbose:process.env.CURL_RUNNER_VERBOSE.toLowerCase()==="true"};if(process.env.CURL_RUNNER_EXECUTION)$.execution=process.env.CURL_RUNNER_EXECUTION;if(process.env.CURL_RUNNER_CONTINUE_ON_ERROR)$.continueOnError=process.env.CURL_RUNNER_CONTINUE_ON_ERROR.toLowerCase()==="true";if(process.env.CURL_RUNNER_OUTPUT_FORMAT){let K=process.env.CURL_RUNNER_OUTPUT_FORMAT;if(["json","pretty","raw"].includes(K))$.output={...$.output,format:K}}if(process.env.CURL_RUNNER_PRETTY_LEVEL){let K=process.env.CURL_RUNNER_PRETTY_LEVEL;if(["minimal","standard","detailed"].includes(K))$.output={...$.output,prettyLevel:K}}if(process.env.CURL_RUNNER_OUTPUT_FILE)$.output={...$.output,saveToFile:process.env.CURL_RUNNER_OUTPUT_FILE};if(process.env.CURL_RUNNER_STRICT_EXIT)$.ci={...$.ci,strictExit:process.env.CURL_RUNNER_STRICT_EXIT.toLowerCase()==="true"};if(process.env.CURL_RUNNER_FAIL_ON)$.ci={...$.ci,failOn:Number.parseInt(process.env.CURL_RUNNER_FAIL_ON,10)};if(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE){let K=Number.parseFloat(process.env.CURL_RUNNER_FAIL_ON_PERCENTAGE);if(K>=0&&K<=100)$.ci={...$.ci,failOnPercentage:K}}return $}async run($){try{let{files:K,options:Q}=this.parseArguments($);if(!Q.version&&!Q.help)new R().checkForUpdates().catch(()=>{});if(Q.help){this.showHelp();return}if(Q.version){console.log(`curl-runner v${j()}`);return}let Z=this.loadEnvironmentVariables(),X=await this.loadConfigFile(),z=await this.findYamlFiles(K,Q);if(z.length===0)this.logger.logError("No YAML files found"),process.exit(1);this.logger.logInfo(`Found ${z.length} YAML file(s)`);let W=this.mergeGlobalConfigs(Z,X),J=[],U=[];for(let F of z){this.logger.logInfo(`Processing: ${F}`);let{requests:I,config:Y}=await this.processYamlFile(F),B=Y?.output||{},G=I.map((T)=>({...T,sourceOutputConfig:B,sourceFile:F}));if(Y){let{...T}=Y;W=this.mergeGlobalConfigs(W,T)}U.push({file:F,requests:G,config:Y}),J.push(...G)}if(Q.execution)W.execution=Q.execution;if(Q.continueOnError!==void 0)W.continueOnError=Q.continueOnError;if(Q.verbose!==void 0)W.output={...W.output,verbose:Q.verbose};if(Q.quiet!==void 0)W.output={...W.output,verbose:!1};if(Q.output)W.output={...W.output,saveToFile:Q.output};if(Q.outputFormat)W.output={...W.output,format:Q.outputFormat};if(Q.prettyLevel)W.output={...W.output,prettyLevel:Q.prettyLevel};if(Q.showHeaders!==void 0)W.output={...W.output,showHeaders:Q.showHeaders};if(Q.showBody!==void 0)W.output={...W.output,showBody:Q.showBody};if(Q.showMetrics!==void 0)W.output={...W.output,showMetrics:Q.showMetrics};if(Q.timeout)W.defaults={...W.defaults,timeout:Q.timeout};if(Q.retries||Q.noRetry){let F=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:F}}}if(Q.retryDelay)W.defaults={...W.defaults,retry:{...W.defaults?.retry,delay:Q.retryDelay}};if(Q.strictExit!==void 0)W.ci={...W.ci,strictExit:Q.strictExit};if(Q.failOn!==void 0)W.ci={...W.ci,failOn:Q.failOn};if(Q.failOnPercentage!==void 0)W.ci={...W.ci,failOnPercentage:Q.failOnPercentage};if(J.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new L(W),D;if(U.length>1){let F=[],I=0;for(let G=0;G<U.length;G++){let T=U[G];this.logger.logFileHeader(T.file,T.requests.length);let k=await w.execute(T.requests);if(F.push(...k.results),I+=k.duration,G<U.length-1)console.log()}let Y=F.filter((G)=>G.success).length,B=F.filter((G)=>!G.success).length;D={total:F.length,successful:Y,failed:B,duration:I,results:F},w.logger.logSummary(D,!0)}else D=await w.execute(J);let _=this.determineExitCode(D,W);process.exit(_)}catch(K){this.logger.logError(K instanceof Error?K.message:String(K)),process.exit(1)}}parseArguments($){let K={},Q=[];for(let Z=0;Z<$.length;Z++){let X=$[Z];if(X.startsWith("--")){let z=X.slice(2),W=$[Z+1];if(z==="help"||z==="version")K[z]=!0;else if(z==="no-retry")K.noRetry=!0;else if(z==="quiet")K.quiet=!0;else if(z==="show-headers")K.showHeaders=!0;else if(z==="show-body")K.showBody=!0;else if(z==="show-metrics")K.showMetrics=!0;else if(z==="strict-exit")K.strictExit=!0;else if(W&&!W.startsWith("--")){if(z==="continue-on-error")K.continueOnError=W==="true";else if(z==="verbose")K.verbose=W==="true";else if(z==="timeout")K.timeout=Number.parseInt(W,10);else if(z==="retries")K.retries=Number.parseInt(W,10);else if(z==="retry-delay")K.retryDelay=Number.parseInt(W,10);else if(z==="fail-on")K.failOn=Number.parseInt(W,10);else if(z==="fail-on-percentage"){let J=Number.parseFloat(W);if(J>=0&&J<=100)K.failOnPercentage=J}else if(z==="output-format"){if(["json","pretty","raw"].includes(W))K.outputFormat=W}else if(z==="pretty-level"){if(["minimal","standard","detailed"].includes(W))K.prettyLevel=W}else K[z]=W;Z++}else K[z]=!0}else if(X.startsWith("-")){let z=X.slice(1);for(let W of z)switch(W){case"h":K.help=!0;break;case"v":K.verbose=!0;break;case"p":K.execution="parallel";break;case"c":K.continueOnError=!0;break;case"q":K.quiet=!0;break;case"o":{let J=$[Z+1];if(J&&!J.startsWith("-"))K.output=J,Z++;break}}}else Q.push(X)}return{files:Q,options:K}}async findYamlFiles($,K){let Q=new Set,Z=[];if($.length===0)Z=K.all?["**/*.yaml","**/*.yml"]:["*.yaml","*.yml"];else for(let X of $)try{let W=await(await import("fs/promises")).stat(X);if(W.isDirectory()){if(Z.push(`${X}/*.yaml`,`${X}/*.yml`),K.all)Z.push(`${X}/**/*.yaml`,`${X}/**/*.yml`)}else if(W.isFile())Z.push(X)}catch{Z.push(X)}for(let X of Z){let z=new i(X);for await(let W of z.scan("."))if(W.endsWith(".yaml")||W.endsWith(".yml"))Q.add(W)}return Array.from(Q).sort()}async processYamlFile($){let K=await H.parseFile($),Q=[],Z;if(K.global)Z=K.global;let X={...K.global?.variables,...K.collection?.variables},z={...K.global?.defaults,...K.collection?.defaults};if(K.request){let W=this.prepareRequest(K.request,X,z);Q.push(W)}if(K.requests)for(let W of K.requests){let J=this.prepareRequest(W,X,z);Q.push(J)}if(K.collection?.requests)for(let W of K.collection.requests){let J=this.prepareRequest(W,X,z);Q.push(J)}return{requests:Q,config:Z}}prepareRequest($,K,Q){let Z=H.interpolateVariables($,K);return H.mergeConfigs(Q,Z)}mergeGlobalConfigs($,K){return{...$,...K,variables:{...$.variables,...K.variables},output:{...$.output,...K.output},defaults:{...$.defaults,...K.defaults},ci:{...$.ci,...K.ci}}}determineExitCode($,K){let{failed:Q,total:Z}=$,X=K.ci;if(Q===0)return 0;if(X){if(X.strictExit)return 1;if(X.failOn!==void 0&&Q>X.failOn)return 1;if(X.failOnPercentage!==void 0&&Z>0){if(Q/Z*100>X.failOnPercentage)return 1}if(X.failOn!==void 0||X.failOnPercentage!==void 0)return 0}return!K.continueOnError?1:0}showHelp(){console.log(`
8
- ${this.logger.color("\uD83D\uDE80 CURL RUNNER","bright")}
9
-
10
- ${this.logger.color("USAGE:","yellow")}
11
- curl-runner [files...] [options]
12
-
13
- ${this.logger.color("OPTIONS:","yellow")}
14
- -h, --help Show this help message
15
- -v, --verbose Enable verbose output
16
- -q, --quiet Suppress non-error output
17
- -p, --execution parallel Execute requests in parallel
18
- -c, --continue-on-error Continue execution on errors
19
- -o, --output <file> Save results to file
20
- --all Find all YAML files recursively
21
- --timeout <ms> Set request timeout in milliseconds
22
- --retries <count> Set maximum retry attempts
23
- --retry-delay <ms> Set delay between retries in milliseconds
24
- --no-retry Disable retry mechanism
25
- --output-format <format> Set output format (json|pretty|raw)
26
- --pretty-level <level> Set pretty format level (minimal|standard|detailed)
27
- --show-headers Include response headers in output
28
- --show-body Include response body in output
29
- --show-metrics Include performance metrics in output
30
- --version Show version
31
-
32
- ${this.logger.color("CI/CD OPTIONS:","yellow")}
33
- --strict-exit Exit with code 1 if any validation fails (for CI/CD)
34
- --fail-on <count> Exit with code 1 if failures exceed this count
35
- --fail-on-percentage <pct> Exit with code 1 if failure percentage exceeds this value
36
-
37
- ${this.logger.color("EXAMPLES:","yellow")}
38
- # Run all YAML files in current directory
39
- curl-runner
40
-
41
- # Run specific file
42
- curl-runner api-tests.yaml
43
-
44
- # Run all files in a directory
45
- curl-runner examples/
46
-
47
- # Run all files in multiple directories
48
- curl-runner tests/ examples/
49
-
50
- # Run all files recursively in parallel
51
- curl-runner --all -p
52
-
53
- # Run directory recursively
54
- curl-runner --all examples/
55
-
56
- # Run with verbose output and continue on errors
57
- curl-runner tests/*.yaml -vc
58
-
59
- # Run with minimal pretty output (only status and errors)
60
- curl-runner --output-format pretty --pretty-level minimal test.yaml
61
-
62
- # Run with detailed pretty output (show all information)
63
- curl-runner --output-format pretty --pretty-level detailed test.yaml
64
-
65
- # CI/CD: Fail if any validation fails (strict mode)
66
- curl-runner tests/ --strict-exit
67
-
68
- # CI/CD: Run all tests but fail if any validation fails
69
- curl-runner tests/ --continue-on-error --strict-exit
70
-
71
- # CI/CD: Allow up to 2 failures
72
- curl-runner tests/ --fail-on 2
73
-
74
- # CI/CD: Allow up to 10% failures
75
- curl-runner tests/ --fail-on-percentage 10
76
-
77
- ${this.logger.color("YAML STRUCTURE:","yellow")}
78
- Single request:
79
- request:
80
- url: https://api.example.com
81
- method: GET
82
-
83
- Multiple requests:
84
- requests:
85
- - url: https://api.example.com/users
86
- method: GET
87
- - url: https://api.example.com/posts
88
- method: POST
89
- body: { title: "Test" }
90
-
91
- With global config:
92
- global:
93
- execution: parallel
94
- variables:
95
- BASE_URL: https://api.example.com
96
- requests:
97
- - url: \${BASE_URL}/users
98
- method: GET
99
- `)}}var n=new C;n.run(process.argv.slice(2));
100
-
101
- //# debugId=1C214F4C836B615D64756E2164756E21
102
- //# sourceMappingURL=cli.js.map