@apiquest/fracture 1.0.4 → 1.0.5

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 (59) hide show
  1. package/README.md +90 -2
  2. package/dist/CollectionRunner.d.ts +2 -0
  3. package/dist/CollectionRunner.d.ts.map +1 -1
  4. package/dist/CollectionRunner.js +20 -2
  5. package/dist/CollectionRunner.js.map +1 -1
  6. package/dist/LibraryLoader.d.ts +49 -0
  7. package/dist/LibraryLoader.d.ts.map +1 -0
  8. package/dist/LibraryLoader.js +198 -0
  9. package/dist/LibraryLoader.js.map +1 -0
  10. package/dist/PluginLoader.d.ts.map +1 -1
  11. package/dist/PluginLoader.js +9 -6
  12. package/dist/PluginLoader.js.map +1 -1
  13. package/dist/PluginResolver.d.ts +1 -1
  14. package/dist/PluginResolver.d.ts.map +1 -1
  15. package/dist/PluginResolver.js +1 -1
  16. package/dist/PluginResolver.js.map +1 -1
  17. package/dist/ScriptEngine.d.ts +2 -1
  18. package/dist/ScriptEngine.d.ts.map +1 -1
  19. package/dist/ScriptEngine.js +15 -8
  20. package/dist/ScriptEngine.js.map +1 -1
  21. package/dist/cli/index.js +35 -3
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/plugin-commands.d.ts.map +1 -1
  24. package/dist/cli/plugin-commands.js +47 -81
  25. package/dist/cli/plugin-commands.js.map +1 -1
  26. package/dist/cli/plugin-installer.d.ts +48 -0
  27. package/dist/cli/plugin-installer.d.ts.map +1 -0
  28. package/dist/cli/plugin-installer.js +136 -0
  29. package/dist/cli/plugin-installer.js.map +1 -0
  30. package/dist/cli/plugin-registry.d.ts +17 -0
  31. package/dist/cli/plugin-registry.d.ts.map +1 -0
  32. package/dist/cli/plugin-registry.js +77 -0
  33. package/dist/cli/plugin-registry.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/CollectionAnalyzer.ts +0 -102
  36. package/src/CollectionRunner.ts +0 -1423
  37. package/src/CollectionRunner.types.ts +0 -9
  38. package/src/CollectionValidator.ts +0 -289
  39. package/src/ConsoleReporter.ts +0 -143
  40. package/src/CookieJar.ts +0 -258
  41. package/src/DagScheduler.ts +0 -439
  42. package/src/Logger.ts +0 -85
  43. package/src/PluginLoader.ts +0 -126
  44. package/src/PluginManager.ts +0 -208
  45. package/src/PluginResolver.ts +0 -154
  46. package/src/QuestAPI.ts +0 -764
  47. package/src/QuestAPI.types.ts +0 -33
  48. package/src/QuestTestAPI.ts +0 -164
  49. package/src/RequestFilter.ts +0 -224
  50. package/src/ScriptEngine.ts +0 -219
  51. package/src/ScriptValidator.ts +0 -428
  52. package/src/TaskGraph.ts +0 -598
  53. package/src/TestCounter.ts +0 -109
  54. package/src/VariableResolver.ts +0 -114
  55. package/src/cli/index.ts +0 -480
  56. package/src/cli/plugin-commands.ts +0 -342
  57. package/src/cli/plugin-discovery.ts +0 -44
  58. package/src/index.ts +0 -24
  59. package/src/utils.ts +0 -52
@@ -1,114 +0,0 @@
1
- import type { ExecutionContext, IterationData } from '@apiquest/types';
2
- import { extractValue, isNullOrEmpty } from './utils.js';
3
- import { Logger } from './Logger.js';
4
-
5
- export class VariableResolver {
6
- private logger: Logger;
7
-
8
- constructor(baseLogger?: Logger) {
9
- this.logger = baseLogger?.createLogger('VariableResolver') ?? new Logger('VariableResolver');
10
- }
11
-
12
- /**
13
- * Resolve {{variables}} in a template string
14
- * Priority: iteration data > local > collection > environment > global
15
- */
16
- resolve(template: string, context: ExecutionContext): string {
17
- if (isNullOrEmpty(template) || typeof template !== 'string') {
18
- return template;
19
- }
20
-
21
- // Find all {{variable}} patterns
22
- const variableCount = (template.match(/\{\{([^}]+)\}\}/g) ?? []).length;
23
- if (variableCount > 0) {
24
- this.logger.trace(`Resolving ${variableCount} variable(s) in template`);
25
- }
26
-
27
- return template.replace(/\{\{([^}]+)\}\}/g, (match: string, varName: string) => {
28
- const trimmedName = varName.trim();
29
- const value = this.getVariable(trimmedName, context);
30
-
31
- if (value !== null && value !== undefined) {
32
- this.logger.trace(`Resolved {{${trimmedName}}} -> ${typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value}`);
33
- return String(value);
34
- } else {
35
- this.logger.trace(`Variable {{${trimmedName}}} not found, keeping placeholder`);
36
- return match;
37
- }
38
- });
39
- }
40
-
41
- /**
42
- * Resolve all values in an object (recursively)
43
- */
44
- resolveAll(obj: unknown, context: ExecutionContext): unknown {
45
- if (obj === null || obj === undefined) {
46
- return obj;
47
- }
48
-
49
- if (typeof obj === 'string') {
50
- return this.resolve(obj, context);
51
- }
52
-
53
- if (Array.isArray(obj)) {
54
- return obj.map(item => this.resolveAll(item, context));
55
- }
56
-
57
- if (typeof obj === 'object') {
58
- const resolved: Record<string, unknown> = {};
59
- for (const [key, value] of Object.entries(obj)) {
60
- resolved[key] = this.resolveAll(value, context);
61
- }
62
- return resolved;
63
- }
64
-
65
- return obj;
66
- }
67
-
68
- /**
69
- * Get variable value with cascading priority
70
- * Priority: iteration data > scope stack (innermost to outermost) > collection > environment > global
71
- */
72
- private getVariable(name: string, context: ExecutionContext): unknown {
73
- // 1. Iteration data (highest priority)
74
- const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
75
- if (currentIterationData !== null && currentIterationData !== undefined && name in currentIterationData) {
76
- this.logger.trace(`Variable '${name}' found in iteration data`);
77
- return currentIterationData[name];
78
- }
79
-
80
- // 2. Scope stack (hierarchical scope variables - search from innermost to outermost)
81
- // This represents quest.scope.variables which flows through the script inheritance chain
82
- if (context.scopeStack !== null && context.scopeStack !== undefined && context.scopeStack.length > 0) {
83
- // Search from top of stack (most specific) to bottom (least specific)
84
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
85
- const frame = context.scopeStack[i];
86
- if (name in frame.vars) {
87
- this.logger.trace(`Variable '${name}' found in scope stack (frame ${context.scopeStack.length - 1 - i})`);
88
- return frame.vars[name];
89
- }
90
- }
91
- }
92
-
93
- // 3. Collection variables
94
- if (name in context.collectionVariables) {
95
- this.logger.trace(`Variable '${name}' found in collection variables`);
96
- return extractValue(context.collectionVariables[name]);
97
- }
98
-
99
- // 4. Environment variables
100
- if (context.environment !== null && context.environment !== undefined && name in context.environment.variables) {
101
- this.logger.trace(`Variable '${name}' found in environment variables`);
102
- return extractValue(context.environment.variables[name]);
103
- }
104
-
105
- // 5. Global variables (lowest priority)
106
- if (name in context.globalVariables) {
107
- this.logger.trace(`Variable '${name}' found in global variables`);
108
- return extractValue(context.globalVariables[name]);
109
- }
110
-
111
- this.logger.trace(`Variable '${name}' not found in any scope`);
112
- return null;
113
- }
114
- }
package/src/cli/index.ts DELETED
@@ -1,480 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import { readFileSync } from 'fs';
5
- import { CollectionRunner } from '../CollectionRunner.js';
6
- import { ConsoleReporter } from '../ConsoleReporter.js';
7
- import type { Collection, Environment, IterationData, RuntimeOptions, EventPayloads, ValidationResult } from '@apiquest/types';
8
- import { LogLevel } from '@apiquest/types';
9
- import { getPluginDirectories } from './plugin-discovery.js';
10
- import { addPluginCommands } from './plugin-commands.js';
11
-
12
- interface CLIOptions {
13
- config?: string;
14
- logLevel?: string;
15
- environment?: string;
16
- envVar?: Record<string, string>;
17
- global?: Record<string, string>;
18
- data?: string;
19
- iterations?: number;
20
- filter?: string;
21
- excludeDeps?: boolean;
22
- parallel?: boolean;
23
- concurrency?: number;
24
- bail?: boolean;
25
- delay?: number;
26
- timeout?: number;
27
- sslCert?: string;
28
- sslKey?: string;
29
- sslKeyPassphrase?: string;
30
- sslCa?: string;
31
- insecure?: boolean;
32
- proxy?: string;
33
- proxyAuth?: string;
34
- noProxy?: string;
35
- followRedirects?: boolean;
36
- maxRedirects?: number;
37
- cookie?: string | string[];
38
- cookieJar?: boolean;
39
- cookieJarPersist?: boolean;
40
- silent?: boolean;
41
- color?: boolean;
42
- strictMode?: boolean;
43
- reporters?: string;
44
- out?: string;
45
- pluginsDir?: string[];
46
- }
47
-
48
- /**
49
- * Load configuration from a JSON file
50
- */
51
- function loadConfigFile(configPath: string): Partial<CLIOptions> {
52
- try {
53
- const configContent = readFileSync(configPath, 'utf-8');
54
- return JSON.parse(configContent) as Partial<CLIOptions>;
55
- } catch (error) {
56
- console.error(`Error loading config file '${configPath}':`, error instanceof Error ? error.message : String(error));
57
- process.exit(4);
58
- }
59
- }
60
-
61
- /**
62
- * Merge config file options with CLI options
63
- * CLI options take precedence over config file options
64
- */
65
- function mergeOptions(configOptions: Partial<CLIOptions>, cliOptions: CLIOptions): CLIOptions {
66
- // Create a merged options object
67
- // CLI options override config file options
68
- const merged: CLIOptions = { ...configOptions, ...cliOptions };
69
-
70
- // Special handling for objects that should be merged rather than replaced
71
- if (configOptions.envVar !== undefined && cliOptions.envVar !== undefined) {
72
- merged.envVar = { ...configOptions.envVar, ...cliOptions.envVar };
73
- }
74
-
75
- if (configOptions.global !== undefined && cliOptions.global !== undefined) {
76
- merged.global = { ...configOptions.global, ...cliOptions.global };
77
- }
78
-
79
- // Arrays that should be merged
80
- if (configOptions.cookie !== undefined && cliOptions.cookie !== undefined) {
81
- const configCookies = Array.isArray(configOptions.cookie) ? configOptions.cookie : [configOptions.cookie];
82
- const cliCookies = Array.isArray(cliOptions.cookie) ? cliOptions.cookie : [cliOptions.cookie];
83
- merged.cookie = [...configCookies, ...cliCookies];
84
- }
85
-
86
- return merged;
87
- }
88
-
89
- const program = new Command();
90
-
91
- // Get command name from process.argv[1] (the bin script name)
92
- const commandName = process.argv[1]?.split(/[\\/]/).pop()?.replace('.js', '') ?? 'fracture';
93
-
94
- program
95
- .name(commandName)
96
- .description('ApiQuest/Fracture - API testing tool')
97
- .version('1.0.0');
98
-
99
- // Add plugin management commands
100
- addPluginCommands(program);
101
-
102
- program
103
- .command('run')
104
- .description('Run a collection')
105
- .argument('<collection>', 'Path to collection JSON file')
106
- // Variables & Environment
107
- .option('-g, --global <key=value...>', 'Set global variable (repeatable)', collectKeyValue, {} as Record<string, string>)
108
- .option('-e, --environment <file>', 'Environment JSON file')
109
- .option('--env-var <key=value...>', 'Set environment variable (repeatable)', collectKeyValue, {} as Record<string, string>)
110
- // Data & Iterations
111
- .option('-d, --data <file>', 'Iteration data file (CSV/JSON)')
112
- .option('-n, --iterations <count>', 'Limit number of iterations', parseInt)
113
- // Filtering & Selection
114
- .option('--filter <pattern>', 'Filter requests by path using regex pattern')
115
- .option('--exclude-deps', 'Exclude dependencies when filtering')
116
- // Execution Control
117
- .option('--parallel', 'Enable parallel execution')
118
- .option('--concurrency <number>', 'Max concurrent requests', parseInt)
119
- .option('--bail', 'Stop on first test failure')
120
- .option('--delay <ms>', 'Delay between requests in milliseconds', parseInt)
121
- // Timeouts
122
- .option('--timeout <ms>', 'Request timeout in milliseconds', parseInt)
123
- // SSL/TLS
124
- .option('--ssl-cert <path>', 'Client certificate file (PEM format)')
125
- .option('--ssl-key <path>', 'Client private key file')
126
- .option('--ssl-key-passphrase <password>', 'Client key passphrase')
127
- .option('--ssl-ca <path>', 'CA certificate bundle')
128
- .option('--insecure', 'Disable SSL certificate validation')
129
- // Proxy
130
- .option('--proxy <url>', 'HTTP/HTTPS proxy URL (http://host:port)')
131
- .option('--proxy-auth <user:pass>', 'Proxy authentication credentials')
132
- .option('--no-proxy <hosts>', 'Bypass proxy for hosts (comma-separated)')
133
- // Redirects
134
- .option('--follow-redirects', 'Follow HTTP redirects (default: true)')
135
- .option('--no-follow-redirects', 'Don\'t follow HTTP redirects')
136
- .option('--max-redirects <count>', 'Maximum redirects to follow (default: 20)', parseInt)
137
- // Cookies
138
- .option('--cookie <name=value>', 'Set cookie for requests (repeatable)')
139
- .option('--cookie-jar', 'Enable persistent cookie jar')
140
- .option('--cookie-jar-persist', 'Persist cookies across runs')
141
- // Output & Reporting
142
- .option('-r, --reporters <types>', 'Output reporters (comma-separated)', 'cli')
143
- .option('-o, --out <directory>', 'Output directory for reports')
144
- .option('--no-color', 'Disable colored output')
145
- .option('--silent', 'Suppress console output')
146
- .option('--log-level <level>', 'Log level: error, warn, info, debug, trace (default: info)')
147
- // Validation & Testing
148
- .option('--no-strict-mode', 'Disable strict validation mode')
149
- // Plugins
150
- .option('--plugin-dir <path>', 'Plugin directory to scan (repeatable, appended to auto-discovered paths)', collectArray, [] as string[])
151
- // Configuration
152
- .option('--config <file>', 'Load options from config file')
153
- .action(async (collectionPath: string, cliOptions: CLIOptions) => {
154
- // Load and merge config file if specified
155
- let options = cliOptions;
156
- if (cliOptions.config !== undefined) {
157
- const configOptions = loadConfigFile(cliOptions.config);
158
- options = mergeOptions(configOptions, cliOptions);
159
- }
160
-
161
- // Validate log level if provided
162
- const validLogLevels = ['error', 'warn', 'info', 'debug', 'trace'];
163
- if (options.logLevel !== undefined && !validLogLevels.includes(options.logLevel)) {
164
- console.error(`Error: Invalid log level '${options.logLevel}'. Valid levels: ${validLogLevels.join(', ')}`);
165
- process.exit(2);
166
- }
167
- try {
168
- // Load collection
169
- const collectionContent = readFileSync(collectionPath, 'utf-8');
170
- const collection: Collection = JSON.parse(collectionContent) as Collection;
171
-
172
- // Load environment if specified
173
- let environment: Environment | undefined;
174
- if (options.environment !== undefined) {
175
- const envContent = readFileSync(options.environment, 'utf-8');
176
- environment = JSON.parse(envContent) as Environment;
177
- }
178
-
179
- // Merge env-var options into environment
180
- if (options.envVar !== undefined && Object.keys(options.envVar).length > 0) {
181
- environment ??= { name: 'CLI Environment', variables: {} };
182
- environment.variables = { ...environment.variables, ...options.envVar };
183
- }
184
-
185
- // Load iteration data if specified
186
- let iterationData: IterationData[] | undefined;
187
- if (options.data !== undefined) {
188
- const dataContent = readFileSync(options.data, 'utf-8');
189
- if (options.data.endsWith('.json')) {
190
- iterationData = JSON.parse(dataContent) as IterationData[];
191
- } else if (options.data.endsWith('.csv')) {
192
- iterationData = parseCSV(dataContent);
193
- }
194
- }
195
-
196
- // Configure HTTP plugin for SSL validation
197
- if (options.insecure === true) {
198
- // This will be passed to axios config in the HTTP plugin
199
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
200
- }
201
-
202
- // Get plugin directories for auto-discovery
203
- const pluginDirs = getPluginDirectories();
204
-
205
- // Append user-specified plugin directories (if any)
206
- if (options.pluginsDir !== undefined && options.pluginsDir.length > 0) {
207
- pluginDirs.push(...options.pluginsDir);
208
- }
209
-
210
- // Convert string log level to LogLevel enum
211
- let logLevel: LogLevel | undefined;
212
- if (options.logLevel !== undefined) {
213
- const levelMap: Record<string, LogLevel> = {
214
- 'error': LogLevel.ERROR,
215
- 'warn': LogLevel.WARN,
216
- 'info': LogLevel.INFO,
217
- 'debug': LogLevel.DEBUG,
218
- 'trace': LogLevel.TRACE
219
- };
220
- logLevel = levelMap[options.logLevel];
221
- }
222
-
223
- // Create runner with plugin auto-discovery and log level
224
- const runner = new CollectionRunner({
225
- pluginsDir: pluginDirs,
226
- logLevel
227
- });
228
-
229
- // Set up console reporter
230
- const silent = options.silent;
231
- const color = options.color; // --no-color sets this to false
232
-
233
- if (silent !== true) {
234
- const reporter = new ConsoleReporter({
235
- logLevel,
236
- color,
237
- runner
238
- });
239
-
240
- // Wire up reporter to runner events
241
- runner.on('beforeRun', (payload: EventPayloads['beforeRun']) => {
242
- reporter.onRunStarted(collection, payload.options as Record<string, unknown>);
243
-
244
- // Show expected test count if available
245
- if (payload.expectedTestCount !== undefined && payload.expectedTestCount >= 0) {
246
- console.log(`Expected tests: ${payload.expectedTestCount}`);
247
- console.log('');
248
- }
249
-
250
- // Show validation errors if any (shouldn't reach here but defensive)
251
- if (payload.validationResult?.valid === false && payload.validationResult.errors !== undefined) {
252
- console.error('\nValidation errors detected:');
253
- for (const error of payload.validationResult.errors) {
254
- console.error(` ${error.location}: ${error.message}`);
255
- }
256
- console.error('');
257
- }
258
- });
259
-
260
- runner.on('beforeRequest', (payload: EventPayloads['beforeRequest']) => {
261
- reporter.onBeforeRequest?.(payload);
262
- });
263
-
264
- runner.on('afterRequest', (payload: EventPayloads['afterRequest']) => {
265
- reporter.onAfterRequest?.(payload);
266
- });
267
-
268
- runner.on('assertion', (payload: EventPayloads['assertion']) => {
269
- reporter.onAssertion?.(payload);
270
- });
271
-
272
- runner.on('afterRun', (payload: EventPayloads['afterRun']) => {
273
- reporter.onRunCompleted(payload.result);
274
- });
275
- }
276
-
277
- // Build RuntimeOptions from CLI options
278
- const runOptions: Record<string, unknown> = {
279
- // CLI-specific options
280
- environment,
281
- globalVariables: options.global,
282
- data: iterationData,
283
- iterations: options.iterations,
284
- filter: options.filter,
285
- excludeDeps: options.excludeDeps,
286
-
287
- // RuntimeOptions - Execution
288
- execution: {
289
- ...(options.parallel !== undefined ? { allowParallel: options.parallel } : {}),
290
- ...(options.concurrency !== undefined ? { maxConcurrency: options.concurrency } : {}),
291
- ...(options.bail !== undefined ? { bail: options.bail } : {}),
292
- ...(options.delay !== undefined ? { delay: options.delay } : {})
293
- },
294
-
295
- // RuntimeOptions - Timeout
296
- ...(options.timeout !== undefined ? {
297
- timeout: { request: options.timeout }
298
- } : {}),
299
-
300
- // RuntimeOptions - SSL
301
- ...(options.sslCert !== undefined || options.sslKey !== undefined || options.sslCa !== undefined || options.insecure !== undefined ? {
302
- ssl: {
303
- ...(options.insecure !== undefined ? { validateCertificates: options.insecure === false } : {}),
304
- ...(options.sslCert !== undefined || options.sslKey !== undefined ? {
305
- clientCertificate: {
306
- ...(options.sslCert !== undefined ? { cert: readFileSync(options.sslCert, 'utf-8') } : {}),
307
- ...(options.sslKey !== undefined ? { key: readFileSync(options.sslKey, 'utf-8') } : {}),
308
- ...(options.sslKeyPassphrase !== undefined ? { passphrase: options.sslKeyPassphrase } : {})
309
- }
310
- } : {}),
311
- ...(options.sslCa !== undefined ? { ca: readFileSync(options.sslCa, 'utf-8') } : {})
312
- }
313
- } : {}),
314
-
315
- // RuntimeOptions - Proxy
316
- ...(options.proxy !== undefined ? {
317
- proxy: {
318
- enabled: true,
319
- ...parseProxyUrl(options.proxy)!,
320
- ...(options.proxyAuth !== undefined ? { auth: parseProxyAuth(options.proxyAuth)! } : {}),
321
- ...(options.noProxy !== undefined ? { bypass: parseNoProxy(options.noProxy) } : {})
322
- }
323
- } : {}),
324
-
325
- // RuntimeOptions - Cookies
326
- ...(options.cookie !== undefined ? {
327
- cookies: (Array.isArray(options.cookie) ? options.cookie : [options.cookie]).map(parseCookie).filter((c): c is { name: string; value: string } => c !== null)
328
- } : {}),
329
-
330
- // RuntimeOptions - Cookie Jar
331
- ...(options.cookieJar !== undefined || options.cookieJarPersist !== undefined ? {
332
- jar: {
333
- enabled: options.cookieJar ?? false,
334
- ...(options.cookieJarPersist !== undefined ? { persist: options.cookieJarPersist } : {})
335
- }
336
- } : {}),
337
-
338
- // RuntimeOptions - Redirects
339
- ...(options.followRedirects !== undefined ? { followRedirects: options.followRedirects } : {}),
340
- ...(options.maxRedirects !== undefined ? { maxRedirects: options.maxRedirects } : {}),
341
-
342
- // RuntimeOptions - Validation
343
- strictMode: options.strictMode !== false // --no-strict-mode sets this to false, default is true
344
- };
345
-
346
- // Run collection
347
- const result = await runner.run(collection, runOptions);
348
-
349
- // Check for validation errors (pre-run validation failed)
350
- if (result.validationErrors !== undefined && result.validationErrors.length > 0) {
351
- if (silent !== true) {
352
- console.error('\nPre-run validation failed:\n');
353
- for (const error of result.validationErrors) {
354
- console.error(` ${error.location}: ${error.message}`);
355
- if (error.details?.line !== undefined) {
356
- console.error(` at line ${error.details.line}${error.details.column !== undefined ? `:${error.details.column}` : ''}`);
357
- }
358
- if (error.details?.suggestion !== undefined) {
359
- console.error(` > ${error.details.suggestion}`);
360
- }
361
- }
362
- }
363
- process.exit(3); // Exit code 3 for validation failures
364
- }
365
-
366
- // Results are displayed by the reporter
367
- // Determine exit code based on TEST results because request errors may be expected
368
- const hasErrors = result.failedTests > 0;
369
- process.exit(hasErrors ? 1 : 0);
370
-
371
- } catch (error) {
372
- console.error('Error:', error instanceof Error ? error.message : String(error));
373
- process.exit(4);
374
- }
375
- });
376
-
377
- program.parse();
378
-
379
- function collectKeyValue(value: string, previous: Record<string, string>): Record<string, string> {
380
- const [key, val] = value.split('=');
381
- if (key === undefined || key === '' || val === undefined) {
382
- throw new Error(`Invalid key=value format: ${value}`);
383
- }
384
- return { ...previous, [key.trim()]: val.trim() };
385
- }
386
-
387
- function collectArray(value: string, previous: string[]): string[] {
388
- return [...previous, value];
389
- }
390
-
391
- function parseCSV(content: string): IterationData[] {
392
- const lines = content.split('\n').filter(line => line.trim() !== '');
393
- if (lines.length === 0) return [];
394
-
395
- const headers = lines[0].split(',').map(h => h.trim());
396
- const data: IterationData[] = [];
397
-
398
- for (let i = 1; i < lines.length; i++) {
399
- const values = lines[i].split(',').map(v => v.trim());
400
- const row: IterationData = {};
401
-
402
- headers.forEach((header, index) => {
403
- const value = values[index];
404
- // Try to parse as number
405
- const numValue = Number(value);
406
- if (Number.isNaN(numValue) === false && value !== '') {
407
- row[header] = numValue;
408
- } else if (value === 'true') {
409
- row[header] = true;
410
- } else if (value === 'false') {
411
- row[header] = false;
412
- } else {
413
- row[header] = value;
414
- }
415
- });
416
-
417
- data.push(row);
418
- }
419
-
420
- return data;
421
- }
422
-
423
- /**
424
- * Parse proxy URL string into ProxyOptions
425
- * Format: http://host:port or https://host:port
426
- */
427
- function parseProxyUrl(proxyUrl: string): { host: string; port: number } | null {
428
- try {
429
- const url = new URL(proxyUrl);
430
- return {
431
- host: url.hostname,
432
- port: parseInt(url.port) > 0 ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80)
433
- };
434
- } catch {
435
- console.error(`Error: Invalid proxy URL format: ${proxyUrl}`);
436
- console.error('Expected format: http://host:port or https://host:port');
437
- process.exit(2);
438
- }
439
- }
440
-
441
- /**
442
- * Parse proxy auth string into username/password
443
- * Format: username:password
444
- */
445
- function parseProxyAuth(authString: string): { username: string; password: string } | null {
446
- const parts = authString.split(':');
447
- if (parts.length !== 2) {
448
- console.error(`Error: Invalid proxy auth format: ${authString}`);
449
- console.error('Expected format: username:password');
450
- process.exit(2);
451
- }
452
- return {
453
- username: parts[0],
454
- password: parts[1]
455
- };
456
- }
457
-
458
- /**
459
- * Parse comma-separated host list for proxy bypass
460
- */
461
- function parseNoProxy(noProxyString: string): string[] {
462
- return noProxyString.split(',').map(h => h.trim()).filter(h => h.length > 0);
463
- }
464
-
465
- /**
466
- * Parse cookie string into Cookie object
467
- * Format: name=value
468
- */
469
- function parseCookie(cookieString: string): { name: string; value: string } | null {
470
- const index = cookieString.indexOf('=');
471
- if (index === -1) {
472
- console.error(`Error: Invalid cookie format: ${cookieString}`);
473
- console.error('Expected format: name=value');
474
- process.exit(2);
475
- }
476
- return {
477
- name: cookieString.substring(0, index).trim(),
478
- value: cookieString.substring(index + 1).trim()
479
- };
480
- }