@apiquest/fracture 1.0.2 → 1.0.4

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 (168) hide show
  1. package/README.md +119 -0
  2. package/bin/cli.js +2 -2
  3. package/dist/CollectionRunner.js +3 -3
  4. package/dist/ScriptEngine.js +4 -4
  5. package/dist/cli/plugin-commands.d.ts.map +1 -1
  6. package/dist/cli/plugin-commands.js +2 -1
  7. package/dist/cli/plugin-commands.js.map +1 -1
  8. package/package.json +55 -50
  9. package/src/CollectionAnalyzer.ts +102 -102
  10. package/src/CollectionRunner.ts +1423 -1423
  11. package/src/CollectionRunner.types.ts +9 -9
  12. package/src/CollectionValidator.ts +289 -289
  13. package/src/ConsoleReporter.ts +143 -143
  14. package/src/CookieJar.ts +258 -258
  15. package/src/DagScheduler.ts +439 -439
  16. package/src/Logger.ts +85 -85
  17. package/src/PluginLoader.ts +126 -126
  18. package/src/PluginManager.ts +208 -208
  19. package/src/PluginResolver.ts +154 -154
  20. package/src/QuestAPI.ts +764 -764
  21. package/src/QuestAPI.types.ts +33 -33
  22. package/src/QuestTestAPI.ts +164 -164
  23. package/src/RequestFilter.ts +224 -224
  24. package/src/ScriptEngine.ts +219 -219
  25. package/src/ScriptValidator.ts +428 -428
  26. package/src/TaskGraph.ts +598 -598
  27. package/src/TestCounter.ts +109 -109
  28. package/src/VariableResolver.ts +114 -114
  29. package/src/cli/index.ts +480 -480
  30. package/src/cli/plugin-commands.ts +342 -341
  31. package/src/cli/plugin-discovery.ts +44 -44
  32. package/src/index.ts +24 -24
  33. package/src/utils.ts +52 -52
  34. package/tsconfig.json +20 -20
  35. package/tsconfig.test.json +5 -5
  36. package/vitest.config.ts +22 -22
  37. package/dist/ExecutionTree.d.ts +0 -77
  38. package/dist/ExecutionTree.d.ts.map +0 -1
  39. package/dist/ExecutionTree.js +0 -265
  40. package/dist/ExecutionTree.js.map +0 -1
  41. package/dist/fracture/src/CollectionAnalyzer.d.ts +0 -17
  42. package/dist/fracture/src/CollectionAnalyzer.d.ts.map +0 -1
  43. package/dist/fracture/src/CollectionAnalyzer.js +0 -70
  44. package/dist/fracture/src/CollectionAnalyzer.js.map +0 -1
  45. package/dist/fracture/src/CollectionRunner.d.ts +0 -39
  46. package/dist/fracture/src/CollectionRunner.d.ts.map +0 -1
  47. package/dist/fracture/src/CollectionRunner.js +0 -802
  48. package/dist/fracture/src/CollectionRunner.js.map +0 -1
  49. package/dist/fracture/src/CollectionRunner.types.d.ts +0 -8
  50. package/dist/fracture/src/CollectionRunner.types.d.ts.map +0 -1
  51. package/dist/fracture/src/CollectionRunner.types.js +0 -2
  52. package/dist/fracture/src/CollectionRunner.types.js.map +0 -1
  53. package/dist/fracture/src/CollectionValidator.d.ts +0 -14
  54. package/dist/fracture/src/CollectionValidator.d.ts.map +0 -1
  55. package/dist/fracture/src/CollectionValidator.js +0 -145
  56. package/dist/fracture/src/CollectionValidator.js.map +0 -1
  57. package/dist/fracture/src/ConsoleReporter.d.ts +0 -24
  58. package/dist/fracture/src/ConsoleReporter.d.ts.map +0 -1
  59. package/dist/fracture/src/ConsoleReporter.js +0 -123
  60. package/dist/fracture/src/ConsoleReporter.js.map +0 -1
  61. package/dist/fracture/src/CookieJar.d.ts +0 -70
  62. package/dist/fracture/src/CookieJar.d.ts.map +0 -1
  63. package/dist/fracture/src/CookieJar.js +0 -233
  64. package/dist/fracture/src/CookieJar.js.map +0 -1
  65. package/dist/fracture/src/ExecutionTree.d.ts +0 -77
  66. package/dist/fracture/src/ExecutionTree.d.ts.map +0 -1
  67. package/dist/fracture/src/ExecutionTree.js +0 -258
  68. package/dist/fracture/src/ExecutionTree.js.map +0 -1
  69. package/dist/fracture/src/Logger.d.ts +0 -25
  70. package/dist/fracture/src/Logger.d.ts.map +0 -1
  71. package/dist/fracture/src/Logger.js +0 -78
  72. package/dist/fracture/src/Logger.js.map +0 -1
  73. package/dist/fracture/src/PluginLoader.d.ts +0 -23
  74. package/dist/fracture/src/PluginLoader.d.ts.map +0 -1
  75. package/dist/fracture/src/PluginLoader.js +0 -102
  76. package/dist/fracture/src/PluginLoader.js.map +0 -1
  77. package/dist/fracture/src/PluginManager.d.ts +0 -64
  78. package/dist/fracture/src/PluginManager.d.ts.map +0 -1
  79. package/dist/fracture/src/PluginManager.js +0 -162
  80. package/dist/fracture/src/PluginManager.js.map +0 -1
  81. package/dist/fracture/src/PluginResolver.d.ts +0 -35
  82. package/dist/fracture/src/PluginResolver.d.ts.map +0 -1
  83. package/dist/fracture/src/PluginResolver.js +0 -128
  84. package/dist/fracture/src/PluginResolver.js.map +0 -1
  85. package/dist/fracture/src/QuestAPI.d.ts +0 -9
  86. package/dist/fracture/src/QuestAPI.d.ts.map +0 -1
  87. package/dist/fracture/src/QuestAPI.js +0 -679
  88. package/dist/fracture/src/QuestAPI.js.map +0 -1
  89. package/dist/fracture/src/QuestAPI.types.d.ts +0 -35
  90. package/dist/fracture/src/QuestAPI.types.d.ts.map +0 -1
  91. package/dist/fracture/src/QuestAPI.types.js +0 -3
  92. package/dist/fracture/src/QuestAPI.types.js.map +0 -1
  93. package/dist/fracture/src/QuestTestAPI.d.ts +0 -12
  94. package/dist/fracture/src/QuestTestAPI.d.ts.map +0 -1
  95. package/dist/fracture/src/QuestTestAPI.js +0 -133
  96. package/dist/fracture/src/QuestTestAPI.js.map +0 -1
  97. package/dist/fracture/src/ScriptEngine.d.ts +0 -21
  98. package/dist/fracture/src/ScriptEngine.d.ts.map +0 -1
  99. package/dist/fracture/src/ScriptEngine.js +0 -183
  100. package/dist/fracture/src/ScriptEngine.js.map +0 -1
  101. package/dist/fracture/src/ScriptValidator.d.ts +0 -68
  102. package/dist/fracture/src/ScriptValidator.d.ts.map +0 -1
  103. package/dist/fracture/src/ScriptValidator.js +0 -351
  104. package/dist/fracture/src/ScriptValidator.js.map +0 -1
  105. package/dist/fracture/src/TestCounter.d.ts +0 -18
  106. package/dist/fracture/src/TestCounter.d.ts.map +0 -1
  107. package/dist/fracture/src/TestCounter.js +0 -82
  108. package/dist/fracture/src/TestCounter.js.map +0 -1
  109. package/dist/fracture/src/VariableResolver.d.ts +0 -20
  110. package/dist/fracture/src/VariableResolver.d.ts.map +0 -1
  111. package/dist/fracture/src/VariableResolver.js +0 -100
  112. package/dist/fracture/src/VariableResolver.js.map +0 -1
  113. package/dist/fracture/src/cli/index.d.ts +0 -3
  114. package/dist/fracture/src/cli/index.d.ts.map +0 -1
  115. package/dist/fracture/src/cli/index.js +0 -347
  116. package/dist/fracture/src/cli/index.js.map +0 -1
  117. package/dist/fracture/src/cli/plugin-commands.d.ts +0 -6
  118. package/dist/fracture/src/cli/plugin-commands.d.ts.map +0 -1
  119. package/dist/fracture/src/cli/plugin-commands.js +0 -263
  120. package/dist/fracture/src/cli/plugin-commands.js.map +0 -1
  121. package/dist/fracture/src/cli/plugin-discovery.d.ts +0 -11
  122. package/dist/fracture/src/cli/plugin-discovery.d.ts.map +0 -1
  123. package/dist/fracture/src/cli/plugin-discovery.js +0 -64
  124. package/dist/fracture/src/cli/plugin-discovery.js.map +0 -1
  125. package/dist/fracture/src/index.d.ts +0 -13
  126. package/dist/fracture/src/index.d.ts.map +0 -1
  127. package/dist/fracture/src/index.js +0 -17
  128. package/dist/fracture/src/index.js.map +0 -1
  129. package/dist/fracture/src/utils.d.ts +0 -28
  130. package/dist/fracture/src/utils.d.ts.map +0 -1
  131. package/dist/fracture/src/utils.js +0 -48
  132. package/dist/fracture/src/utils.js.map +0 -1
  133. package/dist/plugin-auth/src/apikey-auth.d.ts +0 -3
  134. package/dist/plugin-auth/src/apikey-auth.d.ts.map +0 -1
  135. package/dist/plugin-auth/src/apikey-auth.js +0 -73
  136. package/dist/plugin-auth/src/apikey-auth.js.map +0 -1
  137. package/dist/plugin-auth/src/basic-auth.d.ts +0 -3
  138. package/dist/plugin-auth/src/basic-auth.d.ts.map +0 -1
  139. package/dist/plugin-auth/src/basic-auth.js +0 -61
  140. package/dist/plugin-auth/src/basic-auth.js.map +0 -1
  141. package/dist/plugin-auth/src/bearer-auth.d.ts +0 -3
  142. package/dist/plugin-auth/src/bearer-auth.d.ts.map +0 -1
  143. package/dist/plugin-auth/src/bearer-auth.js +0 -49
  144. package/dist/plugin-auth/src/bearer-auth.js.map +0 -1
  145. package/dist/plugin-auth/src/helpers.d.ts +0 -3
  146. package/dist/plugin-auth/src/helpers.d.ts.map +0 -1
  147. package/dist/plugin-auth/src/helpers.js +0 -8
  148. package/dist/plugin-auth/src/helpers.js.map +0 -1
  149. package/dist/plugin-auth/src/index.d.ts +0 -10
  150. package/dist/plugin-auth/src/index.d.ts.map +0 -1
  151. package/dist/plugin-auth/src/index.js +0 -25
  152. package/dist/plugin-auth/src/index.js.map +0 -1
  153. package/dist/plugin-auth/src/oauth2-auth.d.ts +0 -35
  154. package/dist/plugin-auth/src/oauth2-auth.d.ts.map +0 -1
  155. package/dist/plugin-auth/src/oauth2-auth.js +0 -266
  156. package/dist/plugin-auth/src/oauth2-auth.js.map +0 -1
  157. package/dist/plugin-http/src/index.d.ts +0 -4
  158. package/dist/plugin-http/src/index.d.ts.map +0 -1
  159. package/dist/plugin-http/src/index.js +0 -266
  160. package/dist/plugin-http/src/index.js.map +0 -1
  161. package/dist/plugin-vault-file/src/index.d.ts +0 -67
  162. package/dist/plugin-vault-file/src/index.d.ts.map +0 -1
  163. package/dist/plugin-vault-file/src/index.js +0 -171
  164. package/dist/plugin-vault-file/src/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -374
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -13
  168. package/dist/types.js.map +0 -1
package/src/cli/index.ts CHANGED
@@ -1,480 +1,480 @@
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
- }
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
+ }