@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 +1 -1
- package/src/executor/request-executor.ts +53 -0
- package/src/parser/yaml.test.ts +223 -0
- package/src/parser/yaml.ts +120 -10
- package/src/types/config.ts +53 -0
- package/src/utils/curl-builder.test.ts +165 -0
- package/src/utils/curl-builder.ts +35 -2
- package/dist/cli.js +0 -102
package/package.json
CHANGED
|
@@ -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
|
|
package/src/parser/yaml.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/parser/yaml.ts
CHANGED
|
@@ -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
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
package/src/types/config.ts
CHANGED
|
@@ -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.
|
|
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
|