@curl-runner/cli 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +3 -2
  2. package/src/ci-exit.test.ts +0 -215
  3. package/src/cli.ts +0 -1326
  4. package/src/diff/baseline-manager.test.ts +0 -181
  5. package/src/diff/baseline-manager.ts +0 -266
  6. package/src/diff/diff-formatter.ts +0 -316
  7. package/src/diff/index.ts +0 -3
  8. package/src/diff/response-differ.test.ts +0 -330
  9. package/src/diff/response-differ.ts +0 -489
  10. package/src/executor/max-concurrency.test.ts +0 -139
  11. package/src/executor/profile-executor.test.ts +0 -132
  12. package/src/executor/profile-executor.ts +0 -167
  13. package/src/executor/request-executor.ts +0 -663
  14. package/src/parser/yaml.test.ts +0 -480
  15. package/src/parser/yaml.ts +0 -272
  16. package/src/snapshot/index.ts +0 -3
  17. package/src/snapshot/snapshot-differ.test.ts +0 -358
  18. package/src/snapshot/snapshot-differ.ts +0 -296
  19. package/src/snapshot/snapshot-formatter.ts +0 -170
  20. package/src/snapshot/snapshot-manager.test.ts +0 -204
  21. package/src/snapshot/snapshot-manager.ts +0 -342
  22. package/src/types/config.ts +0 -638
  23. package/src/utils/colors.ts +0 -30
  24. package/src/utils/condition-evaluator.test.ts +0 -415
  25. package/src/utils/condition-evaluator.ts +0 -327
  26. package/src/utils/curl-builder.test.ts +0 -165
  27. package/src/utils/curl-builder.ts +0 -201
  28. package/src/utils/logger.ts +0 -856
  29. package/src/utils/response-store.test.ts +0 -213
  30. package/src/utils/response-store.ts +0 -108
  31. package/src/utils/stats.test.ts +0 -161
  32. package/src/utils/stats.ts +0 -151
  33. package/src/utils/version-checker.ts +0 -165
  34. package/src/version.ts +0 -43
  35. package/src/watcher/file-watcher.test.ts +0 -186
  36. package/src/watcher/file-watcher.ts +0 -140
@@ -1,272 +0,0 @@
1
- import { YAML } from 'bun';
2
- import type { RequestConfig, ResponseStoreContext, YamlFile } from '../types/config';
3
-
4
- // Using class for organization, but could be refactored to functions
5
- export class YamlParser {
6
- static async parseFile(filepath: string): Promise<YamlFile> {
7
- const file = Bun.file(filepath);
8
- const content = await file.text();
9
- return YAML.parse(content) as YamlFile;
10
- }
11
-
12
- static parse(content: string): YamlFile {
13
- return YAML.parse(content) as YamlFile;
14
- }
15
-
16
- /**
17
- * Interpolates variables in an object, supporting:
18
- * - Static variables: ${VAR_NAME}
19
- * - Dynamic variables: ${UUID}, ${TIMESTAMP}, ${DATE:format}, ${TIME:format}
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'
23
- *
24
- * @param obj - The object to interpolate
25
- * @param variables - Static variables map
26
- * @param storeContext - Optional stored response values from previous requests
27
- */
28
- static interpolateVariables(
29
- obj: unknown,
30
- variables: Record<string, string>,
31
- storeContext?: ResponseStoreContext,
32
- ): unknown {
33
- if (typeof obj === 'string') {
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;
48
- const resolvedValue = YamlParser.resolveVariable(varName, variables, storeContext);
49
- return resolvedValue !== null ? resolvedValue : obj;
50
- }
51
-
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;
63
- }
64
-
65
- if (Array.isArray(obj)) {
66
- return obj.map((item) => YamlParser.interpolateVariables(item, variables, storeContext));
67
- }
68
-
69
- if (obj && typeof obj === 'object') {
70
- const result: Record<string, unknown> = {};
71
- for (const [key, value] of Object.entries(obj)) {
72
- result[key] = YamlParser.interpolateVariables(value, variables, storeContext);
73
- }
74
- return result;
75
- }
76
-
77
- return obj;
78
- }
79
-
80
- /**
81
- * Resolves a single variable reference.
82
- * Priority: store context > string transforms > dynamic variables > static variables > default values
83
- */
84
- static resolveVariable(
85
- varName: string,
86
- variables: Record<string, string>,
87
- storeContext?: ResponseStoreContext,
88
- ): string | null {
89
- // Check for store variable (${store.variableName})
90
- if (varName.startsWith('store.') && storeContext) {
91
- const storeVarName = varName.slice(6); // Remove 'store.' prefix
92
- if (storeVarName in storeContext) {
93
- return storeContext[storeVarName];
94
- }
95
- return null; // Store variable not found, return null to keep original
96
- }
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
-
144
- // Check for dynamic variable
145
- const dynamicValue = YamlParser.resolveDynamicVariable(varName);
146
- if (dynamicValue !== null) {
147
- return dynamicValue;
148
- }
149
-
150
- // Check for static variable
151
- if (varName in variables) {
152
- return variables[varName];
153
- }
154
-
155
- return null;
156
- }
157
-
158
- static resolveDynamicVariable(varName: string): string | null {
159
- // UUID generation
160
- if (varName === 'UUID') {
161
- return crypto.randomUUID();
162
- }
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
-
187
- // Current timestamp variations
188
- if (varName === 'CURRENT_TIME' || varName === 'TIMESTAMP') {
189
- return Date.now().toString();
190
- }
191
-
192
- // Date formatting - ${DATE:YYYY-MM-DD}
193
- if (varName.startsWith('DATE:')) {
194
- const format = varName.slice(5); // Remove 'DATE:'
195
- return YamlParser.formatDate(new Date(), format);
196
- }
197
-
198
- // Time formatting - ${TIME:HH:mm:ss}
199
- if (varName.startsWith('TIME:')) {
200
- const format = varName.slice(5); // Remove 'TIME:'
201
- return YamlParser.formatTime(new Date(), format);
202
- }
203
-
204
- return null; // Not a dynamic variable
205
- }
206
-
207
- static formatDate(date: Date, format: string): string {
208
- const year = date.getFullYear();
209
- const month = String(date.getMonth() + 1).padStart(2, '0');
210
- const day = String(date.getDate()).padStart(2, '0');
211
-
212
- return format.replace('YYYY', year.toString()).replace('MM', month).replace('DD', day);
213
- }
214
-
215
- static formatTime(date: Date, format: string): string {
216
- const hours = String(date.getHours()).padStart(2, '0');
217
- const minutes = String(date.getMinutes()).padStart(2, '0');
218
- const seconds = String(date.getSeconds()).padStart(2, '0');
219
-
220
- return format.replace('HH', hours).replace('mm', minutes).replace('ss', seconds);
221
- }
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
-
263
- static mergeConfigs(base: Partial<RequestConfig>, override: RequestConfig): RequestConfig {
264
- return {
265
- ...base,
266
- ...override,
267
- headers: { ...base.headers, ...override.headers },
268
- params: { ...base.params, ...override.params },
269
- variables: { ...base.variables, ...override.variables },
270
- };
271
- }
272
- }
@@ -1,3 +0,0 @@
1
- export { SnapshotDiffer } from './snapshot-differ';
2
- export { SnapshotFormatter, type SnapshotStats } from './snapshot-formatter';
3
- export { filterSnapshotBody, SnapshotManager } from './snapshot-manager';
@@ -1,358 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import type { Snapshot } from '../types/config';
3
- import { SnapshotDiffer } from './snapshot-differ';
4
-
5
- describe('SnapshotDiffer', () => {
6
- describe('basic comparison', () => {
7
- test('should match identical snapshots', () => {
8
- const differ = new SnapshotDiffer({});
9
- const snapshot: Snapshot = {
10
- status: 200,
11
- body: { id: 1, name: 'test' },
12
- hash: 'abc123',
13
- updatedAt: '2024-01-01',
14
- };
15
-
16
- const result = differ.compare(snapshot, snapshot);
17
- expect(result.match).toBe(true);
18
- expect(result.differences).toHaveLength(0);
19
- });
20
-
21
- test('should detect status changes', () => {
22
- const differ = new SnapshotDiffer({});
23
- const expected: Snapshot = { status: 200, hash: 'a', updatedAt: '' };
24
- const received: Snapshot = { status: 201, hash: 'b', updatedAt: '' };
25
-
26
- const result = differ.compare(expected, received);
27
- expect(result.match).toBe(false);
28
- expect(result.differences).toHaveLength(1);
29
- expect(result.differences[0]).toEqual({
30
- path: 'status',
31
- expected: 200,
32
- received: 201,
33
- type: 'changed',
34
- });
35
- });
36
-
37
- test('should detect body value changes', () => {
38
- const differ = new SnapshotDiffer({});
39
- const expected: Snapshot = {
40
- body: { name: 'old' },
41
- hash: 'a',
42
- updatedAt: '',
43
- };
44
- const received: Snapshot = {
45
- body: { name: 'new' },
46
- hash: 'b',
47
- updatedAt: '',
48
- };
49
-
50
- const result = differ.compare(expected, received);
51
- expect(result.match).toBe(false);
52
- expect(result.differences[0].path).toBe('body.name');
53
- expect(result.differences[0].type).toBe('changed');
54
- });
55
-
56
- test('should detect added fields', () => {
57
- const differ = new SnapshotDiffer({});
58
- const expected: Snapshot = {
59
- body: { id: 1 },
60
- hash: 'a',
61
- updatedAt: '',
62
- };
63
- const received: Snapshot = {
64
- body: { id: 1, newField: 'value' },
65
- hash: 'b',
66
- updatedAt: '',
67
- };
68
-
69
- const result = differ.compare(expected, received);
70
- expect(result.match).toBe(false);
71
- expect(result.differences[0].path).toBe('body.newField');
72
- expect(result.differences[0].type).toBe('added');
73
- });
74
-
75
- test('should detect removed fields', () => {
76
- const differ = new SnapshotDiffer({});
77
- const expected: Snapshot = {
78
- body: { id: 1, oldField: 'value' },
79
- hash: 'a',
80
- updatedAt: '',
81
- };
82
- const received: Snapshot = {
83
- body: { id: 1 },
84
- hash: 'b',
85
- updatedAt: '',
86
- };
87
-
88
- const result = differ.compare(expected, received);
89
- expect(result.match).toBe(false);
90
- expect(result.differences[0].path).toBe('body.oldField');
91
- expect(result.differences[0].type).toBe('removed');
92
- });
93
- });
94
-
95
- describe('array comparison', () => {
96
- test('should compare array elements', () => {
97
- const differ = new SnapshotDiffer({});
98
- const expected: Snapshot = {
99
- body: { items: [1, 2, 3] },
100
- hash: 'a',
101
- updatedAt: '',
102
- };
103
- const received: Snapshot = {
104
- body: { items: [1, 2, 4] },
105
- hash: 'b',
106
- updatedAt: '',
107
- };
108
-
109
- const result = differ.compare(expected, received);
110
- expect(result.match).toBe(false);
111
- expect(result.differences[0].path).toBe('body.items[2]');
112
- });
113
-
114
- test('should detect added array elements', () => {
115
- const differ = new SnapshotDiffer({});
116
- const expected: Snapshot = {
117
- body: { items: [1, 2] },
118
- hash: 'a',
119
- updatedAt: '',
120
- };
121
- const received: Snapshot = {
122
- body: { items: [1, 2, 3] },
123
- hash: 'b',
124
- updatedAt: '',
125
- };
126
-
127
- const result = differ.compare(expected, received);
128
- expect(result.match).toBe(false);
129
- expect(result.differences[0].path).toBe('body.items[2]');
130
- expect(result.differences[0].type).toBe('added');
131
- });
132
- });
133
-
134
- describe('exclusions', () => {
135
- test('should exclude exact paths', () => {
136
- const differ = new SnapshotDiffer({
137
- exclude: ['body.timestamp'],
138
- });
139
- const expected: Snapshot = {
140
- body: { id: 1, timestamp: '2024-01-01' },
141
- hash: 'a',
142
- updatedAt: '',
143
- };
144
- const received: Snapshot = {
145
- body: { id: 1, timestamp: '2024-12-31' },
146
- hash: 'b',
147
- updatedAt: '',
148
- };
149
-
150
- const result = differ.compare(expected, received);
151
- expect(result.match).toBe(true);
152
- });
153
-
154
- test('should exclude wildcard paths (*.field)', () => {
155
- const differ = new SnapshotDiffer({
156
- exclude: ['*.createdAt'],
157
- });
158
- const expected: Snapshot = {
159
- body: { user: { createdAt: '2024-01-01' }, post: { createdAt: '2024-01-01' } },
160
- hash: 'a',
161
- updatedAt: '',
162
- };
163
- const received: Snapshot = {
164
- body: { user: { createdAt: '2024-12-31' }, post: { createdAt: '2024-12-31' } },
165
- hash: 'b',
166
- updatedAt: '',
167
- };
168
-
169
- const result = differ.compare(expected, received);
170
- expect(result.match).toBe(true);
171
- });
172
-
173
- test('should exclude array wildcard paths (body[*].id)', () => {
174
- const differ = new SnapshotDiffer({
175
- exclude: ['body.items[*].id'],
176
- });
177
- const expected: Snapshot = {
178
- body: {
179
- items: [
180
- { id: 1, name: 'a' },
181
- { id: 2, name: 'b' },
182
- ],
183
- },
184
- hash: 'a',
185
- updatedAt: '',
186
- };
187
- const received: Snapshot = {
188
- body: {
189
- items: [
190
- { id: 99, name: 'a' },
191
- { id: 100, name: 'b' },
192
- ],
193
- },
194
- hash: 'b',
195
- updatedAt: '',
196
- };
197
-
198
- const result = differ.compare(expected, received);
199
- expect(result.match).toBe(true);
200
- });
201
- });
202
-
203
- describe('match rules', () => {
204
- test('should accept any value with wildcard (*)', () => {
205
- const differ = new SnapshotDiffer({
206
- match: { 'body.id': '*' },
207
- });
208
- const expected: Snapshot = {
209
- body: { id: 1 },
210
- hash: 'a',
211
- updatedAt: '',
212
- };
213
- const received: Snapshot = {
214
- body: { id: 999 },
215
- hash: 'b',
216
- updatedAt: '',
217
- };
218
-
219
- const result = differ.compare(expected, received);
220
- expect(result.match).toBe(true);
221
- });
222
-
223
- test('should match regex patterns', () => {
224
- const differ = new SnapshotDiffer({
225
- match: { 'body.version': 'regex:^v\\d+\\.\\d+' },
226
- });
227
- const expected: Snapshot = {
228
- body: { version: 'v1.0.0' },
229
- hash: 'a',
230
- updatedAt: '',
231
- };
232
- const received: Snapshot = {
233
- body: { version: 'v2.5.3' },
234
- hash: 'b',
235
- updatedAt: '',
236
- };
237
-
238
- const result = differ.compare(expected, received);
239
- expect(result.match).toBe(true);
240
- });
241
-
242
- test('should fail on non-matching regex', () => {
243
- const differ = new SnapshotDiffer({
244
- match: { 'body.version': 'regex:^v\\d+\\.\\d+' },
245
- });
246
- const expected: Snapshot = {
247
- body: { version: 'v1.0' },
248
- hash: 'a',
249
- updatedAt: '',
250
- };
251
- const received: Snapshot = {
252
- body: { version: 'invalid' },
253
- hash: 'b',
254
- updatedAt: '',
255
- };
256
-
257
- const result = differ.compare(expected, received);
258
- expect(result.match).toBe(false);
259
- });
260
- });
261
-
262
- describe('type mismatches', () => {
263
- test('should detect type changes', () => {
264
- const differ = new SnapshotDiffer({});
265
- const expected: Snapshot = {
266
- body: { count: 42 },
267
- hash: 'a',
268
- updatedAt: '',
269
- };
270
- const received: Snapshot = {
271
- body: { count: '42' },
272
- hash: 'b',
273
- updatedAt: '',
274
- };
275
-
276
- const result = differ.compare(expected, received);
277
- expect(result.match).toBe(false);
278
- expect(result.differences[0].type).toBe('type_mismatch');
279
- });
280
-
281
- test('should detect object to array change', () => {
282
- const differ = new SnapshotDiffer({});
283
- const expected: Snapshot = {
284
- body: { data: { key: 'value' } },
285
- hash: 'a',
286
- updatedAt: '',
287
- };
288
- const received: Snapshot = {
289
- body: { data: ['value'] },
290
- hash: 'b',
291
- updatedAt: '',
292
- };
293
-
294
- const result = differ.compare(expected, received);
295
- expect(result.match).toBe(false);
296
- expect(result.differences[0].type).toBe('type_mismatch');
297
- });
298
- });
299
-
300
- describe('nested objects', () => {
301
- test('should compare deeply nested values', () => {
302
- const differ = new SnapshotDiffer({});
303
- const expected: Snapshot = {
304
- body: { level1: { level2: { level3: { value: 'old' } } } },
305
- hash: 'a',
306
- updatedAt: '',
307
- };
308
- const received: Snapshot = {
309
- body: { level1: { level2: { level3: { value: 'new' } } } },
310
- hash: 'b',
311
- updatedAt: '',
312
- };
313
-
314
- const result = differ.compare(expected, received);
315
- expect(result.match).toBe(false);
316
- expect(result.differences[0].path).toBe('body.level1.level2.level3.value');
317
- });
318
- });
319
-
320
- describe('header comparison', () => {
321
- test('should compare headers', () => {
322
- const differ = new SnapshotDiffer({});
323
- const expected: Snapshot = {
324
- headers: { 'content-type': 'application/json' },
325
- hash: 'a',
326
- updatedAt: '',
327
- };
328
- const received: Snapshot = {
329
- headers: { 'content-type': 'text/html' },
330
- hash: 'b',
331
- updatedAt: '',
332
- };
333
-
334
- const result = differ.compare(expected, received);
335
- expect(result.match).toBe(false);
336
- expect(result.differences[0].path).toBe('headers.content-type');
337
- });
338
-
339
- test('should exclude headers', () => {
340
- const differ = new SnapshotDiffer({
341
- exclude: ['headers.date', 'headers.x-request-id'],
342
- });
343
- const expected: Snapshot = {
344
- headers: { 'content-type': 'application/json', date: '2024-01-01' },
345
- hash: 'a',
346
- updatedAt: '',
347
- };
348
- const received: Snapshot = {
349
- headers: { 'content-type': 'application/json', date: '2024-12-31' },
350
- hash: 'b',
351
- updatedAt: '',
352
- };
353
-
354
- const result = differ.compare(expected, received);
355
- expect(result.match).toBe(true);
356
- });
357
- });
358
- });