itworksbut 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -270,6 +270,6 @@ Each check is a plain ESM module with an `id`, metadata, and async `run(context)
270
270
 
271
271
  ## What It Does Not Guarantee
272
272
 
273
- ItWorksBut is a static heuristic scanner, not a pentest, SAST replacement, dependency vulnerability database, or runtime security monitor. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
273
+ ItWorksBut is a static heuristic scanner. Findings intentionally use wording such as "possible", "potential", and "appears to" when a check is heuristic.
274
274
 
275
275
  Use it as a CI guardrail for common project hygiene and security mistakes. For production systems, combine it with code review, tests, dependency scanning, secrets scanning, threat modeling, and focused security assessment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "itworksbut",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Static CI checks for common security, repo, dependency, build, and deployment risks in JavaScript vibe coding projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,95 +1,109 @@
1
- import boxen from "boxen";
2
- import chalk, { Chalk } from "chalk";
3
- import figlet from "figlet";
4
- import gradient from "gradient-string";
5
- import ora from "ora";
1
+ import boxen from 'boxen';
2
+ import chalk, { Chalk } from 'chalk';
3
+ import figlet from 'figlet';
4
+ import gradient from 'gradient-string';
5
+ import ora from 'ora';
6
6
 
7
7
  const SPINNER_TEXT = {
8
- git: "Checking git hygiene",
9
- env: "Sniffing for secrets",
10
- dependencies: "Interrogating package.json",
11
- ci: "Inspecting CI rituals",
12
- node: "Poking the Node.js backend",
13
- web: "Looking for frontend footguns",
14
- api: "Testing the API trust issues",
15
- database: "Watching SQL strings misbehave",
16
- electron: "Opening the Electron danger drawer",
17
- tauri: "Reading Tauri permissions",
18
- default: "Looking for things that work but should not ship"
8
+ git: 'Checking git hygiene',
9
+ env: 'Sniffing for secrets',
10
+ dependencies: 'Interrogating package.json',
11
+ ci: 'Inspecting CI rituals',
12
+ node: 'Poking the Node.js backend',
13
+ web: 'Looking for frontend footguns',
14
+ api: 'Testing the API trust issues',
15
+ database: 'Watching SQL strings misbehave',
16
+ electron: 'Opening the Electron danger drawer',
17
+ tauri: 'Reading Tauri permissions',
18
+ stress: 'Running controlled API stress test',
19
+ default: 'Looking for things that work but should not ship',
19
20
  };
20
21
 
21
22
  export function isFancyOutputEnabled(options = {}, env = process.env, stdout = process.stdout) {
22
- return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.todo && !options.noColor;
23
+ return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.todo && !options.noColor;
23
24
  }
24
25
 
25
26
  export function isColorEnabled(options = {}, env = process.env, stdout = process.stdout) {
26
- if (options.noColor || options.json || options.sarif || options.todo) return false;
27
- if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") return true;
28
- if (env.CI) return false;
29
- return Boolean(stdout.isTTY);
27
+ if (options.noColor || options.json || options.sarif || options.todo) return false;
28
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== '0') return true;
29
+ if (env.CI) return false;
30
+ return Boolean(stdout.isTTY);
30
31
  }
31
32
 
32
33
  export function getChalk(options = {}) {
33
- if (!isColorEnabled(options)) return new Chalk({ level: 0 });
34
- return chalk;
34
+ if (!isColorEnabled(options)) return new Chalk({ level: 0 });
35
+ return chalk;
35
36
  }
36
37
 
37
38
  export function shouldUseSpinner(options = {}, env = process.env, stdout = process.stdout) {
38
- return (
39
- Boolean(stdout.isTTY) &&
40
- !env.CI &&
41
- !options.json &&
42
- !options.sarif &&
43
- !options.todo &&
44
- !options.quiet
45
- );
39
+ return Boolean(stdout.isTTY) && !env.CI && !options.json && !options.sarif && !options.todo && !options.quiet;
46
40
  }
47
41
 
48
42
  export function createScanSpinner(options = {}) {
49
- if (!shouldUseSpinner(options)) return null;
50
- return ora({
51
- text: SPINNER_TEXT.default,
52
- stream: process.stderr,
53
- color: "cyan"
54
- });
43
+ if (!shouldUseSpinner(options)) return null;
44
+ return ora({
45
+ text: SPINNER_TEXT.default,
46
+ stream: process.stderr,
47
+ color: 'cyan',
48
+ });
49
+ }
50
+
51
+ export function createStressSpinner(options = {}) {
52
+ if (!shouldUseSpinner(options)) return null;
53
+ return ora({
54
+ text: SPINNER_TEXT.stress,
55
+ stream: process.stderr,
56
+ color: 'cyan',
57
+ });
55
58
  }
56
59
 
57
60
  export function printIntro(options = {}) {
58
- if (options.json || options.sarif || options.todo || options.noBanner || options.quiet || process.env.CI || !process.stdout.isTTY) {
59
- return;
60
- }
61
+ if (
62
+ options.json ||
63
+ options.sarif ||
64
+ options.todo ||
65
+ options.noBanner ||
66
+ options.quiet ||
67
+ process.env.CI ||
68
+ !process.stdout.isTTY
69
+ ) {
70
+ return;
71
+ }
61
72
 
62
- const colors = getChalk(options);
63
- const title = renderTitle(options.noColor);
64
- const claim = `${colors.bold("AI-built? Nice.")}\n${colors.yellow("Now let's see what breaks before production.")}`;
73
+ const colors = getChalk(options);
74
+ const stress = options.command === 'stress';
75
+ const title = renderTitle('ItWorksBut', options.noColor);
76
+ const claim = stress
77
+ ? `${colors.bold('Controlled API stress test.')}\n${colors.yellow('Only run this against systems you own or are authorized to test.')}`
78
+ : `${colors.bold('AI-built? Nice.')}\n${colors.yellow("Now let's see what breaks before production.")}`;
65
79
 
66
- process.stdout.write(`${title}\n`);
67
- process.stdout.write(
68
- `${boxen(claim, {
69
- padding: 1,
70
- margin: 1,
71
- borderStyle: "round",
72
- borderColor: options.noColor ? undefined : "cyan"
73
- })}\n`
74
- );
80
+ process.stdout.write(`${title}\n`);
81
+ process.stdout.write(
82
+ `${boxen(claim, {
83
+ padding: 1,
84
+ margin: 1,
85
+ borderStyle: 'round',
86
+ borderColor: options.noColor ? undefined : 'cyan',
87
+ })}\n`,
88
+ );
75
89
  }
76
90
 
77
- function renderTitle(noColor) {
78
- let title = "ItWorksBut";
79
- try {
80
- title = figlet.textSync("ItWorksBut", {
81
- font: "ANSI Shadow",
82
- horizontalLayout: "default",
83
- verticalLayout: "default"
84
- });
85
- } catch {
86
- title = "ItWorksBut";
87
- }
91
+ function renderTitle(value, noColor) {
92
+ let title = value;
93
+ try {
94
+ title = figlet.textSync(value, {
95
+ font: 'ANSI Shadow',
96
+ horizontalLayout: 'default',
97
+ verticalLayout: 'default',
98
+ });
99
+ } catch {
100
+ title = 'ItWorksBut';
101
+ }
88
102
 
89
- try {
90
- if (noColor) return title;
91
- return gradient.rainbow(title);
92
- } catch {
93
- return title;
94
- }
103
+ try {
104
+ if (noColor) return title;
105
+ return gradient.rainbow(title);
106
+ } catch {
107
+ return title;
108
+ }
95
109
  }
@@ -6,16 +6,31 @@ import { runArtillery } from "../stress/artilleryRunner.js";
6
6
  import { parseArtilleryResult } from "../stress/stressResultParser.js";
7
7
  import { reportStressConsole } from "../stress/stressRenderer.js";
8
8
  import { writeStressMarkdownReport } from "../reporters/stressMarkdownReport.js";
9
+ import { createStressSpinner, printIntro } from "../cli/terminal.js";
9
10
 
10
11
  export async function runStressCommand(args, options = {}) {
11
12
  if (args.todo || args.sarif) {
12
13
  throw new Error("The stress command supports console output, --json, and --report.");
13
14
  }
14
15
 
15
- const result = await runStress({
16
+ const stressOptions = {
16
17
  rootPath: path.resolve(args.path || "."),
17
18
  ...validateStressOptions(args)
18
- }, options);
19
+ };
20
+
21
+ printIntro(args);
22
+
23
+ const spinner = createStressSpinner(args);
24
+ if (spinner) spinner.start();
25
+
26
+ let result;
27
+ try {
28
+ result = await runStress(stressOptions, options);
29
+ if (spinner) spinner.succeed("Stress test complete. Now the pressure readings.");
30
+ } catch (error) {
31
+ if (spinner) spinner.fail("Stress test stopped before results were printed.");
32
+ throw error;
33
+ }
19
34
 
20
35
  if (args.json) {
21
36
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -2,10 +2,21 @@ import path from "node:path";
2
2
  import { DEFAULT_IGNORE } from "../core/config.js";
3
3
  import { walkProject } from "../core/fileWalker.js";
4
4
  import { readFileSafe } from "../utils/fs.js";
5
+ import { isServerOrApiFile } from "../checks/helpers.js";
5
6
 
6
- const HTTP_METHODS = ["get", "head", "post", "put", "patch", "delete"];
7
+ const HTTP_METHODS = ["get", "head", "post", "put", "patch", "delete", "options"];
7
8
  const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
8
- const ROUTE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
9
+ const ROUTE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
10
+ const API_HANDLER_EVIDENCE = [
11
+ /\b(?:req|request)\s*,\s*(?:res|response)\b/i,
12
+ /\b(?:res|response)\s*\.\s*(?:json|send|status|end|redirect|writeHead|setHeader)\s*\(/i,
13
+ /\b(?:ctx|context)\s*\.\s*(?:body|response|status|json)\b/i,
14
+ /\bNextResponse\s*\.\s*(?:json|redirect)\s*\(/,
15
+ /\b(?:new\s+Response|Response\.json)\s*\(/,
16
+ /\bdefineEventHandler\s*\(/,
17
+ /\bexport\s+(?:async\s+)?function\s+(?:GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)\b/,
18
+ /\bexport\s+(?:const|let|var)\s+(?:GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)\s*=/
19
+ ];
9
20
  const DISCOVERY_IGNORE = [
10
21
  ...DEFAULT_IGNORE,
11
22
  "test/**",
@@ -28,15 +39,23 @@ export async function discoverEndpoints(rootPath) {
28
39
 
29
40
  export async function discoverEndpointsFromFiles({ rootPath, files, readFile }) {
30
41
  const endpoints = [];
42
+ const fileContents = [];
31
43
 
32
44
  for (const file of files.filter(isSourceFile)) {
33
45
  const content = await readFile(file);
34
46
  if (content === null || content === undefined) continue;
47
+ fileContents.push({ file, content });
48
+ }
49
+
50
+ const mountPrefixes = collectMountPrefixes(fileContents);
35
51
 
36
- endpoints.push(...discoverExpressEndpoints(file, content));
52
+ for (const { file, content } of fileContents) {
53
+ endpoints.push(...discoverExpressEndpoints(file, content, mountPrefixes));
54
+ endpoints.push(...discoverMountedRouterEndpoints(file, content, mountPrefixes));
55
+ endpoints.push(...discoverFastifyRouteObjects(file, content));
37
56
  endpoints.push(...discoverFetchReferences(file, content));
38
- endpoints.push(...discoverNextAppRouterEndpoints(file, content));
39
- endpoints.push(...discoverNextPagesRouterEndpoints(file, content));
57
+ endpoints.push(...discoverFileConventionEndpoints(file, content));
58
+ endpoints.push(...discoverApiCandidateFileEndpoints(file, content));
40
59
  }
41
60
 
42
61
  return classifyEndpoints(dedupeEndpoints(endpoints), rootPath);
@@ -50,7 +69,10 @@ export function classifyEndpoints(endpoints) {
50
69
  let status = "selected";
51
70
  let reason;
52
71
 
53
- if (unsafe) {
72
+ if (method === "OPTIONS") {
73
+ status = "skipped";
74
+ reason = "unsupported method";
75
+ } else if (unsafe) {
54
76
  status = "skipped";
55
77
  reason = "unsafe method";
56
78
  } else if (dynamic) {
@@ -77,16 +99,60 @@ export function classifyEndpoints(endpoints) {
77
99
  };
78
100
  }
79
101
 
80
- function discoverExpressEndpoints(file, content) {
102
+ function discoverExpressEndpoints(file, content, mountPrefixes = []) {
81
103
  const endpoints = [];
82
104
  const methods = HTTP_METHODS.join("|");
83
- const regex = new RegExp(`\\b(?:app|router|server)\\s*\\.\\s*(${methods})\\s*\\(\\s*(['"\`])([^'"\`]+)\\2`, "gi");
105
+ const regex = new RegExp(`\\b([A-Za-z_$][\\w$]*)\\s*\\.\\s*(${methods})\\s*\\(\\s*(['"\`])([^'"\`]+)\\3`, "gi");
106
+ const receivers = collectRouteReceivers(content);
107
+ const mountedRouterFile = hasApplicableMount(file, mountPrefixes);
84
108
  let match;
85
109
 
86
110
  while ((match = regex.exec(content)) !== null) {
87
- const routePath = normalizeRoutePath(match[3]);
111
+ if (!isRouteReceiver(match[1], receivers)) continue;
112
+ if (mountedRouterFile && isRouterReceiver(match[1], receivers)) continue;
113
+ const routePath = normalizeRoutePath(match[4]);
88
114
  if (!routePath) continue;
89
- endpoints.push(endpoint(match[1], routePath, file, "express"));
115
+ endpoints.push(endpoint(match[2], routePath, file, "express"));
116
+ }
117
+
118
+ return endpoints;
119
+ }
120
+
121
+ function discoverMountedRouterEndpoints(file, content, mountPrefixes) {
122
+ const applicableMounts = mountPrefixes.filter((mount) => mountAppliesToFile(mount, file));
123
+ if (applicableMounts.length === 0) return [];
124
+
125
+ const endpoints = [];
126
+ const methods = HTTP_METHODS.join("|");
127
+ const regex = new RegExp(`\\b([A-Za-z_$][\\w$]*)\\s*\\.\\s*(${methods})\\s*\\(\\s*(['"\`])([^'"\`]+)\\3`, "gi");
128
+ const receivers = collectRouteReceivers(content);
129
+ let match;
130
+
131
+ while ((match = regex.exec(content)) !== null) {
132
+ if (!isRouterReceiver(match[1], receivers)) continue;
133
+ const routePath = normalizeRoutePath(match[4]);
134
+ if (!routePath || routePath.startsWith("/api")) continue;
135
+ for (const mount of applicableMounts) {
136
+ endpoints.push(endpoint(match[2], joinRoutePath(mount.prefix, routePath), file, "mounted-router"));
137
+ }
138
+ }
139
+
140
+ return endpoints;
141
+ }
142
+
143
+ function discoverFastifyRouteObjects(file, content) {
144
+ const endpoints = [];
145
+ const regex = /\bfastify\s*\.\s*route\s*\(\s*\{([\s\S]*?)\}\s*\)/gi;
146
+ let match;
147
+
148
+ while ((match = regex.exec(content)) !== null) {
149
+ const objectText = match[1];
150
+ const methodMatch = objectText.match(/\bmethod\s*:\s*(['"`])(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)\1/i);
151
+ const urlMatch = objectText.match(/\b(?:url|path)\s*:\s*(['"`])([^'"`]+)\1/i);
152
+ if (!methodMatch || !urlMatch) continue;
153
+ const routePath = normalizeRoutePath(urlMatch[2]);
154
+ if (!routePath) continue;
155
+ endpoints.push(endpoint(methodMatch[2], routePath, file, "fastify-route-object"));
90
156
  }
91
157
 
92
158
  return endpoints;
@@ -106,12 +172,22 @@ function discoverFetchReferences(file, content) {
106
172
  return endpoints;
107
173
  }
108
174
 
175
+ function discoverFileConventionEndpoints(file, content) {
176
+ return [
177
+ ...discoverNextAppRouterEndpoints(file, content),
178
+ ...discoverNextPagesRouterEndpoints(file, content),
179
+ ...discoverSvelteKitEndpoints(file, content),
180
+ ...discoverNitroEndpoints(file, content),
181
+ ...discoverAstroApiEndpoints(file, content)
182
+ ];
183
+ }
184
+
109
185
  function discoverNextAppRouterEndpoints(file, content) {
110
186
  const normalized = normalizeFilePath(file);
111
- const match = normalized.match(/(?:^|\/)(?:src\/)?app\/api\/(.+)\/route\.(?:js|jsx|ts|tsx|mjs|cjs)$/);
187
+ const match = normalized.match(/(?:^|\/)(?:src\/)?app\/(.+)\/route\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)$/);
112
188
  if (!match) return [];
113
189
 
114
- const routePath = normalizeRoutePath(`/api/${routeSegmentsToPath(match[1])}`);
190
+ const routePath = normalizeRoutePath(`/${routeSegmentsToPath(match[1])}`);
115
191
  const exportedMethods = discoverExportedRouteMethods(content);
116
192
  const methods = exportedMethods.length > 0 ? exportedMethods : ["GET"];
117
193
 
@@ -120,7 +196,7 @@ function discoverNextAppRouterEndpoints(file, content) {
120
196
 
121
197
  function discoverNextPagesRouterEndpoints(file, content) {
122
198
  const normalized = normalizeFilePath(file);
123
- const match = normalized.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(?:js|jsx|ts|tsx|mjs|cjs)$/);
199
+ const match = normalized.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)$/);
124
200
  if (!match) return [];
125
201
 
126
202
  const routePath = normalizeRoutePath(`/api/${routeSegmentsToPath(match[1])}`);
@@ -130,13 +206,67 @@ function discoverNextPagesRouterEndpoints(file, content) {
130
206
  return methods.map((method) => endpoint(method, routePath, file, "next-pages-router"));
131
207
  }
132
208
 
209
+ function discoverSvelteKitEndpoints(file, content) {
210
+ const normalized = normalizeFilePath(file);
211
+ const match = normalized.match(/(?:^|\/)(?:src\/)?routes\/(.+)\/\+server\.(?:js|ts|mjs|cjs|mts|cts)$/);
212
+ if (!match) return [];
213
+
214
+ const routePath = normalizeRoutePath(`/${routeSegmentsToPath(match[1])}`);
215
+ const exportedMethods = discoverExportedRouteMethods(content);
216
+ const methods = exportedMethods.length > 0 ? exportedMethods : ["GET"];
217
+
218
+ return methods.map((method) => endpoint(method, routePath, file, "sveltekit-server-route"));
219
+ }
220
+
221
+ function discoverNitroEndpoints(file, content) {
222
+ const normalized = normalizeFilePath(file);
223
+ const match = normalized.match(/(?:^|\/)(?:src\/)?server\/api\/(.+?)(?:\.(get|head|post|put|patch|delete|options))?\.(?:js|ts|mjs|cjs|mts|cts)$/i);
224
+ if (!match) return [];
225
+
226
+ const routePath = normalizeRoutePath(`/api/${routeSegmentsToPath(match[1])}`);
227
+ const methods = match[2] ? [match[2].toUpperCase()] : discoverMethodGuards(content);
228
+
229
+ return (methods.length > 0 ? methods : ["GET"]).map((method) => endpoint(method, routePath, file, "nitro-server-api"));
230
+ }
231
+
232
+ function discoverAstroApiEndpoints(file, content) {
233
+ const normalized = normalizeFilePath(file);
234
+ const match = normalized.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(?:js|ts|mjs|cjs|mts|cts)$/);
235
+ if (!match) return [];
236
+
237
+ const routePath = normalizeRoutePath(`/api/${routeSegmentsToPath(match[1])}`);
238
+ const exportedMethods = discoverExportedRouteMethods(content);
239
+ const methods = exportedMethods.length > 0 ? exportedMethods : ["GET"];
240
+
241
+ return methods.map((method) => endpoint(method, routePath, file, "astro-api-route"));
242
+ }
243
+
244
+ function discoverApiCandidateFileEndpoints(file, content) {
245
+ if (!isSastApiCandidate(file, content)) return [];
246
+ if (hasExplicitRouteDeclaration(content) || isFileConventionRoute(file)) return [];
247
+
248
+ const routePath = inferRoutePathFromFile(file);
249
+ if (!routePath) return [];
250
+
251
+ const methods = discoverExportedRouteMethods(content);
252
+ const guardedMethods = discoverMethodGuards(content);
253
+ const inferredMethods = methods.length > 0 ? methods : guardedMethods;
254
+
255
+ return (inferredMethods.length > 0 ? inferredMethods : ["GET"]).map((method) => endpoint(method, routePath, file, "sast-api-candidate"));
256
+ }
257
+
133
258
  function discoverExportedRouteMethods(content) {
134
259
  const methods = new Set();
135
- const regex = /\bexport\s+(?:async\s+)?function\s+(GET|HEAD|POST|PUT|PATCH|DELETE)\b/g;
136
- let match;
137
-
138
- while ((match = regex.exec(content)) !== null) {
139
- methods.add(match[1]);
260
+ const regexes = [
261
+ /\bexport\s+(?:async\s+)?function\s+(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)\b/g,
262
+ /\bexport\s+(?:const|let|var)\s+(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)\s*=/g
263
+ ];
264
+
265
+ for (const regex of regexes) {
266
+ let match;
267
+ while ((match = regex.exec(content)) !== null) {
268
+ methods.add(match[1]);
269
+ }
140
270
  }
141
271
 
142
272
  return [...methods];
@@ -144,7 +274,7 @@ function discoverExportedRouteMethods(content) {
144
274
 
145
275
  function discoverMethodGuards(content) {
146
276
  const methods = new Set();
147
- const regex = /\b(?:req|request)\.method\s*(?:===|!==|==|!=)\s*['"`](GET|HEAD|POST|PUT|PATCH|DELETE)['"`]/gi;
277
+ const regex = /\b(?:req|request)\.method\s*(?:===|!==|==|!=)\s*['"`](GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)['"`]/gi;
148
278
  let match;
149
279
 
150
280
  while ((match = regex.exec(content)) !== null) {
@@ -154,6 +284,131 @@ function discoverMethodGuards(content) {
154
284
  return [...methods];
155
285
  }
156
286
 
287
+ function collectMountPrefixes(fileContents) {
288
+ const mounts = [];
289
+ const useRegex = /\b[A-Za-z_$][\w$]*\s*\.\s*use\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*(?:([A-Za-z_$][\w$]*)\s*\(|([A-Za-z_$][\w$]*)\b|require\s*\(\s*(['"`])([^'"`]+)\5\s*\))/gi;
290
+ const registerRegex = /\b[A-Za-z_$][\w$]*\s*\.\s*register\s*\(\s*([A-Za-z_$][\w$]*)\b[\s\S]*?\bprefix\s*:\s*(['"`])([^'"`]+)\2/gi;
291
+
292
+ for (const { file, content } of fileContents) {
293
+ const localModuleBindings = collectLocalModuleBindings(file, content);
294
+ let match;
295
+
296
+ useRegex.lastIndex = 0;
297
+ while ((match = useRegex.exec(content)) !== null) {
298
+ const mount = createUseMount(match, localModuleBindings, file);
299
+ if (mount) mounts.push(mount);
300
+ }
301
+
302
+ registerRegex.lastIndex = 0;
303
+ while ((match = registerRegex.exec(content)) !== null) {
304
+ const mount = createNamedMount(match[3], match[1], localModuleBindings);
305
+ if (mount) mounts.push(mount);
306
+ }
307
+ }
308
+
309
+ return dedupeMounts(mounts);
310
+ }
311
+
312
+ function collectLocalModuleBindings(file, content) {
313
+ const bindings = new Map();
314
+ const destructuredRequireRegex = /\b(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*require\s*\(\s*(['"`])([^'"`]+)\2\s*\)/g;
315
+ const requireRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*(['"`])([^'"`]+)\2\s*\)/g;
316
+ const namedImportRegex = /\bimport\s*\{\s*([^}]+)\s*\}\s*from\s*(['"`])([^'"`]+)\2/g;
317
+ const defaultImportRegex = /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*(['"`])([^'"`]+)\2/g;
318
+ const namespaceImportRegex = /\bimport\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*(['"`])([^'"`]+)\2/g;
319
+
320
+ let match;
321
+ while ((match = destructuredRequireRegex.exec(content)) !== null) {
322
+ addNamedBindings(bindings, match[1], resolveLocalModuleFiles(file, match[3]));
323
+ }
324
+
325
+ while ((match = requireRegex.exec(content)) !== null) {
326
+ addBinding(bindings, match[1], resolveLocalModuleFiles(file, match[3]));
327
+ }
328
+
329
+ while ((match = namedImportRegex.exec(content)) !== null) {
330
+ addNamedBindings(bindings, match[1], resolveLocalModuleFiles(file, match[3]));
331
+ }
332
+
333
+ while ((match = defaultImportRegex.exec(content)) !== null) {
334
+ addBinding(bindings, match[1], resolveLocalModuleFiles(file, match[3]));
335
+ }
336
+
337
+ while ((match = namespaceImportRegex.exec(content)) !== null) {
338
+ addBinding(bindings, match[1], resolveLocalModuleFiles(file, match[3]));
339
+ }
340
+
341
+ return bindings;
342
+ }
343
+
344
+ function addNamedBindings(bindings, bindingList, targetFiles) {
345
+ for (const binding of bindingList.split(",")) {
346
+ const normalized = binding.trim();
347
+ if (!normalized) continue;
348
+ const localName = normalized.includes(" as ")
349
+ ? normalized.split(/\s+as\s+/).pop().trim()
350
+ : normalized.split(":").pop().trim();
351
+ addBinding(bindings, localName, targetFiles);
352
+ }
353
+ }
354
+
355
+ function addBinding(bindings, name, targetFiles) {
356
+ if (!name || targetFiles.length === 0) return;
357
+ bindings.set(name, targetFiles);
358
+ }
359
+
360
+ function createUseMount(match, localModuleBindings, file) {
361
+ const [, , rawPrefix, calledIdentifier, identifier, , requireSpecifier] = match;
362
+ const mountedName = calledIdentifier || identifier;
363
+ if (requireSpecifier) {
364
+ return createMount(rawPrefix, resolveLocalModuleFiles(file, requireSpecifier));
365
+ }
366
+ return createNamedMount(rawPrefix, mountedName, localModuleBindings, Boolean(calledIdentifier));
367
+ }
368
+
369
+ function createNamedMount(rawPrefix, mountedName, localModuleBindings, called = false) {
370
+ const prefix = normalizeRoutePath(rawPrefix);
371
+ if (!prefix || prefix === "/") return null;
372
+
373
+ const targetFiles = localModuleBindings.get(mountedName) || [];
374
+ if (targetFiles.length > 0) return { prefix, targetFiles };
375
+ if (!called && isRouterReceiver(mountedName)) return { prefix, targetFiles: [] };
376
+
377
+ return null;
378
+ }
379
+
380
+ function createMount(rawPrefix, targetFiles) {
381
+ const prefix = normalizeRoutePath(rawPrefix);
382
+ if (!prefix || prefix === "/" || targetFiles.length === 0) return null;
383
+ return { prefix, targetFiles };
384
+ }
385
+
386
+ function resolveLocalModuleFiles(file, specifier) {
387
+ if (!specifier || !specifier.startsWith(".")) return [];
388
+
389
+ const dirname = path.posix.dirname(normalizeFilePath(file));
390
+ const base = path.posix.normalize(path.posix.join(dirname, normalizeFilePath(specifier)));
391
+ if (ROUTE_EXTENSIONS.has(path.posix.extname(base))) return [base];
392
+
393
+ return [
394
+ ...[...ROUTE_EXTENSIONS].map((extension) => `${base}${extension}`),
395
+ ...[...ROUTE_EXTENSIONS].map((extension) => `${base}/index${extension}`)
396
+ ];
397
+ }
398
+
399
+ function dedupeMounts(mounts) {
400
+ const byKey = new Map();
401
+ for (const mount of mounts) {
402
+ const key = `${mount.prefix}|${mount.targetFiles.join(",")}`;
403
+ if (!byKey.has(key)) byKey.set(key, mount);
404
+ }
405
+ return [...byKey.values()].sort((a, b) => {
406
+ const byPrefix = a.prefix.localeCompare(b.prefix);
407
+ if (byPrefix !== 0) return byPrefix;
408
+ return a.targetFiles.join(",").localeCompare(b.targetFiles.join(","));
409
+ });
410
+ }
411
+
157
412
  function endpoint(method, routePath, file, type) {
158
413
  return {
159
414
  method: method.toUpperCase(),
@@ -177,6 +432,113 @@ function isSourceFile(file) {
177
432
  return ROUTE_EXTENSIONS.has(path.extname(normalized));
178
433
  }
179
434
 
435
+ function hasApplicableMount(file, mounts) {
436
+ return mounts.some((mount) => mountAppliesToFile(mount, file));
437
+ }
438
+
439
+ function mountAppliesToFile(mount, file) {
440
+ const normalized = normalizeFilePath(file);
441
+ if (mount.targetFiles.length > 0) return mount.targetFiles.includes(normalized);
442
+ return isLikelyRouterModulePath(normalized);
443
+ }
444
+
445
+ function isLikelyRouterModulePath(file) {
446
+ return (
447
+ file.startsWith("routes/") ||
448
+ file.startsWith("api/") ||
449
+ file.includes("/routes/") ||
450
+ file.includes("/api/")
451
+ );
452
+ }
453
+
454
+ function collectRouteReceivers(content) {
455
+ const all = new Set(["app", "server", "router", "apirouter", "routes", "route", "fastify"]);
456
+ const routers = new Set(["router", "apirouter", "routes", "route"]);
457
+ const expressAppRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:express|fastify)\s*\(/g;
458
+ const expressRouterRegex = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:express\s*\.\s*Router|Router)\s*\(/g;
459
+
460
+ let match;
461
+ while ((match = expressAppRegex.exec(content)) !== null) {
462
+ all.add(match[1].toLowerCase());
463
+ }
464
+
465
+ while ((match = expressRouterRegex.exec(content)) !== null) {
466
+ all.add(match[1].toLowerCase());
467
+ routers.add(match[1].toLowerCase());
468
+ }
469
+
470
+ return { all, routers };
471
+ }
472
+
473
+ function isRouteReceiver(value, receivers = collectRouteReceivers("")) {
474
+ const normalized = String(value || "").toLowerCase();
475
+ return receivers.all.has(normalized) || normalized.endsWith("router") || normalized.endsWith("server");
476
+ }
477
+
478
+ function isRouterReceiver(value, receivers = collectRouteReceivers("")) {
479
+ const normalized = String(value || "").toLowerCase();
480
+ return receivers.routers.has(normalized) || normalized.endsWith("router");
481
+ }
482
+
483
+ function isSastApiCandidate(file, content) {
484
+ const normalized = normalizeFilePath(file);
485
+ if (hasVendoredPathSegment(normalized)) return false;
486
+
487
+ return (
488
+ isServerOrApiFile(normalized) ||
489
+ normalized.startsWith("api/") ||
490
+ normalized.startsWith("routes/") ||
491
+ normalized.startsWith("handlers/") ||
492
+ normalized.startsWith("controllers/") ||
493
+ normalized.includes("/handlers/") ||
494
+ normalized.includes("/controllers/")
495
+ ) && hasApiHandlerEvidence(content);
496
+ }
497
+
498
+ function hasApiHandlerEvidence(content) {
499
+ return API_HANDLER_EVIDENCE.some((regex) => regex.test(content));
500
+ }
501
+
502
+ function hasVendoredPathSegment(file) {
503
+ return /(?:^|\/)(?:vendor|vendors|third_party|third-party)\//i.test(file);
504
+ }
505
+
506
+ function hasExplicitRouteDeclaration(content) {
507
+ const methods = HTTP_METHODS.join("|");
508
+ return new RegExp(`\\b[A-Za-z_$][\\w$]*\\s*\\.\\s*(?:${methods}|route)\\s*\\(`, "i").test(content);
509
+ }
510
+
511
+ function isFileConventionRoute(file) {
512
+ const normalized = normalizeFilePath(file);
513
+ return (
514
+ /(?:^|\/)(?:src\/)?app\/.+\/route\./.test(normalized) ||
515
+ /(?:^|\/)(?:src\/)?pages\/api\/.+\./.test(normalized) ||
516
+ /(?:^|\/)(?:src\/)?routes\/.+\/\+server\./.test(normalized) ||
517
+ /(?:^|\/)(?:src\/)?server\/api\/.+\./.test(normalized)
518
+ );
519
+ }
520
+
521
+ function inferRoutePathFromFile(file) {
522
+ const normalized = normalizeFilePath(file);
523
+ const withoutExtension = normalized.replace(/\.(?:js|jsx|ts|tsx|mjs|cjs|mts|cts)$/, "");
524
+ const patterns = [
525
+ { regex: /(?:^|\/)(?:src\/)?api\/(.+)$/, prefix: "/api/" },
526
+ { regex: /(?:^|\/)(?:src\/)?server\/api\/(.+)$/, prefix: "/api/" },
527
+ { regex: /(?:^|\/)(?:src\/)?routes\/(.+)$/, prefix: "/" },
528
+ { regex: /(?:^|\/)(?:src\/)?server\/routes\/(.+)$/, prefix: "/" },
529
+ { regex: /(?:^|\/)(?:src\/)?handlers\/(.+)$/, prefix: "/" },
530
+ { regex: /(?:^|\/)(?:src\/)?controllers\/(.+)$/, prefix: "/" }
531
+ ];
532
+
533
+ for (const { regex, prefix } of patterns) {
534
+ const match = withoutExtension.match(regex);
535
+ if (!match) continue;
536
+ return normalizeRoutePath(`${prefix}${routeSegmentsToPath(match[1])}`);
537
+ }
538
+
539
+ return null;
540
+ }
541
+
180
542
  function normalizeFilePath(file) {
181
543
  return String(file).replace(/\\/g, "/");
182
544
  }
@@ -187,11 +549,17 @@ function normalizeRoutePath(value) {
187
549
  return pathValue.replace(/\/+/g, "/").replace(/\/$/, "") || "/";
188
550
  }
189
551
 
552
+ function joinRoutePath(prefix, routePath) {
553
+ return normalizeRoutePath(`${prefix}/${routePath.replace(/^\//, "")}`);
554
+ }
555
+
190
556
  function routeSegmentsToPath(value) {
191
557
  return value
192
558
  .split("/")
193
559
  .filter(Boolean)
194
- .map((segment) => segment.replace(/^\[(\.\.\.)?(.+)]$/, ":$2"))
560
+ .filter((segment) => !segment.startsWith("(") && !segment.startsWith("@") && !segment.startsWith("_"))
561
+ .map((segment) => segment.replace(/^\[\[?\.\.\.(.+)]]?$/, ":$1").replace(/^\[(.+)]$/, ":$1"))
562
+ .filter((segment) => segment !== "index")
195
563
  .join("/");
196
564
  }
197
565
 
@@ -1,80 +1,94 @@
1
- import { getChalk } from "../cli/terminal.js";
1
+ import { getChalk, isFancyOutputEnabled } from '../cli/terminal.js';
2
2
 
3
3
  export function reportStressConsole(result, options = {}) {
4
- const colors = getChalk(options);
5
- const details = result.details || {};
4
+ const colors = getChalk(options);
5
+ const details = result.details || {};
6
+ const rich = isFancyOutputEnabled(options);
6
7
 
7
- if (!options.quiet) {
8
- process.stdout.write(`${colors.bold("ItWorksBut Stress")}\n\n`);
9
- process.stdout.write("Only run this against systems you own or are explicitly authorized to test.\n\n");
10
- process.stdout.write(`Target: ${details.target}\n`);
11
- process.stdout.write(`Duration: ${details.duration}s\n`);
12
- process.stdout.write(`Arrival rate: ${details.arrivalRate} req/s\n`);
13
- process.stdout.write(`Max virtual users: ${details.maxVusers}\n\n`);
8
+ if (!options.quiet) {
9
+ if (!rich) process.stdout.write(`${colors.bold('ItWorksBut Stress')}\n\n`);
10
+ process.stdout.write('Only run this against systems you own or are explicitly authorized to test.\n\n');
11
+ process.stdout.write(`Target: ${details.target}\n`);
12
+ process.stdout.write(`Duration: ${details.duration}s\n`);
13
+ process.stdout.write(`Arrival rate: ${details.arrivalRate} req/s\n`);
14
+ process.stdout.write(`Max virtual users: ${details.maxVusers}\n\n`);
14
15
 
15
- writeDiscovery(details, colors);
16
- writeEndpoints(details, colors);
17
- }
16
+ writeDiscovery(details, colors);
17
+ writeEndpoints(details, colors);
18
+ }
18
19
 
19
- writeSummary(result, colors);
20
+ writeSummary(result, colors);
20
21
  }
21
22
 
22
23
  function writeDiscovery(details, colors) {
23
- const found = details.endpointsFound || 0;
24
- const safe = details.safeEndpoints || 0;
25
- const skipped = details.skippedEndpoints?.length || 0;
26
- const unsafe = details.skippedEndpoints?.filter((endpoint) => endpoint.reason === "unsafe method").length || 0;
27
- const dynamic = details.skippedEndpoints?.filter((endpoint) => endpoint.reason === "dynamic route requires parameter").length || 0;
24
+ const found = details.endpointsFound || 0;
25
+ const safe = details.safeEndpoints || 0;
26
+ const skipped = details.skippedEndpoints?.length || 0;
27
+ const unsafe = details.skippedEndpoints?.filter(endpoint => endpoint.reason === 'unsafe method').length || 0;
28
+ const dynamic =
29
+ details.skippedEndpoints?.filter(endpoint => endpoint.reason === 'dynamic route requires parameter').length ||
30
+ 0;
28
31
 
29
- process.stdout.write(`${colors.green("✓")} Endpoint discovery: ${found} ${found === 1 ? "endpoint" : "endpoints"} found\n`);
30
- process.stdout.write(`${colors.green("")} Safe endpoints: ${safe} GET/HEAD ${safe === 1 ? "endpoint" : "endpoints"} selected\n`);
31
- if (skipped > 0) {
32
- process.stdout.write(`- Skipped: ${unsafe} unsafe methods, ${dynamic} dynamic routes\n`);
33
- }
34
- process.stdout.write("\n");
32
+ process.stdout.write(
33
+ `${colors.green('')} Endpoint discovery: ${found} ${found === 1 ? 'endpoint' : 'endpoints'} found\n`,
34
+ );
35
+ process.stdout.write(
36
+ `${colors.green('✓')} Safe endpoints: ${safe} GET/HEAD ${safe === 1 ? 'endpoint' : 'endpoints'} selected\n`,
37
+ );
38
+ if (skipped > 0) {
39
+ process.stdout.write(`- Skipped: ${unsafe} unsafe methods, ${dynamic} dynamic routes\n`);
40
+ }
41
+ process.stdout.write('\n');
35
42
  }
36
43
 
37
44
  function writeEndpoints(details, colors) {
38
- const testedEndpoints = details.testedEndpoints || [];
39
- if (testedEndpoints.length === 0) {
40
- process.stdout.write("Running Artillery: skipped\n\n");
41
- return;
42
- }
45
+ const testedEndpoints = details.testedEndpoints || [];
46
+ if (testedEndpoints.length === 0) {
47
+ process.stdout.write('Running Artillery: skipped\n\n');
48
+ return;
49
+ }
43
50
 
44
- process.stdout.write("Running Artillery: complete\n\n");
45
- for (const endpoint of testedEndpoints) {
46
- const symbol = endpoint.status === "pass" ? colors.green("✓") : endpoint.status === "warn" ? colors.yellow("⚠") : colors.red("✕");
47
- process.stdout.write(`${symbol} ${endpoint.method} ${endpoint.path}\n`);
48
- process.stdout.write(` p95: ${formatMs(endpoint.p95)}\n`);
49
- process.stdout.write(` p99: ${formatMs(endpoint.p99)}\n`);
50
- process.stdout.write(` errors: ${endpoint.errors}\n`);
51
- process.stdout.write(` error rate: ${formatPercent(endpoint.errorRate)}\n\n`);
52
- }
51
+ process.stdout.write('Running Artillery: complete\n\n');
52
+ for (const endpoint of testedEndpoints) {
53
+ const symbol =
54
+ endpoint.status === 'pass'
55
+ ? colors.green('✓')
56
+ : endpoint.status === 'warn'
57
+ ? colors.yellow('⚠')
58
+ : colors.red('✕');
59
+ process.stdout.write(`${symbol} ${endpoint.method} ${endpoint.path}\n`);
60
+ process.stdout.write(` p95: ${formatMs(endpoint.p95)}\n`);
61
+ process.stdout.write(` p99: ${formatMs(endpoint.p99)}\n`);
62
+ process.stdout.write(` errors: ${endpoint.errors}\n`);
63
+ process.stdout.write(` error rate: ${formatPercent(endpoint.errorRate)}\n\n`);
64
+ }
53
65
  }
54
66
 
55
67
  function writeSummary(result, colors) {
56
- const details = result.details || {};
57
- const warnings = details.warnings || 0;
58
- const failed = details.failed || 0;
59
- const tested = details.testedEndpoints?.length || 0;
60
- const skipped = details.skippedEndpoints?.length || 0;
68
+ const details = result.details || {};
69
+ const warnings = details.warnings || 0;
70
+ const failed = details.failed || 0;
71
+ const tested = details.testedEndpoints?.length || 0;
72
+ const skipped = details.skippedEndpoints?.length || 0;
61
73
 
62
- process.stdout.write("Summary:\n");
63
- process.stdout.write(`${colors.green("")} Tested endpoints: ${tested}\n`);
64
- process.stdout.write(`${colors.yellow("")} Warnings: ${warnings}\n`);
65
- process.stdout.write(`${colors.red("")} Failed: ${failed}\n`);
66
- process.stdout.write(`- Skipped: ${skipped}\n`);
67
- process.stdout.write(`- Status: ${result.status}\n`);
68
- process.stdout.write(`- ${result.summary}\n`);
69
- if (details.artilleryError) {
70
- process.stdout.write(`- Artillery error: ${details.artilleryError}\n`);
71
- }
74
+ process.stdout.write('Summary:\n');
75
+ process.stdout.write(`${colors.green('')} Tested endpoints: ${tested}\n`);
76
+ process.stdout.write(`${colors.yellow('')} Warnings: ${warnings}\n`);
77
+ process.stdout.write(`${colors.red('')} Failed: ${failed}\n`);
78
+ process.stdout.write(`- Skipped: ${skipped}\n`);
79
+ process.stdout.write(`- Status: ${result.status}\n`);
80
+ process.stdout.write(`- ${result.summary}\n`);
81
+ if (details.artilleryError) {
82
+ process.stdout.write(`- Artillery error: ${details.artilleryError}\n`);
83
+ }
72
84
  }
73
85
 
74
86
  function formatMs(value) {
75
- return value === null || value === undefined ? "n/a" : `${Math.round(value)} ms`;
87
+ return value === null || value === undefined ? 'n/a' : `${Math.round(value)} ms`;
76
88
  }
77
89
 
78
90
  function formatPercent(value) {
79
- return `${Number(value || 0).toFixed(Number.isInteger(value) ? 0 : 2).replace(/\.00$/, "")}%`;
91
+ return `${Number(value || 0)
92
+ .toFixed(Number.isInteger(value) ? 0 : 2)
93
+ .replace(/\.00$/, '')}%`;
80
94
  }
@@ -1,100 +1,99 @@
1
- const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0"]);
1
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0']);
2
2
 
3
3
  export const STRESS_DEFAULTS = {
4
- target: "http://localhost:3000",
5
- duration: 30,
6
- arrivalRate: 5,
7
- maxVusers: 50
4
+ target: 'http://localhost:3000',
5
+ duration: 30,
6
+ arrivalRate: 50,
7
+ maxVusers: 200,
8
8
  };
9
-
10
9
  export const STRESS_LIMITS = {
11
- duration: 300,
12
- arrivalRate: 50,
13
- maxVusers: 100
10
+ duration: 300,
11
+ arrivalRate: 100,
12
+ maxVusers: 400,
14
13
  };
15
14
 
16
15
  export function validateStressOptions(args = {}) {
17
- const target = normalizeTarget(args.target || STRESS_DEFAULTS.target);
18
- const parsedTarget = new URL(target);
19
- const targetPath = getExplicitTargetPath(parsedTarget);
20
- const duration = parsePositiveNumber(args.duration, STRESS_DEFAULTS.duration, "duration");
21
- const arrivalRate = parsePositiveNumber(args.arrivalRate, STRESS_DEFAULTS.arrivalRate, "arrival-rate");
22
- const maxVusers = parsePositiveInteger(args.maxVusers, STRESS_DEFAULTS.maxVusers, "max-vusers");
16
+ const target = normalizeTarget(args.target || STRESS_DEFAULTS.target);
17
+ const parsedTarget = new URL(target);
18
+ const targetPath = getExplicitTargetPath(parsedTarget);
19
+ const duration = parsePositiveNumber(args.duration, STRESS_DEFAULTS.duration, 'duration');
20
+ const arrivalRate = parsePositiveNumber(args.arrivalRate, STRESS_DEFAULTS.arrivalRate, 'arrival-rate');
21
+ const maxVusers = parsePositiveInteger(args.maxVusers, STRESS_DEFAULTS.maxVusers, 'max-vusers');
23
22
 
24
- assertWithinLimit(duration, STRESS_LIMITS.duration, "duration", "seconds");
25
- assertWithinLimit(arrivalRate, STRESS_LIMITS.arrivalRate, "arrival-rate", "requests/second");
26
- assertWithinLimit(maxVusers, STRESS_LIMITS.maxVusers, "max-vusers", "virtual users");
23
+ assertWithinLimit(duration, STRESS_LIMITS.duration, 'duration', 'seconds');
24
+ assertWithinLimit(arrivalRate, STRESS_LIMITS.arrivalRate, 'arrival-rate', 'requests/second');
25
+ assertWithinLimit(maxVusers, STRESS_LIMITS.maxVusers, 'max-vusers', 'virtual users');
27
26
 
28
- const local = isLocalTarget(target);
29
- if (!local && !args.iOwnThis) {
30
- throw new Error(
31
- "Refusing to stress-test an external target without --i-own-this. Only run this against systems you own or are explicitly authorized to test."
32
- );
33
- }
27
+ const local = isLocalTarget(target);
28
+ if (!local && !args.iOwnThis) {
29
+ throw new Error(
30
+ 'Refusing to stress-test an external target without --i-own-this. Only run this against systems you own or are explicitly authorized to test.',
31
+ );
32
+ }
34
33
 
35
- return {
36
- target,
37
- artilleryTarget: targetPath ? parsedTarget.origin : target,
38
- targetPath,
39
- duration,
40
- arrivalRate,
41
- maxVusers,
42
- iOwnThis: Boolean(args.iOwnThis),
43
- local
44
- };
34
+ return {
35
+ target,
36
+ artilleryTarget: targetPath ? parsedTarget.origin : target,
37
+ targetPath,
38
+ duration,
39
+ arrivalRate,
40
+ maxVusers,
41
+ iOwnThis: Boolean(args.iOwnThis),
42
+ local,
43
+ };
45
44
  }
46
45
 
47
46
  export function isLocalTarget(target) {
48
- let parsed;
49
- try {
50
- parsed = new URL(target);
51
- } catch {
52
- return false;
53
- }
47
+ let parsed;
48
+ try {
49
+ parsed = new URL(target);
50
+ } catch {
51
+ return false;
52
+ }
54
53
 
55
- return LOCAL_HOSTS.has(parsed.hostname.toLowerCase());
54
+ return LOCAL_HOSTS.has(parsed.hostname.toLowerCase());
56
55
  }
57
56
 
58
57
  function normalizeTarget(value) {
59
- let parsed;
60
- try {
61
- parsed = new URL(value);
62
- } catch {
63
- throw new Error(`Invalid stress target URL: ${value}`);
64
- }
58
+ let parsed;
59
+ try {
60
+ parsed = new URL(value);
61
+ } catch {
62
+ throw new Error(`Invalid stress target URL: ${value}`);
63
+ }
65
64
 
66
- if (!["http:", "https:"].includes(parsed.protocol)) {
67
- throw new Error("Stress target must use http:// or https://.");
68
- }
65
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
66
+ throw new Error('Stress target must use http:// or https://.');
67
+ }
69
68
 
70
- return parsed.toString().replace(/\/$/, "");
69
+ return parsed.toString().replace(/\/$/, '');
71
70
  }
72
71
 
73
72
  function getExplicitTargetPath(parsed) {
74
- const pathname = parsed.pathname.replace(/\/$/, "") || "/";
75
- if (pathname === "/" && !parsed.search) return null;
76
- return `${pathname}${parsed.search}`;
73
+ const pathname = parsed.pathname.replace(/\/$/, '') || '/';
74
+ if (pathname === '/' && !parsed.search) return null;
75
+ return `${pathname}${parsed.search}`;
77
76
  }
78
77
 
79
78
  function parsePositiveNumber(value, fallback, label) {
80
- if (value === undefined || value === null || value === "") return fallback;
81
- const number = Number(value);
82
- if (!Number.isFinite(number) || number <= 0) {
83
- throw new Error(`Invalid --${label}: expected a positive number.`);
84
- }
85
- return number;
79
+ if (value === undefined || value === null || value === '') return fallback;
80
+ const number = Number(value);
81
+ if (!Number.isFinite(number) || number <= 0) {
82
+ throw new Error(`Invalid --${label}: expected a positive number.`);
83
+ }
84
+ return number;
86
85
  }
87
86
 
88
87
  function parsePositiveInteger(value, fallback, label) {
89
- const number = parsePositiveNumber(value, fallback, label);
90
- if (!Number.isInteger(number)) {
91
- throw new Error(`Invalid --${label}: expected a positive integer.`);
92
- }
93
- return number;
88
+ const number = parsePositiveNumber(value, fallback, label);
89
+ if (!Number.isInteger(number)) {
90
+ throw new Error(`Invalid --${label}: expected a positive integer.`);
91
+ }
92
+ return number;
94
93
  }
95
94
 
96
95
  function assertWithinLimit(value, limit, label, unit) {
97
- if (value > limit) {
98
- throw new Error(`Refusing --${label} ${value}. Maximum allowed is ${limit} ${unit}.`);
99
- }
96
+ if (value > limit) {
97
+ throw new Error(`Refusing --${label} ${value}. Maximum allowed is ${limit} ${unit}.`);
98
+ }
100
99
  }