apex-auditor 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/bin.js +117 -45
- package/dist/bundle-cli.js +158 -0
- package/dist/clean-cli.js +159 -0
- package/dist/cli.js +44 -19
- package/dist/console-cli.js +341 -0
- package/dist/dev-server-guidance.js +148 -0
- package/dist/headers-cli.js +301 -0
- package/dist/health-cli.js +252 -0
- package/dist/lighthouse-runner.js +116 -14
- package/dist/links-cli.js +393 -0
- package/dist/measure-cli.js +154 -18
- package/dist/measure-runner.js +83 -3
- package/dist/route-detectors.js +124 -0
- package/dist/shell-cli.js +387 -39
- package/dist/sitemap-discovery.js +136 -0
- package/dist/spinner.js +3 -0
- package/dist/wizard-cli.js +275 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,10 +16,20 @@ From your web project root:
|
|
|
16
16
|
pnpm dlx apex-auditor@latest
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Notes:
|
|
20
|
+
|
|
21
|
+
- `init` can auto-detect your stack from `package.json` (Next.js, Nuxt, Remix/React Router, SvelteKit, SPA).
|
|
22
|
+
- In monorepos, `init` can prompt you to pick an app/package under `apps/` or `packages/`.
|
|
23
|
+
|
|
19
24
|
Inside the interactive shell:
|
|
20
25
|
|
|
21
26
|
- **measure**
|
|
22
27
|
- **audit**
|
|
28
|
+
- **bundle** (scan build output sizes; writes `.apex-auditor/bundle-audit.json`)
|
|
29
|
+
- **health** (HTTP status/latency checks; writes `.apex-auditor/health.json`)
|
|
30
|
+
- **links** (broken links crawl; writes `.apex-auditor/links.json`)
|
|
31
|
+
- **headers** (security headers check; writes `.apex-auditor/headers.json`)
|
|
32
|
+
- **console** (console errors + runtime exceptions; writes `.apex-auditor/console.json`)
|
|
23
33
|
- **open** (open the latest HTML report)
|
|
24
34
|
- **init** (launch config wizard)
|
|
25
35
|
- **config <path>** (switch config file)
|
|
@@ -65,6 +75,26 @@ Notes:
|
|
|
65
75
|
- `measure-summary.json`
|
|
66
76
|
- `measure/` (screenshots and artifacts)
|
|
67
77
|
|
|
78
|
+
### `bundle` outputs
|
|
79
|
+
|
|
80
|
+
- `bundle-audit.json`
|
|
81
|
+
|
|
82
|
+
### `health` outputs
|
|
83
|
+
|
|
84
|
+
- `health.json`
|
|
85
|
+
|
|
86
|
+
### `links` outputs
|
|
87
|
+
|
|
88
|
+
- `links.json`
|
|
89
|
+
|
|
90
|
+
### `headers` outputs
|
|
91
|
+
|
|
92
|
+
- `headers.json`
|
|
93
|
+
|
|
94
|
+
### `console` outputs
|
|
95
|
+
|
|
96
|
+
- `console.json`
|
|
97
|
+
|
|
68
98
|
## Configuration
|
|
69
99
|
|
|
70
100
|
ApexAuditor reads `apex.config.json` by default.
|
package/dist/bin.js
CHANGED
|
@@ -4,6 +4,12 @@ import { runWizardCli } from "./wizard-cli.js";
|
|
|
4
4
|
import { runQuickstartCli } from "./quickstart-cli.js";
|
|
5
5
|
import { runShellCli } from "./shell-cli.js";
|
|
6
6
|
import { runMeasureCli } from "./measure-cli.js";
|
|
7
|
+
import { runBundleCli } from "./bundle-cli.js";
|
|
8
|
+
import { runHealthCli } from "./health-cli.js";
|
|
9
|
+
import { runLinksCli } from "./links-cli.js";
|
|
10
|
+
import { runHeadersCli } from "./headers-cli.js";
|
|
11
|
+
import { runConsoleCli } from "./console-cli.js";
|
|
12
|
+
import { runCleanCli } from "./clean-cli.js";
|
|
7
13
|
function parseBinArgs(argv) {
|
|
8
14
|
const rawCommand = argv[2];
|
|
9
15
|
if (rawCommand === undefined) {
|
|
@@ -18,6 +24,12 @@ function parseBinArgs(argv) {
|
|
|
18
24
|
}
|
|
19
25
|
if (rawCommand === "audit" ||
|
|
20
26
|
rawCommand === "measure" ||
|
|
27
|
+
rawCommand === "bundle" ||
|
|
28
|
+
rawCommand === "health" ||
|
|
29
|
+
rawCommand === "links" ||
|
|
30
|
+
rawCommand === "headers" ||
|
|
31
|
+
rawCommand === "console" ||
|
|
32
|
+
rawCommand === "clean" ||
|
|
21
33
|
rawCommand === "wizard" ||
|
|
22
34
|
rawCommand === "quickstart" ||
|
|
23
35
|
rawCommand === "guide" ||
|
|
@@ -108,6 +120,12 @@ function printHelp(topic) {
|
|
|
108
120
|
" guide Same as wizard, with inline tips for non-technical users",
|
|
109
121
|
" audit Run Lighthouse audits using apex.config.json",
|
|
110
122
|
" measure Fast batch metrics (CDP-based, non-Lighthouse)",
|
|
123
|
+
" bundle Bundle size audit (Next.js .next/ or dist/ build output)",
|
|
124
|
+
" health HTTP status + latency checks for configured routes",
|
|
125
|
+
" links Broken links audit (sitemap + HTML link extraction)",
|
|
126
|
+
" headers Security headers audit",
|
|
127
|
+
" console Console errors + runtime exceptions audit (headless Chrome)",
|
|
128
|
+
" clean Remove ApexAuditor artifacts (reports/cache and optionally config)",
|
|
111
129
|
" help Show this help message",
|
|
112
130
|
"",
|
|
113
131
|
"Options (audit):",
|
|
@@ -127,7 +145,7 @@ function printHelp(topic) {
|
|
|
127
145
|
" --yes, -y Auto-confirm large runs (bypass safety prompt)",
|
|
128
146
|
" --changed-only Run only pages whose paths match files in git diff --name-only (working tree diff)",
|
|
129
147
|
" --rerun-failing Re-run only combos that failed in the previous summary (runtime errors or perf<90)",
|
|
130
|
-
" --accessibility-pass
|
|
148
|
+
" --accessibility-pass Opt-in: run a fast axe-core accessibility sweep after audits (lightweight, CDP-based)",
|
|
131
149
|
" --webhook-url <url> Send a JSON webhook with regressions/budgets/accessibility (regressions-only summary)",
|
|
132
150
|
" --show-parallel Print the resolved parallel workers before running.",
|
|
133
151
|
" --incremental Reuse cached results for unchanged combos (requires --build-id). Opt-in; off by default.",
|
|
@@ -138,6 +156,57 @@ function printHelp(topic) {
|
|
|
138
156
|
" --accurate Preset: devtools throttling + warm-up + stability-first (parallel=1 unless overridden)",
|
|
139
157
|
" --open Open the HTML report after the run.",
|
|
140
158
|
"",
|
|
159
|
+
"Options (measure):",
|
|
160
|
+
" --mobile-only Run measure only for 'mobile' devices defined in the config",
|
|
161
|
+
" --desktop-only Run measure only for 'desktop' devices defined in the config",
|
|
162
|
+
" --parallel <n> Override parallel workers (1-10).",
|
|
163
|
+
" --timeout-ms <ms> Per-navigation timeout in milliseconds (default 60000)",
|
|
164
|
+
" --screenshots Opt-in: save a screenshot per combo (slower; writes .apex-auditor/measure/*.png)",
|
|
165
|
+
" --json Print JSON summary to stdout",
|
|
166
|
+
"",
|
|
167
|
+
"Options (bundle):",
|
|
168
|
+
" --project-root <path> Project root to scan (default cwd)",
|
|
169
|
+
" --top <n> Show top N largest files (default 15)",
|
|
170
|
+
" --json Print JSON report to stdout",
|
|
171
|
+
"",
|
|
172
|
+
"Options (health):",
|
|
173
|
+
" --config <path> Config path (default apex.config.json)",
|
|
174
|
+
" --parallel <n> Parallel requests (default auto)",
|
|
175
|
+
" --timeout-ms <ms> Per-request timeout (default 20000)",
|
|
176
|
+
" --json Print JSON report to stdout",
|
|
177
|
+
"",
|
|
178
|
+
"Options (links):",
|
|
179
|
+
" --config <path> Config path (default apex.config.json)",
|
|
180
|
+
" --sitemap <url> Override sitemap URL (default <baseUrl>/sitemap.xml)",
|
|
181
|
+
" --parallel <n> Parallel requests (default auto)",
|
|
182
|
+
" --timeout-ms <ms> Per-request timeout (default 20000)",
|
|
183
|
+
" --max-urls <n> Limit total URLs checked (default 200)",
|
|
184
|
+
" --json Print JSON report to stdout",
|
|
185
|
+
"",
|
|
186
|
+
"Options (headers):",
|
|
187
|
+
" --config <path> Config path (default apex.config.json)",
|
|
188
|
+
" --parallel <n> Parallel requests (default auto)",
|
|
189
|
+
" --timeout-ms <ms> Per-request timeout (default 20000)",
|
|
190
|
+
" --json Print JSON report to stdout",
|
|
191
|
+
"",
|
|
192
|
+
"Options (console):",
|
|
193
|
+
" --config <path> Config path (default apex.config.json)",
|
|
194
|
+
" --parallel <n> Parallel workers (default auto)",
|
|
195
|
+
" --timeout-ms <ms> Per-navigation timeout (default 60000)",
|
|
196
|
+
" --max-events <n> Cap captured events per combo (default 50)",
|
|
197
|
+
" --json Print JSON report to stdout",
|
|
198
|
+
"",
|
|
199
|
+
"Options (clean):",
|
|
200
|
+
" --project-root <path> Project root (default cwd)",
|
|
201
|
+
" --config-path <path> Config file path relative to project root (default apex.config.json)",
|
|
202
|
+
" --reports Remove .apex-auditor/ (default)",
|
|
203
|
+
" --no-reports Keep .apex-auditor/",
|
|
204
|
+
" --remove-config Remove config file",
|
|
205
|
+
" --all Remove reports and config",
|
|
206
|
+
" --dry-run Print planned removals without deleting",
|
|
207
|
+
" --yes, -y Skip confirmation prompt",
|
|
208
|
+
" --json Print JSON report to stdout",
|
|
209
|
+
"",
|
|
141
210
|
"Outputs:",
|
|
142
211
|
" - Writes .apex-auditor/summary.json, summary.md, report.html",
|
|
143
212
|
" - Prints a file:// link to the HTML report after completion",
|
|
@@ -159,6 +228,9 @@ function printHelp(topic) {
|
|
|
159
228
|
" apex-auditor help budgets",
|
|
160
229
|
].join("\n"));
|
|
161
230
|
}
|
|
231
|
+
function isInteractiveTty() {
|
|
232
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
233
|
+
}
|
|
162
234
|
export async function runBin(argv) {
|
|
163
235
|
const parsed = parseBinArgs(argv);
|
|
164
236
|
if (parsed.command === "help") {
|
|
@@ -172,63 +244,63 @@ export async function runBin(argv) {
|
|
|
172
244
|
}
|
|
173
245
|
if (parsed.command === "quickstart") {
|
|
174
246
|
await runQuickstartCli(parsed.argv);
|
|
247
|
+
if (isInteractiveTty()) {
|
|
248
|
+
await runShellCli(["node", "apex-auditor"]);
|
|
249
|
+
}
|
|
175
250
|
return;
|
|
176
251
|
}
|
|
177
|
-
|
|
178
|
-
|
|
252
|
+
const runOnce = async () => {
|
|
253
|
+
if (parsed.command === "audit") {
|
|
179
254
|
await runAuditCli(parsed.argv);
|
|
255
|
+
return;
|
|
180
256
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
257
|
+
if (parsed.command === "measure") {
|
|
258
|
+
await runMeasureCli(parsed.argv);
|
|
259
|
+
return;
|
|
189
260
|
}
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
await runShellCli(["node", "apex-auditor"]);
|
|
261
|
+
if (parsed.command === "bundle") {
|
|
262
|
+
await runBundleCli(parsed.argv);
|
|
263
|
+
return;
|
|
194
264
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
await runMeasureCli(parsed.argv);
|
|
265
|
+
if (parsed.command === "health") {
|
|
266
|
+
await runHealthCli(parsed.argv);
|
|
267
|
+
return;
|
|
200
268
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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;
|
|
269
|
+
if (parsed.command === "links") {
|
|
270
|
+
await runLinksCli(parsed.argv);
|
|
271
|
+
return;
|
|
209
272
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await runShellCli(["node", "apex-auditor"]);
|
|
273
|
+
if (parsed.command === "headers") {
|
|
274
|
+
await runHeadersCli(parsed.argv);
|
|
275
|
+
return;
|
|
214
276
|
}
|
|
215
|
-
|
|
277
|
+
if (parsed.command === "console") {
|
|
278
|
+
await runConsoleCli(parsed.argv);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (parsed.command === "clean") {
|
|
282
|
+
await runCleanCli(parsed.argv);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (parsed.command === "init" || parsed.command === "wizard" || parsed.command === "guide") {
|
|
286
|
+
await runWizardCli(parsed.argv);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
await runOnce();
|
|
216
292
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (
|
|
293
|
+
catch (error) {
|
|
294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
295
|
+
if (message.includes("ENOENT")) {
|
|
220
296
|
// eslint-disable-next-line no-console
|
|
221
|
-
console.
|
|
222
|
-
|
|
297
|
+
console.error("Config file not found. Run `apex-auditor init` to create a config or set one with `config <path>`.");
|
|
298
|
+
return;
|
|
223
299
|
}
|
|
224
|
-
|
|
300
|
+
throw error;
|
|
225
301
|
}
|
|
226
|
-
if (
|
|
227
|
-
await
|
|
228
|
-
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
229
|
-
await runShellCli(["node", "apex-auditor"]);
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
302
|
+
if (isInteractiveTty()) {
|
|
303
|
+
await runShellCli(["node", "apex-auditor"]);
|
|
232
304
|
}
|
|
233
305
|
}
|
|
234
306
|
void runBin(process.argv).catch((error) => {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { renderPanel } from "./ui/render-panel.js";
|
|
4
|
+
import { renderTable } from "./ui/render-table.js";
|
|
5
|
+
import { UiTheme } from "./ui/ui-theme.js";
|
|
6
|
+
import { stopSpinner } from "./spinner.js";
|
|
7
|
+
const NO_COLOR = Boolean(process.env.NO_COLOR) || process.env.CI === "true";
|
|
8
|
+
const theme = new UiTheme({ noColor: NO_COLOR });
|
|
9
|
+
function formatBytes(bytes) {
|
|
10
|
+
const kb = bytes / 1024;
|
|
11
|
+
if (kb < 1024) {
|
|
12
|
+
return `${Math.round(kb)}KB`;
|
|
13
|
+
}
|
|
14
|
+
const mb = kb / 1024;
|
|
15
|
+
return `${mb.toFixed(1)}MB`;
|
|
16
|
+
}
|
|
17
|
+
async function pathExists(path) {
|
|
18
|
+
try {
|
|
19
|
+
await stat(path);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function walkFiles(params) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
const visitDir = async (dir) => {
|
|
29
|
+
if (params.signal?.aborted) {
|
|
30
|
+
throw new Error("Aborted");
|
|
31
|
+
}
|
|
32
|
+
const children = await readdir(dir);
|
|
33
|
+
for (const name of children) {
|
|
34
|
+
if (params.signal?.aborted) {
|
|
35
|
+
throw new Error("Aborted");
|
|
36
|
+
}
|
|
37
|
+
const absolute = join(dir, name);
|
|
38
|
+
const s = await stat(absolute);
|
|
39
|
+
if (s.isDirectory()) {
|
|
40
|
+
await visitDir(absolute);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const matchedExt = params.extensions.find((ext) => name.endsWith(ext));
|
|
44
|
+
if (!matchedExt) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const kind = matchedExt === ".css" ? "css" : "js";
|
|
48
|
+
const rel = relative(params.base, absolute).replace(/\\/g, "/");
|
|
49
|
+
entries.push({ kind, relativePath: rel, bytes: s.size });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
await visitDir(params.root);
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
let projectRoot = process.cwd();
|
|
57
|
+
let jsonOutput = false;
|
|
58
|
+
let top = 15;
|
|
59
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
60
|
+
const arg = argv[i];
|
|
61
|
+
if ((arg === "--project-root" || arg === "--root") && i + 1 < argv.length) {
|
|
62
|
+
projectRoot = argv[i + 1] ?? projectRoot;
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--json") {
|
|
66
|
+
jsonOutput = true;
|
|
67
|
+
}
|
|
68
|
+
else if (arg === "--top" && i + 1 < argv.length) {
|
|
69
|
+
const value = parseInt(argv[i + 1] ?? "", 10);
|
|
70
|
+
if (!Number.isFinite(value) || value <= 0 || value > 100) {
|
|
71
|
+
throw new Error(`Invalid --top value: ${argv[i + 1]}. Expected 1-100.`);
|
|
72
|
+
}
|
|
73
|
+
top = value;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { projectRoot, jsonOutput, top };
|
|
78
|
+
}
|
|
79
|
+
function buildTopFilesTable(entries, top) {
|
|
80
|
+
const rows = [...entries]
|
|
81
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
82
|
+
.slice(0, top)
|
|
83
|
+
.map((f) => [f.kind, formatBytes(f.bytes), f.relativePath]);
|
|
84
|
+
if (rows.length === 0) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
return renderTable({ headers: ["Type", "Size", "File"], rows });
|
|
88
|
+
}
|
|
89
|
+
export async function runBundleCli(argv, options) {
|
|
90
|
+
stopSpinner();
|
|
91
|
+
const args = parseArgs(argv);
|
|
92
|
+
const projectRoot = resolve(args.projectRoot);
|
|
93
|
+
const nextDir = resolve(projectRoot, ".next");
|
|
94
|
+
const distDir = resolve(projectRoot, "dist");
|
|
95
|
+
const hasNext = await pathExists(nextDir);
|
|
96
|
+
const hasDist = await pathExists(distDir);
|
|
97
|
+
const scanTargets = [
|
|
98
|
+
hasNext ? resolve(nextDir, "static") : "",
|
|
99
|
+
hasDist ? distDir : "",
|
|
100
|
+
].filter((p) => p.length > 0);
|
|
101
|
+
const allEntries = [];
|
|
102
|
+
for (const target of scanTargets) {
|
|
103
|
+
if (options?.signal?.aborted) {
|
|
104
|
+
throw new Error("Aborted");
|
|
105
|
+
}
|
|
106
|
+
const entries = await walkFiles({
|
|
107
|
+
root: target,
|
|
108
|
+
base: projectRoot,
|
|
109
|
+
extensions: [".js", ".css"],
|
|
110
|
+
signal: options?.signal,
|
|
111
|
+
});
|
|
112
|
+
allEntries.push(...entries);
|
|
113
|
+
}
|
|
114
|
+
const jsBytes = allEntries.filter((e) => e.kind === "js").reduce((sum, e) => sum + e.bytes, 0);
|
|
115
|
+
const cssBytes = allEntries.filter((e) => e.kind === "css").reduce((sum, e) => sum + e.bytes, 0);
|
|
116
|
+
const topFiles = [...allEntries].sort((a, b) => b.bytes - a.bytes).slice(0, args.top);
|
|
117
|
+
const report = {
|
|
118
|
+
meta: {
|
|
119
|
+
projectRoot,
|
|
120
|
+
scannedAt: new Date().toISOString(),
|
|
121
|
+
detected: { nextDir: hasNext, distDir: hasDist },
|
|
122
|
+
},
|
|
123
|
+
totals: {
|
|
124
|
+
jsBytes,
|
|
125
|
+
cssBytes,
|
|
126
|
+
fileCount: allEntries.length,
|
|
127
|
+
},
|
|
128
|
+
topFiles,
|
|
129
|
+
files: allEntries,
|
|
130
|
+
};
|
|
131
|
+
const outputDir = resolve(projectRoot, ".apex-auditor");
|
|
132
|
+
const outputPath = resolve(outputDir, "bundle-audit.json");
|
|
133
|
+
await mkdir(outputDir, { recursive: true });
|
|
134
|
+
await writeFile(outputPath, JSON.stringify(report, null, 2), "utf8");
|
|
135
|
+
if (args.jsonOutput) {
|
|
136
|
+
// eslint-disable-next-line no-console
|
|
137
|
+
console.log(JSON.stringify(report, null, 2));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const detectedText = [hasNext ? ".next" : "", hasDist ? "dist" : ""].filter((v) => v.length > 0).join(", ") || "(none)";
|
|
141
|
+
const lines = [
|
|
142
|
+
`Project: ${projectRoot}`,
|
|
143
|
+
`Detected: ${detectedText}`,
|
|
144
|
+
`Files: ${report.totals.fileCount}`,
|
|
145
|
+
`JS: ${formatBytes(report.totals.jsBytes)}`,
|
|
146
|
+
`CSS: ${formatBytes(report.totals.cssBytes)}`,
|
|
147
|
+
`Output: ${relative(projectRoot, outputPath).replace(/\\/g, "/")}`,
|
|
148
|
+
];
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
console.log(renderPanel({ title: theme.bold("Bundle"), lines }));
|
|
151
|
+
const table = buildTopFilesTable(allEntries, args.top);
|
|
152
|
+
if (table.length > 0) {
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.log(`\n${theme.bold(`Largest files (top ${args.top})`)}`);
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.log(table);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
let projectRoot = process.cwd();
|
|
6
|
+
let configPath = "apex.config.json";
|
|
7
|
+
let removeReports = true;
|
|
8
|
+
let removeConfig = false;
|
|
9
|
+
let dryRun = false;
|
|
10
|
+
let yes = false;
|
|
11
|
+
let jsonOutput = false;
|
|
12
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
13
|
+
const arg = argv[i] ?? "";
|
|
14
|
+
if (arg === "--project-root" && i + 1 < argv.length) {
|
|
15
|
+
projectRoot = argv[i + 1] ?? projectRoot;
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if ((arg === "--config-path" || arg === "--config") && i + 1 < argv.length) {
|
|
20
|
+
configPath = argv[i + 1] ?? configPath;
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg === "--reports") {
|
|
25
|
+
removeReports = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg === "--no-reports") {
|
|
29
|
+
removeReports = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === "--remove-config") {
|
|
33
|
+
removeConfig = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--all") {
|
|
37
|
+
removeReports = true;
|
|
38
|
+
removeConfig = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--dry-run") {
|
|
42
|
+
dryRun = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg === "--yes" || arg === "-y") {
|
|
46
|
+
yes = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === "--json") {
|
|
50
|
+
jsonOutput = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
projectRoot: resolve(projectRoot),
|
|
56
|
+
configPath: resolve(projectRoot, configPath),
|
|
57
|
+
removeReports,
|
|
58
|
+
removeConfig,
|
|
59
|
+
dryRun,
|
|
60
|
+
yes,
|
|
61
|
+
jsonOutput,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function confirmPrompt(question) {
|
|
65
|
+
if (!process.stdin.isTTY) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
process.stdin.resume();
|
|
69
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
try {
|
|
71
|
+
const answer = await new Promise((resolvePromise) => {
|
|
72
|
+
rl.question(question, (value) => resolvePromise(value));
|
|
73
|
+
});
|
|
74
|
+
const text = answer.trim().toLowerCase();
|
|
75
|
+
return text === "y" || text === "yes";
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
rl.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function buildPlan(args) {
|
|
82
|
+
const actions = [];
|
|
83
|
+
if (args.removeReports) {
|
|
84
|
+
actions.push({ kind: "rm", path: resolve(args.projectRoot, ".apex-auditor"), existsByAssumption: true });
|
|
85
|
+
}
|
|
86
|
+
if (args.removeConfig) {
|
|
87
|
+
actions.push({ kind: "rm", path: args.configPath, existsByAssumption: true });
|
|
88
|
+
}
|
|
89
|
+
return actions;
|
|
90
|
+
}
|
|
91
|
+
async function executePlan(plan, dryRun) {
|
|
92
|
+
if (dryRun) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const executed = [];
|
|
96
|
+
for (const action of plan) {
|
|
97
|
+
if (action.kind === "rm") {
|
|
98
|
+
await rm(action.path, { recursive: true, force: true });
|
|
99
|
+
executed.push(action);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return executed;
|
|
103
|
+
}
|
|
104
|
+
export async function runCleanCli(argv) {
|
|
105
|
+
const startedAtMs = Date.now();
|
|
106
|
+
const args = parseArgs(argv);
|
|
107
|
+
const planned = buildPlan(args);
|
|
108
|
+
if (planned.length === 0) {
|
|
109
|
+
const empty = {
|
|
110
|
+
meta: {
|
|
111
|
+
projectRoot: args.projectRoot,
|
|
112
|
+
configPath: args.configPath,
|
|
113
|
+
dryRun: args.dryRun,
|
|
114
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
115
|
+
completedAt: new Date(startedAtMs).toISOString(),
|
|
116
|
+
elapsedMs: 0,
|
|
117
|
+
},
|
|
118
|
+
planned: [],
|
|
119
|
+
executed: [],
|
|
120
|
+
};
|
|
121
|
+
if (args.jsonOutput) {
|
|
122
|
+
console.log(JSON.stringify(empty, null, 2));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
console.log("Nothing to clean.");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!args.yes && process.stdin.isTTY) {
|
|
129
|
+
const targets = planned.map((p) => p.path).join("\n");
|
|
130
|
+
const ok = await confirmPrompt(`This will remove:\n${targets}\nContinue? (y/N) `);
|
|
131
|
+
if (!ok) {
|
|
132
|
+
console.log("Cancelled.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const executed = await executePlan(planned, args.dryRun);
|
|
137
|
+
const completedAtMs = Date.now();
|
|
138
|
+
const report = {
|
|
139
|
+
meta: {
|
|
140
|
+
projectRoot: args.projectRoot,
|
|
141
|
+
configPath: args.configPath,
|
|
142
|
+
dryRun: args.dryRun,
|
|
143
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
144
|
+
completedAt: new Date(completedAtMs).toISOString(),
|
|
145
|
+
elapsedMs: completedAtMs - startedAtMs,
|
|
146
|
+
},
|
|
147
|
+
planned,
|
|
148
|
+
executed,
|
|
149
|
+
};
|
|
150
|
+
if (args.jsonOutput) {
|
|
151
|
+
console.log(JSON.stringify(report, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (args.dryRun) {
|
|
155
|
+
console.log(`Planned removals: ${planned.length} (dry-run).`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`Removed: ${executed.length}/${planned.length}.`);
|
|
159
|
+
}
|