@curl-runner/cli 1.3.0 → 1.6.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.3.0",
3
+ "version": "1.6.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",
@@ -174,3 +174,307 @@ describe('YamlParser.resolveVariable', () => {
174
174
  expect(result).toBe('store-value');
175
175
  });
176
176
  });
177
+
178
+ describe('YamlParser string transforms', () => {
179
+ test('should transform variable to uppercase with :upper', () => {
180
+ const variables = { ENV: 'production' };
181
+ const result = YamlParser.resolveVariable('ENV:upper', variables, {});
182
+ expect(result).toBe('PRODUCTION');
183
+ });
184
+
185
+ test('should transform variable to lowercase with :lower', () => {
186
+ const variables = { RESOURCE: 'USERS' };
187
+ const result = YamlParser.resolveVariable('RESOURCE:lower', variables, {});
188
+ expect(result).toBe('users');
189
+ });
190
+
191
+ test('should return null for transform on missing variable', () => {
192
+ const result = YamlParser.resolveVariable('MISSING:upper', {}, {});
193
+ expect(result).toBeNull();
194
+ });
195
+
196
+ test('should work with interpolateVariables for :upper transform', () => {
197
+ const obj = {
198
+ headers: {
199
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
200
+ 'X-Environment': '${ENV:upper}',
201
+ },
202
+ };
203
+ const variables = { ENV: 'production' };
204
+ const result = YamlParser.interpolateVariables(obj, variables);
205
+ expect(result).toEqual({
206
+ headers: {
207
+ 'X-Environment': 'PRODUCTION',
208
+ },
209
+ });
210
+ });
211
+
212
+ test('should work with interpolateVariables for :lower transform', () => {
213
+ const obj = {
214
+ headers: {
215
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
216
+ 'X-Resource': '${RESOURCE:lower}',
217
+ },
218
+ };
219
+ const variables = { RESOURCE: 'USERS' };
220
+ const result = YamlParser.interpolateVariables(obj, variables);
221
+ expect(result).toEqual({
222
+ headers: {
223
+ 'X-Resource': 'users',
224
+ },
225
+ });
226
+ });
227
+
228
+ test('should mix transforms with regular variables', () => {
229
+ const obj = {
230
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
231
+ url: '${BASE_URL}/${RESOURCE:lower}',
232
+ headers: {
233
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
234
+ 'X-Environment': '${ENV:upper}',
235
+ },
236
+ };
237
+ const variables = { BASE_URL: 'https://api.example.com', RESOURCE: 'Users', ENV: 'staging' };
238
+ const result = YamlParser.interpolateVariables(obj, variables);
239
+ expect(result).toEqual({
240
+ url: 'https://api.example.com/users',
241
+ headers: {
242
+ 'X-Environment': 'STAGING',
243
+ },
244
+ });
245
+ });
246
+
247
+ test('should keep unresolved transforms as-is', () => {
248
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
249
+ const obj = { value: '${MISSING:upper}' };
250
+ const result = YamlParser.interpolateVariables(obj, {});
251
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing string transform
252
+ expect(result).toEqual({ value: '${MISSING:upper}' });
253
+ });
254
+ });
255
+
256
+ describe('YamlParser.resolveVariable with default values', () => {
257
+ test('should use default value when variable is not set', () => {
258
+ const result = YamlParser.resolveVariable('API_TIMEOUT:5000', {}, {});
259
+ expect(result).toBe('5000');
260
+ });
261
+
262
+ test('should use variable value when set, ignoring default', () => {
263
+ const variables = { API_TIMEOUT: '10000' };
264
+ const result = YamlParser.resolveVariable('API_TIMEOUT:5000', variables, {});
265
+ expect(result).toBe('10000');
266
+ });
267
+
268
+ test('should handle nested default with first variable set', () => {
269
+ const variables = { DATABASE_HOST: 'prod-db.example.com' };
270
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
271
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', variables, {});
272
+ expect(result).toBe('prod-db.example.com');
273
+ });
274
+
275
+ test('should handle nested default with second variable set', () => {
276
+ const variables = { DB_HOST: 'staging-db.example.com' };
277
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
278
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', variables, {});
279
+ expect(result).toBe('staging-db.example.com');
280
+ });
281
+
282
+ test('should use final fallback when no variables are set', () => {
283
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
284
+ const result = YamlParser.resolveVariable('DATABASE_HOST:${DB_HOST:localhost}', {}, {});
285
+ expect(result).toBe('localhost');
286
+ });
287
+
288
+ test('should not confuse DATE: with default syntax', () => {
289
+ const result = YamlParser.resolveVariable('DATE:YYYY-MM-DD', {}, {});
290
+ // Should return a formatted date, not treat 'YYYY-MM-DD' as a default
291
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
292
+ });
293
+
294
+ test('should not confuse TIME: with default syntax', () => {
295
+ const result = YamlParser.resolveVariable('TIME:HH:mm:ss', {}, {});
296
+ // Should return a formatted time, not treat 'HH:mm:ss' as a default
297
+ expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
298
+ });
299
+
300
+ test('should handle default value with special characters', () => {
301
+ const result = YamlParser.resolveVariable('API_URL:https://api.example.com/v1', {}, {});
302
+ expect(result).toBe('https://api.example.com/v1');
303
+ });
304
+
305
+ test('should handle empty default value', () => {
306
+ const result = YamlParser.resolveVariable('OPTIONAL:', {}, {});
307
+ expect(result).toBe('');
308
+ });
309
+
310
+ test('should use store variable when available before default', () => {
311
+ const storeContext = { token: 'stored-token' };
312
+ // Note: store variables have a different syntax (store.X), but we test
313
+ // that the default mechanism doesn't interfere
314
+ const result = YamlParser.resolveVariable('AUTH_TOKEN:default-token', {}, storeContext);
315
+ expect(result).toBe('default-token');
316
+ });
317
+ });
318
+
319
+ describe('YamlParser.interpolateVariables with default values', () => {
320
+ test('should interpolate variable with default in string', () => {
321
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
322
+ const obj = { timeout: '${API_TIMEOUT:5000}' };
323
+ const result = YamlParser.interpolateVariables(obj, {});
324
+ expect(result).toEqual({ timeout: '5000' });
325
+ });
326
+
327
+ test('should interpolate variable with value set over default', () => {
328
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
329
+ const obj = { timeout: '${API_TIMEOUT:5000}' };
330
+ const variables = { API_TIMEOUT: '10000' };
331
+ const result = YamlParser.interpolateVariables(obj, variables);
332
+ expect(result).toEqual({ timeout: '10000' });
333
+ });
334
+
335
+ test('should handle multiple variables with defaults in one string', () => {
336
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
337
+ const obj = { url: '${BASE_URL:https://api.example.com}/${VERSION:v1}/users' };
338
+ const result = YamlParser.interpolateVariables(obj, {});
339
+ expect(result).toEqual({ url: 'https://api.example.com/v1/users' });
340
+ });
341
+
342
+ test('should mix defaults with regular variables', () => {
343
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
344
+ const obj = { url: '${BASE_URL}/api/${VERSION:v1}/users' };
345
+ const variables = { BASE_URL: 'https://prod.example.com' };
346
+ const result = YamlParser.interpolateVariables(obj, variables);
347
+ expect(result).toEqual({ url: 'https://prod.example.com/api/v1/users' });
348
+ });
349
+
350
+ test('should handle nested defaults in interpolation', () => {
351
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
352
+ const obj = { host: '${DATABASE_HOST:${DB_HOST:localhost}}' };
353
+ const result = YamlParser.interpolateVariables(obj, {});
354
+ expect(result).toEqual({ host: 'localhost' });
355
+ });
356
+
357
+ test('should preserve DATE: formatting syntax', () => {
358
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
359
+ const obj = { date: '${DATE:YYYY-MM-DD}' };
360
+ const result = YamlParser.interpolateVariables(obj, {}) as { date: string };
361
+ expect(result.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
362
+ });
363
+
364
+ test('should preserve TIME: formatting syntax', () => {
365
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing variable interpolation
366
+ const obj = { time: '${TIME:HH:mm:ss}' };
367
+ const result = YamlParser.interpolateVariables(obj, {}) as { time: string };
368
+ expect(result.time).toMatch(/^\d{2}:\d{2}:\d{2}$/);
369
+ });
370
+ });
371
+
372
+ describe('YamlParser.resolveDynamicVariable', () => {
373
+ test('should resolve UUID to a valid full UUID', () => {
374
+ const result = YamlParser.resolveDynamicVariable('UUID');
375
+ expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
376
+ });
377
+
378
+ test('should resolve UUID:short to first 8 characters of a UUID', () => {
379
+ const result = YamlParser.resolveDynamicVariable('UUID:short');
380
+ expect(result).toMatch(/^[0-9a-f]{8}$/i);
381
+ expect(result).toHaveLength(8);
382
+ });
383
+
384
+ test('should resolve RANDOM:min-max to a number within range', () => {
385
+ const result = YamlParser.resolveDynamicVariable('RANDOM:1-100');
386
+ expect(result).not.toBeNull();
387
+ const num = Number(result);
388
+ expect(num).toBeGreaterThanOrEqual(1);
389
+ expect(num).toBeLessThanOrEqual(100);
390
+ });
391
+
392
+ test('should resolve RANDOM:min-max with large range', () => {
393
+ const result = YamlParser.resolveDynamicVariable('RANDOM:1-1000');
394
+ expect(result).not.toBeNull();
395
+ const num = Number(result);
396
+ expect(num).toBeGreaterThanOrEqual(1);
397
+ expect(num).toBeLessThanOrEqual(1000);
398
+ });
399
+
400
+ test('should resolve RANDOM:min-max with same min and max', () => {
401
+ const result = YamlParser.resolveDynamicVariable('RANDOM:5-5');
402
+ expect(result).toBe('5');
403
+ });
404
+
405
+ test('should resolve RANDOM:string:length to alphanumeric string of correct length', () => {
406
+ const result = YamlParser.resolveDynamicVariable('RANDOM:string:10');
407
+ expect(result).not.toBeNull();
408
+ expect(result).toHaveLength(10);
409
+ expect(result).toMatch(/^[A-Za-z0-9]+$/);
410
+ });
411
+
412
+ test('should resolve RANDOM:string:length with different lengths', () => {
413
+ const result5 = YamlParser.resolveDynamicVariable('RANDOM:string:5');
414
+ const result20 = YamlParser.resolveDynamicVariable('RANDOM:string:20');
415
+ expect(result5).toHaveLength(5);
416
+ expect(result20).toHaveLength(20);
417
+ expect(result5).toMatch(/^[A-Za-z0-9]+$/);
418
+ expect(result20).toMatch(/^[A-Za-z0-9]+$/);
419
+ });
420
+
421
+ test('should resolve TIMESTAMP to a numeric string', () => {
422
+ const result = YamlParser.resolveDynamicVariable('TIMESTAMP');
423
+ expect(result).toMatch(/^\d+$/);
424
+ });
425
+
426
+ test('should resolve CURRENT_TIME to a numeric string', () => {
427
+ const result = YamlParser.resolveDynamicVariable('CURRENT_TIME');
428
+ expect(result).toMatch(/^\d+$/);
429
+ });
430
+
431
+ test('should return null for unknown dynamic variable', () => {
432
+ const result = YamlParser.resolveDynamicVariable('UNKNOWN');
433
+ expect(result).toBeNull();
434
+ });
435
+ });
436
+
437
+ describe('YamlParser.interpolateVariables with new dynamic variables', () => {
438
+ test('should interpolate UUID:short in objects', () => {
439
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${UUID:short} interpolation
440
+ const obj = { id: '${UUID:short}' };
441
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
442
+ expect(result.id).toMatch(/^[0-9a-f]{8}$/i);
443
+ });
444
+
445
+ test('should interpolate RANDOM:min-max in objects', () => {
446
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${RANDOM:x-y} interpolation
447
+ const obj = { value: '${RANDOM:1-100}' };
448
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
449
+ const num = Number(result.value);
450
+ expect(num).toBeGreaterThanOrEqual(1);
451
+ expect(num).toBeLessThanOrEqual(100);
452
+ });
453
+
454
+ test('should interpolate RANDOM:string:length in objects', () => {
455
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing ${RANDOM:string:n} interpolation
456
+ const obj = { token: '${RANDOM:string:16}' };
457
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
458
+ expect(result.token).toHaveLength(16);
459
+ expect(result.token).toMatch(/^[A-Za-z0-9]+$/);
460
+ });
461
+
462
+ test('should interpolate multiple new dynamic variables in one object', () => {
463
+ const obj = {
464
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
465
+ sessionId: '${UUID:short}',
466
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
467
+ randomNum: '${RANDOM:1-1000}',
468
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Testing dynamic variable interpolation
469
+ randomStr: '${RANDOM:string:10}',
470
+ };
471
+ const result = YamlParser.interpolateVariables(obj, {}) as typeof obj;
472
+
473
+ expect(result.sessionId).toMatch(/^[0-9a-f]{8}$/i);
474
+ const num = Number(result.randomNum);
475
+ expect(num).toBeGreaterThanOrEqual(1);
476
+ expect(num).toBeLessThanOrEqual(1000);
477
+ expect(result.randomStr).toHaveLength(10);
478
+ expect(result.randomStr).toMatch(/^[A-Za-z0-9]+$/);
479
+ });
480
+ });
@@ -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,35 @@ 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 (
43
+ extractedVars.length === 1 &&
44
+ extractedVars[0].start === 0 &&
45
+ extractedVars[0].end === obj.length
46
+ ) {
47
+ const varName = extractedVars[0].name;
36
48
  const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
37
49
  return resolvedValue !== null ? resolvedValue : obj;
38
50
  }
39
51
 
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
- });
52
+ // Handle multiple variables in the string
53
+ let result = '';
54
+ let lastEnd = 0;
55
+ for (const varRef of extractedVars) {
56
+ result += obj.slice(lastEnd, varRef.start);
57
+ const resolvedValue = YamlParser.resolveVariable(varRef.name, variables, storeContext);
58
+ result += resolvedValue !== null ? resolvedValue : obj.slice(varRef.start, varRef.end);
59
+ lastEnd = varRef.end;
60
+ }
61
+ result += obj.slice(lastEnd);
62
+ return result;
45
63
  }
46
64
 
47
65
  if (Array.isArray(obj)) {
@@ -61,7 +79,7 @@ export class YamlParser {
61
79
 
62
80
  /**
63
81
  * Resolves a single variable reference.
64
- * Priority: store context > dynamic variables > static variables
82
+ * Priority: store context > string transforms > dynamic variables > static variables > default values
65
83
  */
66
84
  static resolveVariable(
67
85
  varName: string,
@@ -77,6 +95,52 @@ export class YamlParser {
77
95
  return null; // Store variable not found, return null to keep original
78
96
  }
79
97
 
98
+ // Check for string transforms: ${VAR:upper} or ${VAR:lower}
99
+ const transformMatch = varName.match(/^([^:]+):(upper|lower)$/);
100
+ if (transformMatch) {
101
+ const baseVarName = transformMatch[1];
102
+ const transform = transformMatch[2];
103
+ const baseValue = variables[baseVarName] || process.env[baseVarName];
104
+
105
+ if (baseValue) {
106
+ return transform === 'upper' ? baseValue.toUpperCase() : baseValue.toLowerCase();
107
+ }
108
+ return null; // Base variable not found
109
+ }
110
+
111
+ // Check for default value syntax: ${VAR:default}
112
+ // Must check before dynamic variables to properly handle defaults
113
+ const colonIndex = varName.indexOf(':');
114
+ if (colonIndex !== -1) {
115
+ const actualVarName = varName.slice(0, colonIndex);
116
+ const defaultValue = varName.slice(colonIndex + 1);
117
+
118
+ // Don't confuse with DATE:, TIME:, UUID:, RANDOM: patterns
119
+ // These are reserved prefixes for dynamic variable generation
120
+ const reservedPrefixes = ['DATE', 'TIME', 'UUID', 'RANDOM'];
121
+ if (!reservedPrefixes.includes(actualVarName)) {
122
+ // Try to resolve the actual variable name
123
+ const resolved = YamlParser.resolveVariable(actualVarName, variables, storeContext);
124
+ if (resolved !== null) {
125
+ return resolved;
126
+ }
127
+ // Variable not found, use the default value
128
+ // The default value might itself be a variable reference like ${OTHER_VAR:fallback}
129
+ // Note: Due to the regex in interpolateVariables using [^}]+, nested braces
130
+ // get truncated (e.g., "${VAR:${OTHER:default}}" captures "VAR:${OTHER:default")
131
+ // So we check for both complete ${...} and truncated ${... patterns
132
+ if (defaultValue.startsWith('${')) {
133
+ // Handle both complete ${VAR} and truncated ${VAR (from nested braces)
134
+ const nestedVarName = defaultValue.endsWith('}')
135
+ ? defaultValue.slice(2, -1)
136
+ : defaultValue.slice(2);
137
+ const nestedResolved = YamlParser.resolveVariable(nestedVarName, variables, storeContext);
138
+ return nestedResolved !== null ? nestedResolved : defaultValue;
139
+ }
140
+ return defaultValue;
141
+ }
142
+ }
143
+
80
144
  // Check for dynamic variable
81
145
  const dynamicValue = YamlParser.resolveDynamicVariable(varName);
82
146
  if (dynamicValue !== null) {
@@ -97,6 +161,29 @@ export class YamlParser {
97
161
  return crypto.randomUUID();
98
162
  }
99
163
 
164
+ // UUID:short - first segment (8 chars) of a UUID
165
+ if (varName === 'UUID:short') {
166
+ return crypto.randomUUID().split('-')[0];
167
+ }
168
+
169
+ // RANDOM:min-max - random number in range
170
+ const randomRangeMatch = varName.match(/^RANDOM:(\d+)-(\d+)$/);
171
+ if (randomRangeMatch) {
172
+ const min = Number(randomRangeMatch[1]);
173
+ const max = Number(randomRangeMatch[2]);
174
+ return String(Math.floor(Math.random() * (max - min + 1)) + min);
175
+ }
176
+
177
+ // RANDOM:string:length - random alphanumeric string
178
+ const randomStringMatch = varName.match(/^RANDOM:string:(\d+)$/);
179
+ if (randomStringMatch) {
180
+ const length = Number(randomStringMatch[1]);
181
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
182
+ return Array.from({ length }, () =>
183
+ chars.charAt(Math.floor(Math.random() * chars.length)),
184
+ ).join('');
185
+ }
186
+
100
187
  // Current timestamp variations
101
188
  if (varName === 'CURRENT_TIME' || varName === 'TIMESTAMP') {
102
189
  return Date.now().toString();
@@ -133,6 +220,46 @@ export class YamlParser {
133
220
  return format.replace('HH', hours).replace('mm', minutes).replace('ss', seconds);
134
221
  }
135
222
 
223
+ /**
224
+ * Extracts variable references from a string, properly handling nested braces.
225
+ * For example, "${VAR:${OTHER:default}}" is extracted as a single variable reference.
226
+ */
227
+ static extractVariables(str: string): Array<{ start: number; end: number; name: string }> {
228
+ const variables: Array<{ start: number; end: number; name: string }> = [];
229
+ let i = 0;
230
+
231
+ while (i < str.length) {
232
+ // Look for ${
233
+ if (str[i] === '$' && str[i + 1] === '{') {
234
+ const start = i;
235
+ i += 2; // Skip past ${
236
+ let braceCount = 1;
237
+ const nameStart = i;
238
+
239
+ // Find the matching closing brace
240
+ while (i < str.length && braceCount > 0) {
241
+ if (str[i] === '{') {
242
+ braceCount++;
243
+ } else if (str[i] === '}') {
244
+ braceCount--;
245
+ }
246
+ i++;
247
+ }
248
+
249
+ if (braceCount === 0) {
250
+ // Found matching closing brace
251
+ const name = str.slice(nameStart, i - 1); // Exclude the closing }
252
+ variables.push({ start, end: i, name });
253
+ }
254
+ // If braceCount > 0, we have unmatched braces - skip this variable
255
+ } else {
256
+ i++;
257
+ }
258
+ }
259
+
260
+ return variables;
261
+ }
262
+
136
263
  static mergeConfigs(base: Partial<RequestConfig>, override: RequestConfig): RequestConfig {
137
264
  return {
138
265
  ...base,
package/dist/cli.js DELETED
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env bun
2
- // @bun
3
- var x=Object.create;var{getPrototypeOf:y,defineProperty:q,getOwnPropertyNames:f}=Object;var p=Object.prototype.hasOwnProperty;var g=($,K,Q)=>{Q=$!=null?x(y($)):{};let Z=K||!$||!$.__esModule?q(Q,"default",{value:$,enumerable:!0}):Q;for(let X of f($))if(!p.call(Z,X))q(Z,X,{get:()=>$[X],enumerable:!0});return Z};var E=import.meta.require;var{Glob:u}=globalThis.Bun;var{YAML:N}=globalThis.Bun;class G{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=G.resolveVariable(X,K,Q);return z!==null?z:$}return $.replace(/\$\{([^}]+)\}/g,(X,z)=>{let W=G.resolveVariable(z,K,Q);return W!==null?W:X})}if(Array.isArray($))return $.map((Z)=>G.interpolateVariables(Z,K,Q));if($&&typeof $==="object"){let Z={};for(let[X,z]of Object.entries($))Z[X]=G.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=G.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 G.formatDate(new Date,K)}if($.startsWith("TIME:")){let K=$.slice(5);return G.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}}}}function d($){return typeof $==="object"&&$!==null&&"file"in $}function P($){return $.replace(/'/g,"'\\''")}class j{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($.formData)for(let[Z,X]of Object.entries($.formData))if(d(X)){let z=`@${X.file}`;if(X.filename)z+=`;filename=${X.filename}`;if(X.contentType)z+=`;type=${X.contentType}`;K.push("-F",`'${Z}=${P(z)}'`)}else{let z=String(X);K.push("-F",`'${Z}=${P(z)}'`)}else 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(J){}}let U={};if(z.response_code){let J=Z.split(`
4
- `).filter((w)=>w.includes(":"));for(let w of J){let[_,...H]=w.split(":");if(_&&H.length>0)U[_.trim()]=H.join(":").trim()}}return{success:!0,status:z.response_code||z.http_code,headers:U,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 L{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,U=W.split(`
5
- `);if(U.length===1)console.log(`${z} ${Q.label}: ${W}`);else{console.log(`${z} ${Q.label}:`);let J=X?`${K} `:`${K}\u2502 `;U.forEach((w)=>{console.log(`${J}${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 ","|")),U=this.color(z,"red");console.log(` ${this.color("\u2717","red")} ${this.color("Error:","red")} Expected ${this.color("status","yellow")} ${W}, got ${U}`)}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,U=this.colorStatusCode(z.replace(" or ","|")),J=this.color(W,"red");console.log(` ${this.color("\u2022","red")} ${this.color("status","yellow")}: expected ${U}, got ${J}`)}else{let z=Z.match(/^Expected (.+?) to be (.+?), got (.+)$/);if(z){let[,W,U,J]=z;console.log(` ${this.color("\u2022","red")} ${this.color(W,"yellow")}: expected ${this.color(U,"green")}, got ${this.color(J,"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 U=this.formatJson($.body);console.log(U)}return}if(this.config.format==="json"){let U={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(U,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 U=$.request.sourceFile?this.getShortFilename($.request.sourceFile):"inline";console.log(`${this.color(Z,Q)} ${this.color(X,"bright")} [${U}]`);let J=[],w=new L(this.colors);J.push({label:$.request.method||"GET",value:$.request.url,color:"blue"});let _=$.status?`${$.status}`:"ERROR";if(J.push({label:`${Z} Status`,value:_,color:Q}),$.metrics){let H=`${this.formatDuration($.metrics.duration)} | ${this.formatSize($.metrics.size)}`;J.push({label:"Duration",value:H,color:"cyan"})}if(w.render(J),$.error)console.log(),this.logValidationErrors($.error);console.log();return}console.log(`${this.color(Z,Q)} ${this.color(X,"bright")}`);let z=[],W=new L(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 U=Object.entries($.headers).map(([J,w])=>({label:this.color(J,"dim"),value:String(w)}));z.push({label:"Headers",children:U})}if(this.shouldShowBody()&&$.body){let J=this.formatJson($.body).split(`
6
- `),w=this.shouldShowRequestDetails()?1/0:10,_=J.slice(0,w);if(J.length>w)_.push(this.color(`... (${J.length-w} more lines)`,"dim"));z.push({label:"Response Body",value:_.join(`
7
- `)})}if(this.shouldShowMetrics()&&$.metrics&&K==="detailed"){let U=$.metrics,J=[];if(J.push({label:"Request Duration",value:this.formatDuration(U.duration),color:"cyan"}),U.size!==void 0)J.push({label:"Response Size",value:this.formatSize(U.size),color:"cyan"});if(U.dnsLookup)J.push({label:"DNS Lookup",value:this.formatDuration(U.dnsLookup),color:"cyan"});if(U.tcpConnection)J.push({label:"TCP Connection",value:this.formatDuration(U.tcpConnection),color:"cyan"});if(U.tlsHandshake)J.push({label:"TLS Handshake",value:this.formatDuration(U.tlsHandshake),color:"cyan"});if(U.firstByte)J.push({label:"Time to First Byte",value:this.formatDuration(U.firstByte),color:"cyan"});z.push({label:"Metrics",children:J})}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 U={summary:{total:$.total,successful:$.successful,failed:$.failed,duration:$.duration},results:$.results.map((J)=>({request:{name:J.request.name,url:J.request.url,method:J.request.method||"GET"},success:J.success,status:J.status,...this.shouldShowHeaders()&&J.headers?{headers:J.headers}:{},...this.shouldShowBody()&&J.body?{body:J.body}:{},...J.error?{error:J.error}:{},...this.shouldShowMetrics()&&J.metrics?{metrics:J.metrics}:{}}))};console.log(JSON.stringify(U,null,2));return}if(!this.shouldShowOutput())return;let Q=this.config.prettyLevel||"minimal";if(K)console.log();if(Q==="minimal"){let U=$.failed===0?"green":"red",J=$.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(J,U)}`);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((U)=>!U.success).forEach((U)=>{let J=U.request.name||U.request.url;console.log(` ${this.color("\u2022","red")} ${J}: ${U.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 m($,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,U]=z,J=Number.parseInt(U,10);if(Z=Z[W],Array.isArray(Z))Z=Z[J];else return}else if(/^\d+$/.test(X)&&Array.isArray(Z))Z=Z[Number.parseInt(X,10)];else Z=Z[X]}return Z}function c($){if($===void 0||$===null)return"";if(typeof $==="string")return $;if(typeof $==="number"||typeof $==="boolean")return String($);return JSON.stringify($)}function h($,K){let Q={},Z={status:$.status,headers:$.headers||{},body:$.body,metrics:$.metrics};for(let[X,z]of Object.entries(K)){let W=m(Z,z);Q[X]=c(W)}return Q}function b(){return{}}class S{logger;globalConfig;constructor($={}){this.globalConfig=$,this.logger=new M($.output)}mergeOutputConfig($){return{...this.globalConfig.output,...$.sourceOutputConfig}}isFileAttachment($){return typeof $==="object"&&$!==null&&"file"in $}async validateFileAttachments($){if(!$.formData)return;let K=[];for(let[Q,Z]of Object.entries($.formData))if(this.isFileAttachment(Z)){let X=Z.file;if(!await Bun.file(X).exists())K.push(`${Q}: ${X}`)}if(K.length>0)return`File(s) not found: ${K.join(", ")}`;return}async executeRequest($,K=0){let Q=performance.now(),Z=this.mergeOutputConfig($),X=new M(Z);X.logRequestStart($,K);let z=await this.validateFileAttachments($);if(z){let H={request:$,success:!1,error:z,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(H),H}let W=j.buildCommand($);X.logCommand(W);let U=0,J,w=($.retry?.count||0)+1;while(U<w){if(U>0){if(X.logRetry(U,w-1),$.retry?.delay)await Bun.sleep($.retry.delay)}let H=await j.executeCurl(W);if(H.success){let D=H.body;try{if(H.headers?.["content-type"]?.includes("application/json")||D&&(D.trim().startsWith("{")||D.trim().startsWith("[")))D=JSON.parse(D)}catch(T){}let I={request:$,success:!0,status:H.status,headers:H.headers,body:D,metrics:{...H.metrics,duration:performance.now()-Q}};if($.expect){let T=this.validateResponse(I,$.expect);if(!T.success)I.success=!1,I.error=T.error}return X.logRequestComplete(I),I}J=H.error,U++}let _={request:$,success:!1,error:J,metrics:{duration:performance.now()-Q}};return X.logRequestComplete(_),_}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,U;if(Array.isArray($)&&this.isArraySelector(X))U=this.getArrayValue($,X);else U=$?.[X];if(typeof z==="object"&&z!==null&&!Array.isArray(z)){let J=this.validateBodyProperties(U,z,W);Z.push(...J)}else{let J=this.validateValue(U,z,W);if(!J.isValid)Z.push(J.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 U=W[1],J=Number(W[2]);switch(U){case">":return Q>J;case">=":return Q>=J;case"<":return Q<J;case"<=":return Q<=J;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=b();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 U=h(W,z.store);Object.assign(Z,U),this.logStoredValues(U)}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 G.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 B(){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 C=`${process.env.HOME}/.curl-runner-version-cache.json`,i=86400000,n="https://registry.npmjs.org/@curl-runner/cli/latest";class R{async checkForUpdates($=!1){try{if(process.env.CI)return;let K=B();if(K==="0.0.0")return;if(!$){let Z=await this.getCachedVersion();if(Z&&Date.now()-Z.lastCheck<i){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(n,{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 U=X[W]||0,J=z[W]||0;if(J>U)return!0;if(J<U)return!1}return!1}catch{return!1}}async getCachedVersion(){try{let $=Bun.file(C);if(await $.exists())return JSON.parse(await $.text())}catch{}return null}async setCachedVersion($){try{let K={lastCheck:Date.now(),latestVersion:$};await Bun.write(C,JSON.stringify(K))}catch{}}}class v{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 G.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${B()}`);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),U=[],J=[];for(let D of z){this.logger.logInfo(`Processing: ${D}`);let{requests:I,config:T}=await this.processYamlFile(D),A=T?.output||{},F=I.map((Y)=>({...Y,sourceOutputConfig:A,sourceFile:D}));if(T){let{...Y}=T;W=this.mergeGlobalConfigs(W,Y)}J.push({file:D,requests:F,config:T}),U.push(...F)}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 D=Q.noRetry?0:Q.retries||0;W.defaults={...W.defaults,retry:{...W.defaults?.retry,count:D}}}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(U.length===0)this.logger.logError("No requests found in YAML files"),process.exit(1);let w=new S(W),_;if(J.length>1){let D=[],I=0;for(let F=0;F<J.length;F++){let Y=J[F];this.logger.logFileHeader(Y.file,Y.requests.length);let k=await w.execute(Y.requests);if(D.push(...k.results),I+=k.duration,F<J.length-1)console.log()}let T=D.filter((F)=>F.success).length,A=D.filter((F)=>!F.success).length;_={total:D.length,successful:T,failed:A,duration:I,results:D},w.logger.logSummary(_,!0)}else _=await w.execute(U);let H=this.determineExitCode(_,W);process.exit(H)}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 U=Number.parseFloat(W);if(U>=0&&U<=100)K.failOnPercentage=U}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 U=$[Z+1];if(U&&!U.startsWith("-"))K.output=U,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 u(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 G.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 U=this.prepareRequest(W,X,z);Q.push(U)}if(K.collection?.requests)for(let W of K.collection.requests){let U=this.prepareRequest(W,X,z);Q.push(U)}return{requests:Q,config:Z}}prepareRequest($,K,Q){let Z=G.interpolateVariables($,K);return G.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 l=new v;l.run(process.argv.slice(2));
100
-
101
- //# debugId=EA3EA83C7A39EBEE64756E2164756E21
102
- //# sourceMappingURL=cli.js.map