apex-auditor 0.3.6 → 0.3.7

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
@@ -20,7 +20,7 @@ Notes:
20
20
 
21
21
  - `init` can auto-detect your stack from `package.json` (Next.js, Nuxt, Remix/React Router, SvelteKit, SPA).
22
22
  - In monorepos, `init` can prompt you to pick an app/package under `apps/` or `packages/`.
23
- - `init` can auto-discover routes from the filesystem and top-up from `robots.txt`/`sitemap.xml`. You can optionally filter detected routes with include/exclude patterns and still add manual routes.
23
+ - `init` can auto-discover routes from the filesystem and top-up from `robots.txt`/`sitemap.xml`. You can optionally filter detected routes with include/exclude patterns and still add manual routes. For larger route sets, the wizard may default filtering to **Yes** and prefill common excludes (framework-specific).
24
24
  - For static sites, `init` can discover routes from HTML files under `dist/`, `build/`, `out/`, `public/`, and `src/`.
25
25
  - When using a localhost base URL (e.g. `http://localhost:3000`), make sure the dev server port matches the project you’re configuring (important when multiple projects are running).
26
26
 
@@ -34,6 +34,9 @@ Inside the interactive shell:
34
34
  - **headers** (security headers check; writes `.apex-auditor/headers.json`)
35
35
  - **console** (console errors + runtime exceptions; writes `.apex-auditor/console.json`)
36
36
  - **open** (open the latest HTML report)
37
+ - **pages** / **routes** (print configured pages/routes from the current config)
38
+ - **add-page** (interactive: append a page to `apex.config.json`)
39
+ - **rm-page** (interactive: remove a page from `apex.config.json`)
37
40
  - **init** (launch config wizard)
38
41
  - **config <path>** (switch config file)
39
42
 
@@ -132,11 +135,8 @@ The docs in `docs/` reflect the current shell-based workflow:
132
135
 
133
136
  ## Known issues
134
137
 
135
- - **Shell exits after init wizard**: in some environments, the process may exit after completing `init` in shell mode. Workaround: run `apex-auditor init` outside the shell, then re-run `apex-auditor shell`.
136
138
  - **Large-run Lighthouse stability**: very large audits (many page/device combinations) may show higher score variance than manual Lighthouse runs and can intermittently hit worker/Chrome disconnects. Workaround: reduce parallelism (e.g. `--stable`) and retry.
137
139
 
138
- The target for a truly stable release is after v0.3.7.
139
-
140
140
  ## License
141
141
 
142
142
  MIT
package/dist/cli.js CHANGED
@@ -1140,15 +1140,17 @@ export async function runAuditCli(argv, options) {
1140
1140
  const plannedRuns = overviewSample.config.runs ?? 1;
1141
1141
  const plannedSteps = plannedCombos * plannedRuns;
1142
1142
  const LARGE_RUN_COMBOS_THRESHOLD = 76;
1143
- const LARGE_RUN_PARALLEL_CAP = 2;
1143
+ const LARGE_RUN_PARALLEL_CAP = 4;
1144
1144
  const usingDefaultParallel = args.parallelOverride === undefined && presetParallel === undefined && config.parallel === undefined;
1145
- const autoStableLargeRun = plannedCombos >= LARGE_RUN_COMBOS_THRESHOLD && !args.stable && usingDefaultParallel;
1145
+ const isLargeRun = plannedCombos >= LARGE_RUN_COMBOS_THRESHOLD;
1146
+ const autoStableLargeRun = isLargeRun && !args.stable && usingDefaultParallel;
1146
1147
  const resolvedConfigForRun = autoStableLargeRun
1147
1148
  ? { ...overviewSample.config, parallel: Math.min(overviewSample.config.parallel ?? DEFAULT_PARALLEL, LARGE_RUN_PARALLEL_CAP) }
1148
1149
  : overviewSample.config;
1149
1150
  const maxSteps = args.maxSteps ?? DEFAULT_MAX_STEPS;
1150
1151
  const maxCombos = args.maxCombos ?? DEFAULT_MAX_COMBOS;
1151
1152
  const isTty = typeof process !== "undefined" && process.stdout?.isTTY === true;
1153
+ const LARGE_RUN_HINT_COMBOS_THRESHOLD = 40;
1152
1154
  const exceeds = plannedSteps > maxSteps || plannedCombos > maxCombos;
1153
1155
  const useColor = shouldUseColor(args.ci, args.colorMode);
1154
1156
  if (args.plan) {
@@ -1165,15 +1167,9 @@ export async function runAuditCli(argv, options) {
1165
1167
  });
1166
1168
  return;
1167
1169
  }
1168
- if (isTty && !args.ci && !args.jsonOutput) {
1169
- const tipLines = [
1170
- "Tip: use --plan to preview run size before starting.",
1171
- "Note: runs-per-combo is always 1; rerun the same command to compare results.",
1172
- "If parallel mode flakes (worker disconnects), retry with --stable (forces parallel=1).",
1173
- ];
1170
+ if (isTty && !args.ci && !args.jsonOutput && plannedCombos >= LARGE_RUN_HINT_COMBOS_THRESHOLD) {
1174
1171
  // eslint-disable-next-line no-console
1175
- console.log(boxifyWithSeparators(tipLines));
1176
- printDivider();
1172
+ console.log(`Tip: large run (${plannedCombos} combos). Use --plan to preview, and retry with --stable if parallel mode flakes.`);
1177
1173
  }
1178
1174
  if (autoStableLargeRun && isTty && !args.ci && !args.jsonOutput) {
1179
1175
  // eslint-disable-next-line no-console
@@ -1249,17 +1245,14 @@ export async function runAuditCli(argv, options) {
1249
1245
  showParallel: args.showParallel,
1250
1246
  onlyCategories,
1251
1247
  signal: abortController.signal,
1252
- onAfterWarmUp: stopSpinner,
1248
+ onAfterWarmUp: startAuditSpinner,
1253
1249
  onProgress: ({ completed, total, path, device, etaMs }) => {
1254
1250
  if (!process.stdout.isTTY) {
1255
1251
  return;
1256
1252
  }
1257
1253
  const etaText = etaMs !== undefined ? ` | ETA ${formatEtaText(etaMs)}` : "";
1258
- const message = `* Running audit (Lighthouse) page ${completed}/${total} — ${path} [${device}]${etaText}`;
1259
- const padded = message.padEnd(lastProgressLine?.length ?? message.length, " ");
1260
- process.stdout.write(`\r${padded}`);
1261
- lastProgressLine = padded;
1262
- updateSpinnerMessage(`Running audit (Lighthouse) page ${completed}/${total}`);
1254
+ startAuditSpinner();
1255
+ updateSpinnerMessage(`Running audit (Lighthouse) page ${completed}/${total} ${path} [${device}]${etaText}`);
1263
1256
  },
1264
1257
  });
1265
1258
  }
@@ -347,6 +347,11 @@ async function createChromeSession(chromePort) {
347
347
  "--safebrowsing-disable-auto-update",
348
348
  "--password-store=basic",
349
349
  "--use-mock-keychain",
350
+ // Stability flags for parallel runs
351
+ "--disable-hang-monitor",
352
+ "--disable-ipc-flooding-protection",
353
+ "--disable-domain-reliability",
354
+ "--disable-component-update",
350
355
  ],
351
356
  });
352
357
  return {
@@ -741,7 +746,14 @@ function isTransientLighthouseError(error) {
741
746
  message.includes("LanternError") ||
742
747
  message.includes("top level events") ||
743
748
  message.includes("CDP") ||
744
- message.includes("disconnected"));
749
+ message.includes("disconnected") ||
750
+ // Additional transient errors for better retry handling
751
+ message.includes("WebSocket") ||
752
+ message.includes("webSocket") ||
753
+ message.includes("fetch failed") ||
754
+ message.includes("ECONNREFUSED") ||
755
+ message.includes("ECONNRESET") ||
756
+ message.includes("socket hang up"));
745
757
  }
746
758
  async function runSingleAuditWithRetry({ task, sessionRef, updateProgress, maxRetries, }) {
747
759
  let attempt = 0;
@@ -13,7 +13,14 @@ function isTransientLighthouseError(error) {
13
13
  message.includes("top level events") ||
14
14
  message.includes("CDP") ||
15
15
  message.includes("disconnected") ||
16
- message.includes("ApexAuditor timeout"));
16
+ message.includes("ApexAuditor timeout") ||
17
+ // Additional transient errors for better retry handling
18
+ message.includes("WebSocket") ||
19
+ message.includes("webSocket") ||
20
+ message.includes("fetch failed") ||
21
+ message.includes("ECONNREFUSED") ||
22
+ message.includes("ECONNRESET") ||
23
+ message.includes("socket hang up"));
17
24
  }
18
25
  async function createChromeSession() {
19
26
  const userDataDir = await mkdtemp(join(tmpdir(), "apex-auditor-chrome-"));
@@ -39,6 +46,11 @@ async function createChromeSession() {
39
46
  "--safebrowsing-disable-auto-update",
40
47
  "--password-store=basic",
41
48
  "--use-mock-keychain",
49
+ // Stability flags for parallel runs
50
+ "--disable-hang-monitor",
51
+ "--disable-ipc-flooding-protection",
52
+ "--disable-domain-reliability",
53
+ "--disable-component-update",
42
54
  ],
43
55
  });
44
56
  return {
@@ -213,6 +225,7 @@ function send(message) {
213
225
  }
214
226
  async function main() {
215
227
  const maxRetries = 3;
228
+ // Recycle Chrome periodically to prevent memory leaks
216
229
  const maxTasksPerChrome = 10;
217
230
  const sessionRef = { session: await createChromeSession() };
218
231
  let tasksSinceChromeStart = 0;
@@ -238,7 +251,7 @@ async function main() {
238
251
  await sessionRef.session.close();
239
252
  }
240
253
  catch {
241
- return;
254
+ // Ignore close errors
242
255
  }
243
256
  sessionRef.session = await createChromeSession();
244
257
  tasksSinceChromeStart = 0;
package/dist/shell-cli.js CHANGED
@@ -13,8 +13,10 @@ import { runMeasureCli } from "./measure-cli.js";
13
13
  import { runWizardCli } from "./wizard-cli.js";
14
14
  import { runCleanCli } from "./clean-cli.js";
15
15
  import { runUninstallCli } from "./uninstall-cli.js";
16
+ import { loadConfig } from "./config.js";
16
17
  import { pathExists } from "./fs-utils.js";
17
18
  import { renderPanel } from "./ui/render-panel.js";
19
+ import { renderTable } from "./ui/render-table.js";
18
20
  import { startSpinner, stopSpinner } from "./spinner.js";
19
21
  import { UiTheme } from "./ui/ui-theme.js";
20
22
  const SESSION_DIR_NAME = ".apex-auditor";
@@ -124,6 +126,148 @@ async function runConsoleAuditFromShell(session, args) {
124
126
  async function writeJsonFile(absolutePath, value) {
125
127
  await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
126
128
  }
129
+ async function loadCurrentConfig(configPath) {
130
+ const loaded = await loadConfig({ configPath });
131
+ return { absolutePath: loaded.configPath, config: loaded.config };
132
+ }
133
+ function formatPagesTable(pages) {
134
+ const headers = ["#", "path", "label", "devices"];
135
+ const rows = pages.map((p, index) => {
136
+ return [String(index + 1), p.path, p.label, p.devices.join(",")];
137
+ });
138
+ return renderTable({ headers, rows });
139
+ }
140
+ async function printConfiguredPages(session) {
141
+ const exists = await pathExists(session.configPath);
142
+ if (!exists) {
143
+ // eslint-disable-next-line no-console
144
+ console.log(`Config not found at ${session.configPath}. Run 'init' to create a config, or use 'config <path>' to point to one.`);
145
+ return;
146
+ }
147
+ const { absolutePath, config } = await loadCurrentConfig(session.configPath);
148
+ const lines = [];
149
+ lines.push(`${theme.dim("Config")}: ${absolutePath}`);
150
+ lines.push(`${theme.dim("Base URL")}: ${config.baseUrl}`);
151
+ lines.push("");
152
+ lines.push(formatPagesTable(config.pages));
153
+ // eslint-disable-next-line no-console
154
+ console.log(renderPanel({ title: theme.bold("Pages"), lines }));
155
+ }
156
+ function parseDevices(raw) {
157
+ const parts = raw
158
+ .split(",")
159
+ .map((p) => p.trim().toLowerCase())
160
+ .filter((p) => p.length > 0);
161
+ if (parts.length === 0) {
162
+ return undefined;
163
+ }
164
+ const devices = [];
165
+ for (const part of parts) {
166
+ if (part === "mobile" || part === "desktop") {
167
+ devices.push(part);
168
+ continue;
169
+ }
170
+ return undefined;
171
+ }
172
+ return [...new Set(devices)];
173
+ }
174
+ function askLine(rl, question) {
175
+ return new Promise((resolvePromise) => {
176
+ rl.question(question, (value) => resolvePromise(value));
177
+ });
178
+ }
179
+ async function addPageInteractive(rl, session) {
180
+ const exists = await pathExists(session.configPath);
181
+ if (!exists) {
182
+ // eslint-disable-next-line no-console
183
+ console.log(`Config not found at ${session.configPath}. Run 'init' first.`);
184
+ return;
185
+ }
186
+ const { absolutePath, config } = await loadCurrentConfig(session.configPath);
187
+ // eslint-disable-next-line no-console
188
+ console.log(formatPagesTable(config.pages));
189
+ const rawPath = (await askLine(rl, "New page path (must start with /): ")).trim();
190
+ if (!rawPath.startsWith("/")) {
191
+ // eslint-disable-next-line no-console
192
+ console.log("Cancelled: path must start with '/'.");
193
+ return;
194
+ }
195
+ const label = (await askLine(rl, "Label (optional): ")).trim() || rawPath;
196
+ const rawDevices = (await askLine(rl, "Devices (comma-separated: mobile,desktop) [mobile,desktop]: ")).trim();
197
+ const devices = parseDevices(rawDevices.length > 0 ? rawDevices : "mobile,desktop") ?? ["mobile", "desktop"];
198
+ const nextPages = [...config.pages, { path: rawPath, label, devices }];
199
+ const nextConfig = { ...config, pages: nextPages };
200
+ await writeJsonFile(absolutePath, nextConfig);
201
+ // eslint-disable-next-line no-console
202
+ console.log(`Added page ${rawPath}.`);
203
+ }
204
+ async function removePageInteractive(rl, session, args) {
205
+ const exists = await pathExists(session.configPath);
206
+ if (!exists) {
207
+ // eslint-disable-next-line no-console
208
+ console.log(`Config not found at ${session.configPath}. Run 'init' first.`);
209
+ return;
210
+ }
211
+ const { absolutePath, config } = await loadCurrentConfig(session.configPath);
212
+ if (config.pages.length === 0) {
213
+ // eslint-disable-next-line no-console
214
+ console.log("No pages configured.");
215
+ return;
216
+ }
217
+ const byPath = args[0]?.trim();
218
+ const resolvedIndex = (() => {
219
+ if (byPath && byPath.startsWith("/")) {
220
+ const index = config.pages.findIndex((p) => p.path === byPath);
221
+ return index >= 0 ? index : undefined;
222
+ }
223
+ if (byPath && /^\d+$/.test(byPath)) {
224
+ const idx = parseInt(byPath, 10) - 1;
225
+ return idx >= 0 && idx < config.pages.length ? idx : undefined;
226
+ }
227
+ return undefined;
228
+ })();
229
+ const indexToRemove = resolvedIndex ?? (() => {
230
+ // eslint-disable-next-line no-console
231
+ console.log(formatPagesTable(config.pages));
232
+ return undefined;
233
+ })();
234
+ const finalIndex = indexToRemove ?? (() => {
235
+ return undefined;
236
+ })();
237
+ let resolvedFinalIndex = finalIndex;
238
+ if (resolvedFinalIndex === undefined) {
239
+ const answer = (await askLine(rl, "Remove which page? Enter # or /path (blank to cancel): ")).trim();
240
+ if (answer.length === 0) {
241
+ // eslint-disable-next-line no-console
242
+ console.log("Cancelled.");
243
+ return;
244
+ }
245
+ if (answer.startsWith("/")) {
246
+ const idx = config.pages.findIndex((p) => p.path === answer);
247
+ resolvedFinalIndex = idx >= 0 ? idx : undefined;
248
+ }
249
+ else if (/^\d+$/.test(answer)) {
250
+ const idx = parseInt(answer, 10) - 1;
251
+ resolvedFinalIndex = idx >= 0 && idx < config.pages.length ? idx : undefined;
252
+ }
253
+ }
254
+ if (resolvedFinalIndex === undefined) {
255
+ // eslint-disable-next-line no-console
256
+ console.log("No matching page.");
257
+ return;
258
+ }
259
+ if (config.pages.length <= 1) {
260
+ // eslint-disable-next-line no-console
261
+ console.log("Cancelled: config must contain at least one page.");
262
+ return;
263
+ }
264
+ const removed = config.pages[resolvedFinalIndex];
265
+ const nextPages = config.pages.filter((_p, i) => i !== resolvedFinalIndex);
266
+ const nextConfig = { ...config, pages: nextPages };
267
+ await writeJsonFile(absolutePath, nextConfig);
268
+ // eslint-disable-next-line no-console
269
+ console.log(`Removed page ${removed.path}.`);
270
+ }
127
271
  function getSessionPaths(projectRoot) {
128
272
  const dir = resolve(projectRoot, SESSION_DIR_NAME);
129
273
  const file = join(dir, SESSION_FILE_NAME);
@@ -364,6 +508,10 @@ function printHelp() {
364
508
  lines.push(`${theme.cyan("console")} Console errors + runtime exceptions audit (headless Chrome)`);
365
509
  lines.push("");
366
510
  lines.push(theme.bold("Other commands"));
511
+ lines.push(`${theme.cyan("pages")} Print configured pages/routes from the current config`);
512
+ lines.push(`${theme.cyan("routes")} Alias for pages`);
513
+ lines.push(`${theme.cyan("add-page")} Add a page to apex.config.json (interactive)`);
514
+ lines.push(`${theme.cyan("rm-page [#|/path]")} Remove a page from apex.config.json (interactive)`);
367
515
  lines.push(`${theme.cyan("clean")} Remove ApexAuditor artifacts (reports/cache and optionally config)`);
368
516
  lines.push(`${theme.cyan("uninstall")} Remove .apex-auditor and the current config file`);
369
517
  lines.push(`${theme.cyan("open")} Open the last HTML report (or .apex-auditor/report.html)`);
@@ -431,6 +579,10 @@ function createCompleter() {
431
579
  "links",
432
580
  "headers",
433
581
  "console",
582
+ "pages",
583
+ "routes",
584
+ "add-page",
585
+ "rm-page",
434
586
  "clean",
435
587
  "uninstall",
436
588
  "open",
@@ -690,6 +842,10 @@ async function handleShellCommand(projectRoot, session, command) {
690
842
  if (command.id === "exit" || command.id === "quit") {
691
843
  return { session, shouldExit: true };
692
844
  }
845
+ if (command.id === "pages" || command.id === "routes") {
846
+ await printConfiguredPages(session);
847
+ return { session, shouldExit: false };
848
+ }
693
849
  if (command.id === "audit") {
694
850
  const nextSession = await runAuditFromShell(projectRoot, session, command.args);
695
851
  return { session: nextSession, shouldExit: false };
@@ -857,6 +1013,28 @@ export async function runShellCli(argv) {
857
1013
  return;
858
1014
  }
859
1015
  let command = parseShellCommand(line);
1016
+ const isEditCommand = command.id === "add-page" || command.id === "rm-page";
1017
+ if (isEditCommand && process.stdin.isTTY) {
1018
+ suppressInput = true;
1019
+ rl.pause();
1020
+ try {
1021
+ if (command.id === "add-page") {
1022
+ await addPageInteractive(rl, session);
1023
+ }
1024
+ else {
1025
+ await removePageInteractive(rl, session, command.args);
1026
+ }
1027
+ }
1028
+ finally {
1029
+ if (!rlClosed) {
1030
+ rl.resume();
1031
+ suppressInput = false;
1032
+ rl.setPrompt(buildPrompt(session));
1033
+ rl.prompt();
1034
+ }
1035
+ }
1036
+ return;
1037
+ }
860
1038
  if (command.id === "clean" && !command.args.includes("--yes") && !command.args.includes("-y") && process.stdin.isTTY) {
861
1039
  const targets = resolveCleanTargets({ projectRoot, session, args: command.args });
862
1040
  const ok = await confirmCleanInShell({ rl, targets });
@@ -908,12 +1086,11 @@ export async function runShellCli(argv) {
908
1086
  // eslint-disable-next-line no-console
909
1087
  console.log("Starting config wizard...");
910
1088
  await runWizardInShell();
911
- if (!rlClosed) {
912
- // eslint-disable-next-line no-console
913
- console.log(`Ready. Next: ${theme.cyan("measure")} or ${theme.cyan("audit")}.`);
914
- rl.setPrompt(buildPrompt(session));
915
- rl.prompt();
916
- }
1089
+ // eslint-disable-next-line no-console
1090
+ console.log(`Ready. Next: ${theme.cyan("measure")} or ${theme.cyan("audit")}.`);
1091
+ // Force readline recreation by closing current instance - this ensures
1092
+ // the shell stays alive after the wizard completes even if prompts closed stdin
1093
+ rl.close();
917
1094
  return;
918
1095
  }
919
1096
  rl.pause();
@@ -82,13 +82,13 @@ const pageQuestions = [
82
82
  const addFirstPageQuestion = {
83
83
  type: "confirm",
84
84
  name: "value",
85
- message: "Add your first page to audit?",
85
+ message: "No pages were auto-detected. Add a page to audit now? (You can always edit apex.config.json later)",
86
86
  initial: true,
87
87
  };
88
88
  const addMorePagesQuestion = {
89
89
  type: "confirm",
90
90
  name: "value",
91
- message: "Add another page to audit?",
91
+ message: "Add another page to audit? (Optional — you can edit apex.config.json later)",
92
92
  initial: false,
93
93
  };
94
94
  const projectRootQuestion = {
@@ -130,6 +130,39 @@ const routeFilterQuestions = [
130
130
  initial: "",
131
131
  },
132
132
  ];
133
+ const ROUTE_FILTER_DEFAULT_ON_THRESHOLD = 15;
134
+ function buildSuggestedExcludePatterns(profile) {
135
+ if (profile === "next") {
136
+ return ["/_next/*", "/api/*"];
137
+ }
138
+ if (profile === "nuxt") {
139
+ return ["/__nuxt/*", "/api/*"];
140
+ }
141
+ if (profile === "sveltekit") {
142
+ return ["/__data.json*", "/api/*"];
143
+ }
144
+ if (profile === "remix") {
145
+ return ["/api/*"];
146
+ }
147
+ return [];
148
+ }
149
+ function buildRouteFilterQuestions(params) {
150
+ const excludeInitial = params.suggestedExclude.join(",");
151
+ return [
152
+ {
153
+ type: "text",
154
+ name: "include",
155
+ message: "Include patterns (comma-separated, optional)",
156
+ initial: "",
157
+ },
158
+ {
159
+ type: "text",
160
+ name: "exclude",
161
+ message: "Exclude patterns (comma-separated, optional)",
162
+ initial: excludeInitial,
163
+ },
164
+ ];
165
+ }
133
166
  function handleCancel() {
134
167
  console.log("Wizard cancelled. No config written.");
135
168
  throw new WizardAbortError("cancelled");
@@ -415,6 +448,10 @@ async function collectSinglePage() {
415
448
  }
416
449
  async function collectPages(initialPages) {
417
450
  const pages = [...initialPages];
451
+ if (pages.length > 0) {
452
+ console.log("Tip: You can add/remove pages later by editing apex.config.json or re-running init.");
453
+ return pages;
454
+ }
418
455
  while (true) {
419
456
  const shouldAdd = await confirmAddPage(pages.length > 0);
420
457
  if (!shouldAdd) {
@@ -452,8 +489,13 @@ async function maybeDetectPages(params) {
452
489
  return [];
453
490
  }
454
491
  console.log(`Detected ${combined.length} route(s) using auto-discovery.`);
455
- const shouldFilter = await ask(routeFilterConfirmQuestion);
456
- const filtered = shouldFilter.value ? await filterDetectedRoutes(combined) : combined;
492
+ const suggestedExclude = buildSuggestedExcludePatterns(params.profile);
493
+ const initialFilter = combined.length >= ROUTE_FILTER_DEFAULT_ON_THRESHOLD || suggestedExclude.length > 0;
494
+ const confirmQuestion = { ...routeFilterConfirmQuestion, initial: initialFilter };
495
+ const shouldFilter = await ask(confirmQuestion);
496
+ const filtered = shouldFilter.value
497
+ ? await filterDetectedRoutes({ routes: combined, profile: params.profile, suggestedExclude })
498
+ : combined;
457
499
  if (filtered.length === 0) {
458
500
  console.log("No routes remain after filtering. Add pages manually.");
459
501
  return [];
@@ -463,13 +505,14 @@ async function maybeDetectPages(params) {
463
505
  }
464
506
  return selectDetectedRoutes(filtered);
465
507
  }
466
- async function filterDetectedRoutes(routes) {
467
- const answers = await ask(routeFilterQuestions);
508
+ async function filterDetectedRoutes(params) {
509
+ const questions = buildRouteFilterQuestions({ profile: params.profile, suggestedExclude: params.suggestedExclude });
510
+ const answers = await ask(questions);
468
511
  const includePatterns = splitPatterns(answers.include);
469
512
  const excludePatterns = splitPatterns(answers.exclude);
470
513
  const includeRegexes = includePatterns.map((pattern) => compileGlob(pattern));
471
514
  const excludeRegexes = excludePatterns.map((pattern) => compileGlob(pattern));
472
- return routes.filter((route) => {
515
+ return params.routes.filter((route) => {
473
516
  const included = includeRegexes.length === 0 ? true : includeRegexes.some((regex) => regex.test(route.path));
474
517
  const excluded = excludeRegexes.some((regex) => regex.test(route.path));
475
518
  return included && !excluded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-auditor",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "description": "CLI to run structured Lighthouse audits (Performance, Accessibility, Best Practices, SEO) across routes.",
6
6
  "type": "module",