apex-auditor 0.2.9 → 0.3.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
@@ -1,151 +1,101 @@
1
1
  # ApexAuditor
2
2
 
3
- ApexAuditor is a small, framework-agnostic Lighthouse runner that gives you **fast, structured insights** across multiple pages and devices.
3
+ ApexAuditor is a **measure-first** performance + metrics assistant.
4
4
 
5
- It focuses on:
5
+ Use it to:
6
6
 
7
- - **Multi-page, multi-device audits**: run Lighthouse across your key flows in one shot.
8
- - **Framework flexibility**: works with any stack that serves HTTP (Next.js, Remix, Vite/React, SvelteKit, Rails, static sites, etc.).
9
- - **Smart route discovery**: auto-detects routes for Next.js (App/Pages), Remix, SvelteKit, and can crawl generic SPAs.
10
- - **Developer-friendly reports**: readable console output, Markdown tables, HTML reports, and JSON summaries for CI.
11
- - **Configurable throttling**: tune CPU/network throttling for accuracy vs speed trade-offs.
12
- - **Parallel execution**: speed up batch testing with multiple Chrome instances.
7
+ - **Measure fast** (CDP-based): LCP/CLS/INP + screenshot + console errors.
8
+ - **Audit deep** (Lighthouse): Performance + Accessibility + Best Practices + SEO.
9
+ - **Review results** in a clean, structured console output and an HTML report.
13
10
 
14
- ---
11
+ ## Most common commands
15
12
 
16
- ## Example output
17
-
18
- Terminal summary:
19
-
20
- ![Example terminal output 1](./public/example_output_1.png)
21
-
22
- ![Example terminal output 2](./public/example_output_2.png)
23
-
24
- Wizard route selection:
25
-
26
- ![Wizard screenshot](./public/wizard_1.png)
27
-
28
- ---
29
-
30
- ## Installation
31
-
32
- Install as a dev dependency (recommended):
13
+ From your web project root:
33
14
 
34
15
  ```bash
35
- pnpm add -D apex-auditor
36
- # or
37
- npm install --save-dev apex-auditor
16
+ pnpm dlx apex-auditor@latest
38
17
  ```
39
18
 
40
- You can also run it without installing by using your package manager's "dlx"/"npx" style command, for example:
19
+ Inside the interactive shell:
41
20
 
42
- ```bash
43
- pnpm dlx apex-auditor@latest wizard
44
- ```
21
+ - **measure**
22
+ - **audit**
23
+ - **open** (open the latest HTML report)
24
+ - **init** (launch config wizard)
25
+ - **config <path>** (switch config file)
45
26
 
46
- ---
27
+ Cancel long-running commands:
47
28
 
48
- ## Common commands
29
+ - **Esc** (returns you to the shell prompt)
49
30
 
50
- All commands are available as a CLI named `apex-auditor` once installed.
31
+ ## Install
51
32
 
52
- ### Quickstart (auto-detect routes and run a one-off audit)
33
+ Install as a dev dependency (recommended):
53
34
 
54
35
  ```bash
55
- apex-auditor quickstart --base-url http://localhost:3000
36
+ pnpm add -D apex-auditor
56
37
  ```
57
38
 
58
- ### Wizard (interactive config with route auto-detection)
39
+ Or run without installing:
59
40
 
60
41
  ```bash
61
- apex-auditor wizard
42
+ pnpm dlx apex-auditor@latest
62
43
  ```
63
44
 
64
- The wizard can detect routes for:
65
-
66
- - Next.js (App Router / Pages Router)
67
- - Remix
68
- - SvelteKit
69
- - Single Page Apps (Vite/CRA/etc., via HTML crawl)
70
-
71
- ### Audit (run using an existing config)
72
-
73
- ```bash
74
- apex-auditor audit --config apex.config.json
75
- ```
45
+ ## Outputs
76
46
 
77
- **CLI flags:**
47
+ All outputs are written under `.apex-auditor/` in your project.
78
48
 
79
- | Flag | Description |
80
- |------|-------------|
81
- | `--ci` | Enable CI mode with budgets and non-zero exit codes |
82
- | `--no-color` / `--color` | Control ANSI colours in console output |
83
- | `--log-level <level>` | Override Lighthouse log level (`silent`, `error`, `info`, `verbose`) |
84
- | `--throttling <method>` | Throttling method: `simulate` (fast) or `devtools` (accurate) |
85
- | `--cpu-slowdown <n>` | CPU slowdown multiplier (1-20, default: 4) |
86
- | `--parallel <n>` | Number of pages to audit in parallel (1-10) |
87
- | `--warm-up` | Perform warm-up requests before auditing |
88
- | `--open` | Auto-open HTML report in browser after audit |
89
- | `--json` | Output JSON to stdout (for piping) |
90
- | `--mobile-only` | Only audit mobile device configurations |
91
- | `--desktop-only` | Only audit desktop device configurations |
49
+ ### `audit` outputs
92
50
 
93
- ---
51
+ - `summary.json`
52
+ - `summary.md`
53
+ - `report.html`
54
+ - `accessibility-summary.json`
55
+ - `accessibility/` (axe-core artifacts per page/device)
94
56
 
95
- ## Output files
57
+ Notes:
96
58
 
97
- After each audit, results are saved to `.apex-auditor/`:
59
+ - **Runs-per-combo is always 1**. Re-run the same command to compare results.
60
+ - During an audit you will see a runtime progress line like `page X/Y — /path [device] | ETA ...`.
61
+ - After `audit` completes, type `open` to open the latest HTML report.
98
62
 
99
- - `summary.json` – structured JSON results
100
- - `summary.md` – Markdown table
101
- - `report.html` – visual HTML report with score circles and metrics
63
+ ### `measure` outputs
102
64
 
103
- ---
65
+ - `measure-summary.json`
66
+ - `measure/` (screenshots and artifacts)
104
67
 
105
68
  ## Configuration
106
69
 
107
- Example `apex.config.json`:
70
+ ApexAuditor reads `apex.config.json` by default.
71
+
72
+ Example:
108
73
 
109
74
  ```json
110
75
  {
111
76
  "baseUrl": "http://localhost:3000",
112
- "runs": 3,
113
- "throttlingMethod": "devtools",
77
+ "throttlingMethod": "simulate",
114
78
  "cpuSlowdownMultiplier": 4,
115
- "parallel": 2,
79
+ "parallel": 4,
116
80
  "warmUp": true,
117
81
  "pages": [
118
82
  { "path": "/", "label": "home", "devices": ["mobile", "desktop"] },
119
- { "path": "/docs", "label": "docs", "devices": ["mobile"] }
83
+ { "path": "/docs", "label": "docs", "devices": ["desktop"] }
120
84
  ],
121
85
  "budgets": {
122
- "categories": { "performance": 80, "accessibility": 90 },
123
- "metrics": { "lcpMs": 2500, "inpMs": 200 }
86
+ "categories": { "performance": 80, "accessibility": 90, "bestPractices": 90, "seo": 90 },
87
+ "metrics": { "lcpMs": 2500, "inpMs": 200, "cls": 0.1 }
124
88
  }
125
89
  }
126
90
  ```
127
91
 
128
- ---
129
-
130
- ## Metrics tracked
131
-
132
- - **LCP** (Largest Contentful Paint)
133
- - **FCP** (First Contentful Paint)
134
- - **TBT** (Total Blocking Time)
135
- - **CLS** (Cumulative Layout Shift)
136
- - **INP** (Interaction to Next Paint) - Core Web Vital
137
-
138
- ---
139
-
140
- ## Further documentation
141
-
142
- For detailed guides, configuration options, and CI examples, see the `docs/` directory:
92
+ ## Documentation
143
93
 
144
- - `docs/getting-started.md` installation, quickstart, wizard, and audit flows.
145
- - `docs/configuration-and-routes.md` – `apex.config.json` schema and route detection details.
146
- - `docs/cli-and-ci.md` – CLI flags, CI mode, budgets, and example workflows.
94
+ The docs in `docs/` reflect the current shell-based workflow:
147
95
 
148
- ---
96
+ - `docs/getting-started.md`
97
+ - `docs/configuration-and-routes.md`
98
+ - `docs/cli-and-ci.md`
149
99
 
150
100
  ## License
151
101
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,152 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { launch as launchChrome } from "chrome-launcher";
5
+ import { createAxeScript } from "./axe-script.js";
6
+ import { CdpClient } from "./cdp-client.js";
7
+ import { buildUrl } from "./url.js";
8
+ const DEFAULT_PARALLEL = 2;
9
+ const DEFAULT_TIMEOUT_MS = 30_000;
10
+ const CHROME_FLAGS = [
11
+ "--headless=new",
12
+ "--disable-gpu",
13
+ "--no-sandbox",
14
+ "--disable-dev-shm-usage",
15
+ "--disable-extensions",
16
+ "--disable-default-apps",
17
+ "--no-first-run",
18
+ "--no-default-browser-check",
19
+ ];
20
+ async function launchChromeSession() {
21
+ const chrome = await launchChrome({
22
+ chromeFlags: [...CHROME_FLAGS],
23
+ logLevel: "silent",
24
+ });
25
+ const close = async () => {
26
+ try {
27
+ await chrome.kill();
28
+ }
29
+ catch {
30
+ /* noop */
31
+ }
32
+ };
33
+ return { port: chrome.port, close };
34
+ }
35
+ async function fetchJsonVersion(port) {
36
+ return await new Promise((resolveVersion, rejectVersion) => {
37
+ const req = httpRequest({ host: "localhost", port, path: "/json/version", method: "GET" }, (res) => {
38
+ let data = "";
39
+ res.on("data", (chunk) => {
40
+ data += chunk.toString();
41
+ });
42
+ res.on("end", () => {
43
+ try {
44
+ const parsed = JSON.parse(data);
45
+ resolveVersion(parsed);
46
+ }
47
+ catch (error) {
48
+ rejectVersion(error instanceof Error ? error : new Error("Failed to parse json/version"));
49
+ }
50
+ });
51
+ });
52
+ req.on("error", (error) => rejectVersion(error));
53
+ req.end();
54
+ });
55
+ }
56
+ async function createTargetSession(client) {
57
+ const created = await client.send("Target.createTarget", { url: "about:blank" });
58
+ const attached = await client.send("Target.attachToTarget", { targetId: created.targetId, flatten: true });
59
+ return { targetId: created.targetId, sessionId: attached.sessionId };
60
+ }
61
+ async function applyDevice(client, sessionId, device) {
62
+ if (device === "mobile") {
63
+ await client.send("Emulation.setDeviceMetricsOverride", { width: 375, height: 667, deviceScaleFactor: 2, mobile: true }, sessionId);
64
+ await client.send("Emulation.setUserAgentOverride", {
65
+ userAgent: "Mozilla/5.0 (Linux; Android 12; Pixel 6 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36",
66
+ }, sessionId);
67
+ return;
68
+ }
69
+ await client.send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false }, sessionId);
70
+ }
71
+ async function runAxeForPage(params) {
72
+ const { baseUrl, path, label, device, query, timeoutMs, artifactsDir, client } = params;
73
+ const url = buildUrl({ baseUrl, path, query });
74
+ const axeScript = createAxeScript();
75
+ let runtimeErrorMessage;
76
+ const session = await createTargetSession(client);
77
+ try {
78
+ await applyDevice(client, session.sessionId, device);
79
+ await client.send("Page.enable", {}, session.sessionId);
80
+ await client.send("Runtime.enable", {}, session.sessionId);
81
+ const navResponse = await client.send("Page.navigate", { url }, session.sessionId);
82
+ if (navResponse.errorText) {
83
+ throw new Error(navResponse.errorText);
84
+ }
85
+ await client.waitForEventForSession("Page.loadEventFired", session.sessionId, timeoutMs);
86
+ await client.send("Runtime.evaluate", { expression: axeScript, awaitPromise: false }, session.sessionId);
87
+ const evaluation = await client.send("Runtime.evaluate", {
88
+ expression: "(() => { return (globalThis.__axeCore && globalThis.__axeCore.run) ? globalThis.__axeCore.run() : { violations: [] }; })()",
89
+ awaitPromise: true,
90
+ returnByValue: true,
91
+ }, session.sessionId);
92
+ const value = evaluation.result?.value;
93
+ const violations = value && typeof value === "object" && Array.isArray(value.violations)
94
+ ? value.violations
95
+ : [];
96
+ const baseName = path.replace(/\//g, "_").replace(/^_/, "") || "page";
97
+ const artifactPath = resolve(artifactsDir, `${baseName}_${device}_axe.json`);
98
+ await writeFile(artifactPath, JSON.stringify({ url, violations }, null, 2), "utf8");
99
+ return { url, path, label, device, violations };
100
+ }
101
+ catch (error) {
102
+ runtimeErrorMessage = error instanceof Error ? error.message : String(error);
103
+ return { url, path, label, device, violations: [], runtimeErrorMessage };
104
+ }
105
+ finally {
106
+ try {
107
+ await client.send("Target.closeTarget", { targetId: session.targetId });
108
+ }
109
+ catch {
110
+ /* noop */
111
+ }
112
+ }
113
+ }
114
+ export async function runAccessibilityAudit(params) {
115
+ const { config, configPath, parallelOverride, timeoutMs, artifactsDir } = params;
116
+ await mkdir(artifactsDir, { recursive: true });
117
+ const tasks = config.pages.flatMap((page) => page.devices.map((device) => ({ ...page, device })));
118
+ const startedAt = new Date().toISOString();
119
+ const comboCount = tasks.length;
120
+ const resolvedParallel = Math.max(1, Math.min(parallelOverride ?? DEFAULT_PARALLEL, 4));
121
+ const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
122
+ const chrome = await launchChromeSession();
123
+ try {
124
+ const version = await fetchJsonVersion(chrome.port);
125
+ const client = new CdpClient(version.webSocketDebuggerUrl);
126
+ await client.connect();
127
+ const results = [];
128
+ for (let i = 0; i < tasks.length; i += resolvedParallel) {
129
+ const slice = tasks.slice(i, i + resolvedParallel);
130
+ const batchResults = await Promise.all(slice.map((task) => runAxeForPage({
131
+ baseUrl: config.baseUrl,
132
+ path: task.path,
133
+ label: task.label,
134
+ device: task.device,
135
+ query: config.query,
136
+ timeoutMs: timeout,
137
+ artifactsDir,
138
+ client,
139
+ })));
140
+ results.push(...batchResults);
141
+ }
142
+ const completedAt = new Date().toISOString();
143
+ const elapsedMs = Date.parse(completedAt) - Date.parse(startedAt);
144
+ return {
145
+ meta: { configPath, comboCount, startedAt, completedAt, elapsedMs },
146
+ results,
147
+ };
148
+ }
149
+ finally {
150
+ await chrome.close();
151
+ }
152
+ }
@@ -0,0 +1,26 @@
1
+ const AXE_CDN_URL = "https://cdn.jsdelivr.net/npm/axe-core@4.8.4/axe.min.js";
2
+ export function createAxeScript() {
3
+ return `(function() {
4
+ return new Promise((resolve) => {
5
+ const runAxe = () => {
6
+ const axe = (globalThis).axe;
7
+ if (axe && axe.run) {
8
+ (globalThis).__axeCore = axe;
9
+ axe.run().then(resolve).catch(() => resolve({ violations: [] }));
10
+ return;
11
+ }
12
+ resolve({ violations: [] });
13
+ };
14
+ if ((globalThis).axe) {
15
+ runAxe();
16
+ return;
17
+ }
18
+ const script = document.createElement('script');
19
+ script.src = '${AXE_CDN_URL}';
20
+ script.async = true;
21
+ script.onload = runAxe;
22
+ script.onerror = () => resolve({ violations: [] });
23
+ document.head.appendChild(script);
24
+ });
25
+ })();`;
26
+ }
package/dist/bin.js CHANGED
@@ -2,45 +2,172 @@
2
2
  import { runAuditCli } from "./cli.js";
3
3
  import { runWizardCli } from "./wizard-cli.js";
4
4
  import { runQuickstartCli } from "./quickstart-cli.js";
5
+ import { runShellCli } from "./shell-cli.js";
6
+ import { runMeasureCli } from "./measure-cli.js";
5
7
  function parseBinArgs(argv) {
6
8
  const rawCommand = argv[2];
7
- if (rawCommand === undefined || rawCommand === "help" || rawCommand === "--help" || rawCommand === "-h") {
9
+ if (rawCommand === undefined) {
10
+ return { command: "shell", argv };
11
+ }
12
+ if (rawCommand === "help" || rawCommand === "--help" || rawCommand === "-h") {
8
13
  return { command: "help", argv };
9
14
  }
10
- if (rawCommand === "audit" || rawCommand === "wizard" || rawCommand === "quickstart") {
15
+ if (rawCommand === "shell") {
16
+ const commandArgv = ["node", "apex-auditor", ...argv.slice(3)];
17
+ return { command: "shell", argv: commandArgv };
18
+ }
19
+ if (rawCommand === "audit" ||
20
+ rawCommand === "measure" ||
21
+ rawCommand === "wizard" ||
22
+ rawCommand === "quickstart" ||
23
+ rawCommand === "guide" ||
24
+ rawCommand === "init") {
11
25
  const commandArgv = ["node", "apex-auditor", ...argv.slice(3)];
12
26
  return { command: rawCommand, argv: commandArgv };
13
27
  }
14
28
  return { command: "help", argv };
15
29
  }
16
- function printHelp() {
30
+ function printHelp(topic) {
31
+ if (topic === "topics") {
32
+ console.log([
33
+ "Help topics:",
34
+ " budgets Budget schema and CI behaviour",
35
+ " configs apex.config.json fields and defaults",
36
+ " ci CI mode, exit codes, budgets",
37
+ " topics This list",
38
+ "Examples:",
39
+ " apex-auditor help budgets",
40
+ " apex-auditor help configs",
41
+ " apex-auditor help ci",
42
+ ].join("\n"));
43
+ return;
44
+ }
45
+ if (topic === "budgets") {
46
+ console.log([
47
+ "Budgets:",
48
+ " - categories: performance, accessibility, bestPractices, seo (0-100 scores)",
49
+ " - metrics: lcpMs, fcpMs, tbtMs, cls, inpMs (numeric thresholds)",
50
+ "Example apex.config.json budget:",
51
+ ' "budgets": {',
52
+ ' "categories": { "performance": 80, "accessibility": 90 },',
53
+ ' "metrics": { "lcpMs": 2500, "inpMs": 200 }',
54
+ " }",
55
+ "",
56
+ "CI behaviour:",
57
+ " - --ci exits non-zero if any budget is under/over its limit",
58
+ " - Summary prints violations; JSON includes full results",
59
+ ].join("\n"));
60
+ return;
61
+ }
62
+ if (topic === "configs") {
63
+ console.log([
64
+ "Config (apex.config.json):",
65
+ " baseUrl (required) Base URL of your running site",
66
+ " query Query string appended to every path (e.g., ?lhci=1)",
67
+ " buildId Build identifier used for incremental cache keys",
68
+ " runs Runs per page/device (must be 1; rerun command to compare)",
69
+ " auditTimeoutMs Per-audit timeout in milliseconds (prevents hung runs from stalling)",
70
+ " throttlingMethod simulate | devtools (default simulate)",
71
+ " cpuSlowdownMultiplier CPU slowdown (default 4)",
72
+ " parallel Workers (default auto up to 4, respects CPU/memory)",
73
+ " warmUp true/false to warm cache before auditing (bounded concurrency)",
74
+ " incremental (deprecated default) Set in config but only active when --incremental is passed",
75
+ " pages Array of { path, label, devices: [mobile|desktop] }",
76
+ " budgets Optional { categories, metrics } thresholds",
77
+ "",
78
+ "Tip:",
79
+ " For best accuracy and stable throughput, run audits against a production server (e.g., Next.js: next build && next start)",
80
+ ].join("\n"));
81
+ return;
82
+ }
83
+ if (topic === "ci") {
84
+ console.log([
85
+ "CI mode:",
86
+ " - Use --ci to enable budgets and non-zero exit codes on failures",
87
+ " - Exit code 1 if any budget fails or a runtime error occurs",
88
+ " - Use --json to pipe results to other tools",
89
+ " - Combine with --parallel and --throttling for speed/accuracy trade-offs",
90
+ ].join("\n"));
91
+ return;
92
+ }
17
93
  console.log([
18
94
  "ApexAuditor CLI",
19
95
  "",
20
96
  "Usage:",
97
+ " apex-auditor # interactive shell (default)",
21
98
  " apex-auditor quickstart --base-url <url> [--project-root <path>]",
22
99
  " apex-auditor wizard [--config <path>]",
23
100
  " apex-auditor audit [--config <path>] [--ci] [--no-color|--color] [--log-level <level>]",
101
+ " apex-auditor guide (alias of wizard) interactive flow with tips for non-technical users",
102
+ " apex-auditor shell # same as default entrypoint",
24
103
  "",
25
104
  "Commands:",
105
+ " shell Start interactive shell (audit/open/diff/preset/help/exit)",
26
106
  " quickstart Detect routes and run a one-off audit with sensible defaults",
27
- " wizard Run interactive config wizard",
28
- " audit Run Lighthouse audits using apex.config.json",
29
- " help Show this help message",
107
+ " wizard Run interactive config wizard",
108
+ " guide Same as wizard, with inline tips for non-technical users",
109
+ " audit Run Lighthouse audits using apex.config.json",
110
+ " measure Fast batch metrics (CDP-based, non-Lighthouse)",
111
+ " help Show this help message",
30
112
  "",
31
113
  "Options (audit):",
32
114
  " --ci Enable CI mode with budgets and non-zero exit code on failure",
115
+ " --fail-on-budget Exit non-zero if budgets fail even outside CI",
33
116
  " --no-color Disable ANSI colours in console output (default in CI mode)",
34
117
  " --color Force ANSI colours in console output",
35
118
  " --log-level <lvl> Override Lighthouse log level: silent|error|info|verbose",
119
+ " --stable Flake-resistant mode: forces parallel=1, good for big suites or flaky runners",
36
120
  " --mobile-only Run audits only for 'mobile' devices defined in the config",
37
121
  " --desktop-only Run audits only for 'desktop' devices defined in the config",
122
+ " --parallel <n> Override parallel workers (1-10). Default auto-tunes from CPU/memory.",
123
+ " --audit-timeout-ms <ms> Per-audit timeout in milliseconds (prevents hung runs from stalling)",
124
+ " --plan Print resolved settings + run size estimate and exit without auditing",
125
+ " --max-steps <n> Safety limit: refuse/prompt if planned Lighthouse runs exceed this (default 120)",
126
+ " --max-combos <n> Safety limit: refuse/prompt if planned page/device combos exceed this (default 60)",
127
+ " --yes, -y Auto-confirm large runs (bypass safety prompt)",
128
+ " --changed-only Run only pages whose paths match files in git diff --name-only (working tree diff)",
129
+ " --rerun-failing Re-run only combos that failed in the previous summary (runtime errors or perf<90)",
130
+ " --accessibility-pass Run a fast axe-core accessibility sweep after audits (lightweight, CDP-based)",
131
+ " --webhook-url <url> Send a JSON webhook with regressions/budgets/accessibility (regressions-only summary)",
132
+ " --show-parallel Print the resolved parallel workers before running.",
133
+ " --incremental Reuse cached results for unchanged combos (requires --build-id). Opt-in; off by default.",
134
+ " --build-id <id> Build identifier used as the cache key boundary for --incremental",
135
+ " --overview Preset: quick overview (runs=1) and samples a small set of combos unless --yes.",
136
+ " --overview-combos <n> Overview sampling size (default 10).",
137
+ " --quick Preset: fast feedback (runs=1) without changing throttling defaults",
138
+ " --accurate Preset: devtools throttling + warm-up + stability-first (parallel=1 unless overridden)",
139
+ " --open Open the HTML report after the run.",
140
+ "",
141
+ "Outputs:",
142
+ " - Writes .apex-auditor/summary.json, summary.md, report.html",
143
+ " - Prints a file:// link to the HTML report after completion",
144
+ "",
145
+ "Quick start:",
146
+ " pnpm dlx apex-auditor@latest wizard # guided setup",
147
+ " pnpm dlx apex-auditor@latest audit # run with apex.config.json",
148
+ "",
149
+ "Defaults:",
150
+ " - Parallel auto-tunes from CPU/memory (up to 4 by default)",
151
+ " - Throttling: simulate, CPU slowdown: 4, Runs: 1",
152
+ " - Stable: use --stable to force serial runs when parallel flakes (e.g., TargetClose/Lantern errors)",
153
+ " - Accuracy tip: run against a production server (e.g., Next.js: next build && next start)",
154
+ " - Incremental: use --incremental --build-id <id> to skip unchanged audits between runs",
155
+ " - Presets: choose only one of --fast, --quick, --accurate",
156
+ "",
157
+ "More help:",
158
+ " apex-auditor help topics",
159
+ " apex-auditor help budgets",
38
160
  ].join("\n"));
39
161
  }
40
162
  export async function runBin(argv) {
41
163
  const parsed = parseBinArgs(argv);
42
164
  if (parsed.command === "help") {
43
- printHelp();
165
+ const topic = argv[3];
166
+ printHelp(topic);
167
+ return;
168
+ }
169
+ if (parsed.command === "shell") {
170
+ await runShellCli(parsed.argv);
44
171
  return;
45
172
  }
46
173
  if (parsed.command === "quickstart") {
@@ -48,11 +175,60 @@ export async function runBin(argv) {
48
175
  return;
49
176
  }
50
177
  if (parsed.command === "audit") {
51
- await runAuditCli(parsed.argv);
178
+ try {
179
+ await runAuditCli(parsed.argv);
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ if (message.includes("ENOENT")) {
184
+ // eslint-disable-next-line no-console
185
+ console.error("Config file not found. Run `apex-auditor init` to create a config or set one with `config <path>`.");
186
+ return;
187
+ }
188
+ throw error;
189
+ }
190
+ if (process.stdin.isTTY && process.stdout.isTTY) {
191
+ // eslint-disable-next-line no-console
192
+ console.log("\nAudit completed. Press Ctrl+C to exit, or enter another command (type help for options).");
193
+ await runShellCli(["node", "apex-auditor"]);
194
+ }
52
195
  return;
53
196
  }
54
- if (parsed.command === "wizard") {
197
+ if (parsed.command === "measure") {
198
+ try {
199
+ await runMeasureCli(parsed.argv);
200
+ }
201
+ catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ if (message.includes("ENOENT")) {
204
+ // eslint-disable-next-line no-console
205
+ console.error("Config file not found. Run `apex-auditor init` to create a config or set one with `config <path>`.");
206
+ return;
207
+ }
208
+ throw error;
209
+ }
210
+ if (process.stdin.isTTY && process.stdout.isTTY) {
211
+ // eslint-disable-next-line no-console
212
+ console.log("\nMeasure run completed. Press Ctrl+C to exit, or enter another command (type help for options).");
213
+ await runShellCli(["node", "apex-auditor"]);
214
+ }
215
+ return;
216
+ }
217
+ if (parsed.command === "init") {
55
218
  await runWizardCli(parsed.argv);
219
+ if (process.stdin.isTTY && process.stdout.isTTY) {
220
+ // eslint-disable-next-line no-console
221
+ console.log("\nConfig initialized. Press Ctrl+C to exit, or enter another command (type help for options).");
222
+ await runShellCli(["node", "apex-auditor"]);
223
+ }
224
+ return;
225
+ }
226
+ if (parsed.command === "wizard" || parsed.command === "guide") {
227
+ await runWizardCli(parsed.argv);
228
+ if (process.stdin.isTTY && process.stdout.isTTY) {
229
+ await runShellCli(["node", "apex-auditor"]);
230
+ }
231
+ return;
56
232
  }
57
233
  }
58
234
  void runBin(process.argv).catch((error) => {