@vertaaux/cli 0.4.0 → 0.5.0
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/CHANGELOG.md +97 -0
- package/MIGRATION.md +239 -0
- package/README.md +34 -16
- package/dist/app/interactive-app.d.ts +101 -0
- package/dist/app/interactive-app.d.ts.map +1 -0
- package/dist/app/interactive-app.js +309 -0
- package/dist/app/layout/canvas.d.ts +23 -0
- package/dist/app/layout/canvas.d.ts.map +1 -0
- package/dist/app/layout/canvas.js +36 -0
- package/dist/app/layout/footer.d.ts +31 -0
- package/dist/app/layout/footer.d.ts.map +1 -0
- package/dist/app/layout/footer.js +41 -0
- package/dist/app/layout/header.d.ts +20 -0
- package/dist/app/layout/header.d.ts.map +1 -0
- package/dist/app/layout/header.js +27 -0
- package/dist/app/menu/categories.d.ts +20 -0
- package/dist/app/menu/categories.d.ts.map +1 -0
- package/dist/app/menu/categories.js +181 -0
- package/dist/app/menu/filter.d.ts +17 -0
- package/dist/app/menu/filter.d.ts.map +1 -0
- package/dist/app/menu/filter.js +33 -0
- package/dist/app/menu/menu-view.d.ts +35 -0
- package/dist/app/menu/menu-view.d.ts.map +1 -0
- package/dist/app/menu/menu-view.js +230 -0
- package/dist/app/menu/recent.d.ts +24 -0
- package/dist/app/menu/recent.d.ts.map +1 -0
- package/dist/app/menu/recent.js +49 -0
- package/dist/app/types.d.ts +43 -0
- package/dist/app/types.d.ts.map +1 -0
- package/dist/app/types.js +7 -0
- package/dist/app/views/command-runner.d.ts +36 -0
- package/dist/app/views/command-runner.d.ts.map +1 -0
- package/dist/app/views/command-runner.js +372 -0
- package/dist/app/views/help-overlay.d.ts +21 -0
- package/dist/app/views/help-overlay.d.ts.map +1 -0
- package/dist/app/views/help-overlay.js +45 -0
- package/dist/auth/ci-token.d.ts +8 -2
- package/dist/auth/ci-token.d.ts.map +1 -1
- package/dist/auth/ci-token.js +15 -30
- package/dist/auth/device-flow.d.ts +2 -1
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +13 -10
- package/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +12 -2
- package/dist/baseline/diff.d.ts +2 -2
- package/dist/baseline/diff.d.ts.map +1 -1
- package/dist/baseline/diff.js +15 -34
- package/dist/commands/a11y.d.ts +9 -0
- package/dist/commands/a11y.d.ts.map +1 -0
- package/dist/commands/a11y.js +76 -0
- package/dist/commands/audit/artifacts.d.ts +27 -0
- package/dist/commands/audit/artifacts.d.ts.map +1 -0
- package/dist/commands/audit/artifacts.js +158 -0
- package/dist/commands/audit/ci-detection.d.ts +18 -0
- package/dist/commands/audit/ci-detection.d.ts.map +1 -0
- package/dist/commands/audit/ci-detection.js +71 -0
- package/dist/commands/audit/explain.d.ts +11 -0
- package/dist/commands/audit/explain.d.ts.map +1 -0
- package/dist/commands/audit/explain.js +45 -0
- package/dist/commands/audit/filters.d.ts +17 -0
- package/dist/commands/audit/filters.d.ts.map +1 -0
- package/dist/commands/audit/filters.js +40 -0
- package/dist/commands/audit/index.d.ts +18 -0
- package/dist/commands/audit/index.d.ts.map +1 -0
- package/dist/commands/audit/index.js +564 -0
- package/dist/commands/audit/output.d.ts +32 -0
- package/dist/commands/audit/output.d.ts.map +1 -0
- package/dist/commands/audit/output.js +130 -0
- package/dist/commands/audit/policy.d.ts +19 -0
- package/dist/commands/audit/policy.d.ts.map +1 -0
- package/dist/commands/audit/policy.js +102 -0
- package/dist/commands/audit/scoring.d.ts +23 -0
- package/dist/commands/audit/scoring.d.ts.map +1 -0
- package/dist/commands/audit/scoring.js +70 -0
- package/dist/commands/audit/types.d.ts +88 -0
- package/dist/commands/audit/types.d.ts.map +1 -0
- package/dist/commands/audit/types.js +8 -0
- package/dist/commands/audit.d.ts +2 -60
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +2 -1097
- package/dist/commands/baseline.d.ts +1 -0
- package/dist/commands/baseline.d.ts.map +1 -1
- package/dist/commands/baseline.js +205 -121
- package/dist/commands/comment.d.ts +22 -0
- package/dist/commands/comment.d.ts.map +1 -1
- package/dist/commands/comment.js +122 -58
- package/dist/commands/compare.d.ts +17 -0
- package/dist/commands/compare.d.ts.map +1 -1
- package/dist/commands/compare.js +287 -180
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +168 -141
- package/dist/commands/doc.d.ts +10 -0
- package/dist/commands/doc.d.ts.map +1 -1
- package/dist/commands/doc.js +134 -76
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +164 -17
- package/dist/commands/download.d.ts +10 -0
- package/dist/commands/download.d.ts.map +1 -1
- package/dist/commands/download.js +169 -112
- package/dist/commands/explain.d.ts +5 -0
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +241 -155
- package/dist/commands/fix-all.d.ts +25 -0
- package/dist/commands/fix-all.d.ts.map +1 -0
- package/dist/commands/fix-all.js +206 -0
- package/dist/commands/fix-plan.d.ts +9 -0
- package/dist/commands/fix-plan.d.ts.map +1 -1
- package/dist/commands/fix-plan.js +152 -89
- package/dist/commands/fix.d.ts +17 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +111 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +94 -42
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +263 -92
- package/dist/commands/patch-review.d.ts +11 -0
- package/dist/commands/patch-review.d.ts.map +1 -1
- package/dist/commands/patch-review.js +159 -97
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +269 -124
- package/dist/commands/release-notes.d.ts +10 -0
- package/dist/commands/release-notes.d.ts.map +1 -1
- package/dist/commands/release-notes.js +127 -73
- package/dist/commands/scan.d.ts +13 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +133 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/suggest.d.ts +10 -0
- package/dist/commands/suggest.d.ts.map +1 -1
- package/dist/commands/suggest.js +153 -82
- package/dist/commands/triage.d.ts +35 -0
- package/dist/commands/triage.d.ts.map +1 -1
- package/dist/commands/triage.js +206 -81
- package/dist/commands/upload.d.ts +9 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +140 -101
- package/dist/commands/verify.d.ts +13 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +118 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -990
- package/dist/interactive/fix-wizard.d.ts +3 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -1
- package/dist/interactive/fix-wizard.js +130 -112
- package/dist/interactive/init-wizard.d.ts +3 -1
- package/dist/interactive/init-wizard.d.ts.map +1 -1
- package/dist/interactive/init-wizard.js +207 -138
- package/dist/interactive/prompts.d.ts +7 -3
- package/dist/interactive/prompts.d.ts.map +1 -1
- package/dist/interactive/prompts.js +44 -23
- package/dist/output/envelope.d.ts +2 -0
- package/dist/output/envelope.d.ts.map +1 -1
- package/dist/output/envelope.js +18 -2
- package/dist/output/factory.d.ts +2 -1
- package/dist/output/factory.d.ts.map +1 -1
- package/dist/output/html.d.ts +2 -1
- package/dist/output/html.d.ts.map +1 -1
- package/dist/output/html.js +3 -2
- package/dist/output/human.d.ts +2 -1
- package/dist/output/human.d.ts.map +1 -1
- package/dist/output/human.js +3 -2
- package/dist/output/json.d.ts +2 -1
- package/dist/output/json.d.ts.map +1 -1
- package/dist/output/junit.d.ts +2 -1
- package/dist/output/junit.d.ts.map +1 -1
- package/dist/output/sarif.d.ts +2 -1
- package/dist/output/sarif.d.ts.map +1 -1
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/ui/banner.d.ts +34 -0
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +97 -5
- package/dist/ui/diagnostics.d.ts +9 -4
- package/dist/ui/diagnostics.d.ts.map +1 -1
- package/dist/ui/diagnostics.js +32 -82
- package/dist/ui/strings.d.ts +373 -0
- package/dist/ui/strings.d.ts.map +1 -0
- package/dist/ui/strings.js +499 -0
- package/dist/ui/table.d.ts +0 -2
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +3 -4
- package/dist/utils/api-client.d.ts +46 -0
- package/dist/utils/api-client.d.ts.map +1 -0
- package/dist/utils/api-client.js +170 -0
- package/dist/utils/client.d.ts +29 -18
- package/dist/utils/client.d.ts.map +1 -1
- package/dist/utils/client.js +102 -12
- package/dist/utils/formatters.d.ts +38 -0
- package/dist/utils/formatters.d.ts.map +1 -0
- package/dist/utils/formatters.js +277 -0
- package/dist/utils/url-classify.d.ts.map +1 -1
- package/dist/utils/url-classify.js +24 -3
- package/node_modules/@vertaaux/tui/dist/index.cjs +713 -20
- package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -1
- package/node_modules/@vertaaux/tui/dist/index.d.cts +361 -4
- package/node_modules/@vertaaux/tui/dist/index.d.ts +361 -4
- package/node_modules/@vertaaux/tui/dist/index.js +689 -21
- package/node_modules/@vertaaux/tui/dist/index.js.map +1 -1
- package/package.json +13 -5
- package/dist/commands/client.d.ts +0 -14
- package/dist/commands/client.d.ts.map +0 -1
- package/dist/commands/client.js +0 -362
- package/dist/commands/drift.d.ts +0 -15
- package/dist/commands/drift.d.ts.map +0 -1
- package/dist/commands/drift.js +0 -309
- package/dist/commands/protect.d.ts +0 -16
- package/dist/commands/protect.d.ts.map +0 -1
- package/dist/commands/protect.js +0 -323
- package/dist/commands/report.d.ts +0 -15
- package/dist/commands/report.d.ts.map +0 -1
- package/dist/commands/report.js +0 -214
- package/dist/policy/sync.d.ts +0 -67
- package/dist/policy/sync.d.ts.map +0 -1
- package/dist/policy/sync.js +0 -147
package/dist/commands/audit.js
CHANGED
|
@@ -1,1097 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Runs UX and accessibility audits on web pages.
|
|
5
|
-
* Supports various targets, output formats, and CI integration.
|
|
6
|
-
*/
|
|
7
|
-
import fs from "fs";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import { resolveConfig } from "../config/loader.js";
|
|
10
|
-
import { ExitCode } from "../utils/exit-codes.js";
|
|
11
|
-
import { parseTimeout, parseInterval, parseConcurrency, parseThreshold, parseMode, parseFailOn, parseGroupBy, parseBudget, validateNumeric, } from "../utils/validators.js";
|
|
12
|
-
import { isTTY } from "../utils/detect-env.js";
|
|
13
|
-
import { resolveApiBase, getApiKey, apiRequest, waitForAudit, } from "../utils/client.js";
|
|
14
|
-
import { createOutput, formatSarif, formatAuditHtml } from "../output/factory.js";
|
|
15
|
-
import { createEnvelope, writeJsonOutput, writeOutput as writeStdout } from "../output/envelope.js";
|
|
16
|
-
import { resolveCommandFormat } from "../output/formats.js";
|
|
17
|
-
import { getVersion } from "../ui/banner.js";
|
|
18
|
-
import { createSpinner, updateSpinner, succeedSpinner, failSpinner, } from "../ui/spinner.js";
|
|
19
|
-
import { createRenderer, createKeyboardHandler, AuditPhase, phaseIndex, phaseTotal, } from "@vertaaux/tui";
|
|
20
|
-
import { runFixWizard } from "../interactive/fix-wizard.js";
|
|
21
|
-
import { isInteractive } from "../interactive/prompts.js";
|
|
22
|
-
import { evaluateQualityGate, DEFAULT_QUALITY_GATE_CONFIG, } from "../quality-gate/index.js";
|
|
23
|
-
import { loadBaseline } from "../baseline/manager.js";
|
|
24
|
-
import { getChangedRoutes, detectBaseBranch, getBudgetConfig, } from "../ci/index.js";
|
|
25
|
-
import { loadPolicy, resolveBranchPolicy, } from "../policy/index.js";
|
|
26
|
-
import { detectMonorepo, getAuditableApps, generateMatrixConfig, aggregateResults, formatAggregatedResults, } from "../monorepo/index.js";
|
|
27
|
-
import { createLogger } from "../utils/logger.js";
|
|
28
|
-
import { validateBranchName, assertPathContainment } from "../utils/sanitize.js";
|
|
29
|
-
import { isLocalUrl } from "../utils/url-classify.js";
|
|
30
|
-
import { captureLocalPage } from "../utils/local-capture.js";
|
|
31
|
-
import chalk from "chalk";
|
|
32
|
-
import semver from "semver";
|
|
33
|
-
// Artifact directory
|
|
34
|
-
const ARTIFACTS_DIR = ".vertaaux/artifacts";
|
|
35
|
-
// CLI version for policy version requirements (read from package.json)
|
|
36
|
-
const CLI_VERSION = getVersion();
|
|
37
|
-
/**
|
|
38
|
-
* Detect current branch from CI environment or git.
|
|
39
|
-
*/
|
|
40
|
-
function detectCurrentBranch() {
|
|
41
|
-
// GitHub Actions
|
|
42
|
-
if (process.env.GITHUB_HEAD_REF) {
|
|
43
|
-
return process.env.GITHUB_HEAD_REF;
|
|
44
|
-
}
|
|
45
|
-
if (process.env.GITHUB_REF_NAME) {
|
|
46
|
-
return process.env.GITHUB_REF_NAME;
|
|
47
|
-
}
|
|
48
|
-
// GitLab CI
|
|
49
|
-
if (process.env.CI_COMMIT_REF_NAME) {
|
|
50
|
-
return process.env.CI_COMMIT_REF_NAME;
|
|
51
|
-
}
|
|
52
|
-
// Azure DevOps
|
|
53
|
-
if (process.env.BUILD_SOURCEBRANCHNAME) {
|
|
54
|
-
return process.env.BUILD_SOURCEBRANCHNAME;
|
|
55
|
-
}
|
|
56
|
-
// CircleCI
|
|
57
|
-
if (process.env.CIRCLE_BRANCH) {
|
|
58
|
-
return process.env.CIRCLE_BRANCH;
|
|
59
|
-
}
|
|
60
|
-
// Jenkins
|
|
61
|
-
if (process.env.GIT_BRANCH) {
|
|
62
|
-
// Jenkins often includes origin/, remove it
|
|
63
|
-
return process.env.GIT_BRANCH.replace(/^origin\//, "");
|
|
64
|
-
}
|
|
65
|
-
// Generic CI
|
|
66
|
-
if (process.env.BRANCH_NAME) {
|
|
67
|
-
return process.env.BRANCH_NAME;
|
|
68
|
-
}
|
|
69
|
-
// Default
|
|
70
|
-
return "";
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Detect PR labels from CI environment variables.
|
|
74
|
-
*
|
|
75
|
-
* Supports:
|
|
76
|
-
* - GitHub Actions: GITHUB_EVENT_PATH contains event JSON with labels
|
|
77
|
-
* - GitLab CI: CI_MERGE_REQUEST_LABELS is comma-separated
|
|
78
|
-
*/
|
|
79
|
-
function detectPRLabels() {
|
|
80
|
-
// GitHub Actions: GITHUB_EVENT_PATH contains event JSON with labels
|
|
81
|
-
if (process.env.GITHUB_EVENT_PATH) {
|
|
82
|
-
try {
|
|
83
|
-
const eventContent = fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf-8");
|
|
84
|
-
const event = JSON.parse(eventContent);
|
|
85
|
-
const labels = event.pull_request?.labels;
|
|
86
|
-
if (Array.isArray(labels)) {
|
|
87
|
-
return labels.map((l) => l.name || "").filter(Boolean);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// Ignore errors reading event file
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// GitLab CI: CI_MERGE_REQUEST_LABELS is comma-separated
|
|
95
|
-
if (process.env.CI_MERGE_REQUEST_LABELS) {
|
|
96
|
-
return process.env.CI_MERGE_REQUEST_LABELS.split(",")
|
|
97
|
-
.map((l) => l.trim())
|
|
98
|
-
.filter(Boolean);
|
|
99
|
-
}
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Build quality gate configuration from options and config.
|
|
104
|
-
*/
|
|
105
|
-
function buildQualityGateConfig(config, options) {
|
|
106
|
-
// Start with defaults
|
|
107
|
-
const gateConfig = { ...DEFAULT_QUALITY_GATE_CONFIG };
|
|
108
|
-
// Apply config file settings
|
|
109
|
-
const configGate = config.qualityGate;
|
|
110
|
-
if (configGate) {
|
|
111
|
-
if (configGate.failOn)
|
|
112
|
-
gateConfig.failOn = configGate.failOn;
|
|
113
|
-
if (configGate.thresholds) {
|
|
114
|
-
gateConfig.thresholds = { ...gateConfig.thresholds, ...configGate.thresholds };
|
|
115
|
-
}
|
|
116
|
-
if (configGate.maxNew) {
|
|
117
|
-
gateConfig.maxNew = { ...gateConfig.maxNew, ...configGate.maxNew };
|
|
118
|
-
}
|
|
119
|
-
if (configGate.failOnExisting !== undefined) {
|
|
120
|
-
gateConfig.failOnExisting = configGate.failOnExisting;
|
|
121
|
-
}
|
|
122
|
-
if (configGate.bypassLabels) {
|
|
123
|
-
gateConfig.bypassLabels = configGate.bypassLabels;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Apply CLI flags (highest precedence)
|
|
127
|
-
if (options.failOn) {
|
|
128
|
-
gateConfig.failOn = options.failOn;
|
|
129
|
-
}
|
|
130
|
-
if (options.threshold !== undefined) {
|
|
131
|
-
gateConfig.thresholds.overall = options.threshold;
|
|
132
|
-
}
|
|
133
|
-
if (options.maxNewErrors !== undefined) {
|
|
134
|
-
gateConfig.maxNew.error = options.maxNewErrors;
|
|
135
|
-
}
|
|
136
|
-
if (options.maxNewWarnings !== undefined) {
|
|
137
|
-
gateConfig.maxNew.warning = options.maxNewWarnings;
|
|
138
|
-
}
|
|
139
|
-
if (options.failOnExisting !== undefined) {
|
|
140
|
-
gateConfig.failOnExisting = options.failOnExisting;
|
|
141
|
-
}
|
|
142
|
-
if (options.bypassLabels) {
|
|
143
|
-
gateConfig.bypassLabels = options.bypassLabels.split(",").map((l) => l.trim());
|
|
144
|
-
}
|
|
145
|
-
return gateConfig;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Normalize issues from various API response formats.
|
|
149
|
-
*/
|
|
150
|
-
function normalizeIssues(issues) {
|
|
151
|
-
if (Array.isArray(issues))
|
|
152
|
-
return issues;
|
|
153
|
-
if (issues && typeof issues === "object") {
|
|
154
|
-
const values = Object.values(issues);
|
|
155
|
-
return values.flatMap((value) => Array.isArray(value) ? value : []);
|
|
156
|
-
}
|
|
157
|
-
return [];
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Filter issues by severity.
|
|
161
|
-
*/
|
|
162
|
-
function filterBySeverity(issues, severityFilter) {
|
|
163
|
-
const allowed = new Set(severityFilter.split(",").map((s) => s.trim().toLowerCase()));
|
|
164
|
-
// Map common aliases
|
|
165
|
-
if (allowed.has("error"))
|
|
166
|
-
allowed.add("critical");
|
|
167
|
-
if (allowed.has("warning"))
|
|
168
|
-
allowed.add("serious");
|
|
169
|
-
return issues.filter((issue) => {
|
|
170
|
-
const sev = issue.severity?.toLowerCase() || "info";
|
|
171
|
-
return allowed.has(sev);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Filter issues by category.
|
|
176
|
-
*/
|
|
177
|
-
function filterByCategory(issues, categoryFilter) {
|
|
178
|
-
const allowed = new Set(categoryFilter.split(",").map((c) => c.trim().toLowerCase()));
|
|
179
|
-
return issues.filter((issue) => {
|
|
180
|
-
const cat = issue.category?.toLowerCase() || "";
|
|
181
|
-
return allowed.has(cat) || Array.from(allowed).some((a) => cat.includes(a));
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Write output to file. Returns the resolved path if written to file, undefined otherwise.
|
|
186
|
-
*/
|
|
187
|
-
function writeOutputToFile(content, outputPath, defaultPath) {
|
|
188
|
-
// Determine the final output path
|
|
189
|
-
const finalPath = outputPath || defaultPath;
|
|
190
|
-
if (finalPath) {
|
|
191
|
-
// Write to file
|
|
192
|
-
const resolvedPath = path.resolve(process.cwd(), finalPath);
|
|
193
|
-
// Ensure directory exists
|
|
194
|
-
const dir = path.dirname(resolvedPath);
|
|
195
|
-
if (!fs.existsSync(dir)) {
|
|
196
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
197
|
-
}
|
|
198
|
-
fs.writeFileSync(resolvedPath, content, "utf-8");
|
|
199
|
-
return resolvedPath;
|
|
200
|
-
}
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Get default output path based on format.
|
|
205
|
-
* Returns undefined for formats that should go to stdout by default.
|
|
206
|
-
*/
|
|
207
|
-
function getDefaultOutputPath(format) {
|
|
208
|
-
switch (format) {
|
|
209
|
-
case "html":
|
|
210
|
-
return "vertaaux-report.html";
|
|
211
|
-
default:
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Save repro artifacts from API response.
|
|
217
|
-
*/
|
|
218
|
-
function saveReproArtifacts(jobId, response, options, quiet) {
|
|
219
|
-
const hasArtifactOptions = options.saveTrace || options.saveHar || options.screenshots || options.domSnapshots;
|
|
220
|
-
if (!hasArtifactOptions)
|
|
221
|
-
return;
|
|
222
|
-
// Create artifacts directory
|
|
223
|
-
const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR, jobId);
|
|
224
|
-
// Check if API response includes artifact data
|
|
225
|
-
const responseAny = response;
|
|
226
|
-
const artifacts = responseAny.artifacts;
|
|
227
|
-
if (!artifacts) {
|
|
228
|
-
if (!quiet) {
|
|
229
|
-
console.error("Note: Repro artifacts were requested but not available in API response.");
|
|
230
|
-
console.error("Artifact capture may require a 'deep' mode audit or premium plan.");
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
// Ensure directory exists
|
|
235
|
-
if (!fs.existsSync(artifactsPath)) {
|
|
236
|
-
fs.mkdirSync(artifactsPath, { recursive: true });
|
|
237
|
-
}
|
|
238
|
-
const saved = [];
|
|
239
|
-
// Save trace file if available and requested
|
|
240
|
-
if (options.saveTrace && artifacts.trace) {
|
|
241
|
-
const tracePath = path.join(artifactsPath, "trace.zip");
|
|
242
|
-
fs.writeFileSync(tracePath, Buffer.from(artifacts.trace, "base64"));
|
|
243
|
-
saved.push("trace.zip");
|
|
244
|
-
}
|
|
245
|
-
// Save HAR file if available and requested
|
|
246
|
-
if (options.saveHar && artifacts.har) {
|
|
247
|
-
const harPath = path.join(artifactsPath, "network.har");
|
|
248
|
-
fs.writeFileSync(harPath, typeof artifacts.har === "string" ? artifacts.har : JSON.stringify(artifacts.har, null, 2));
|
|
249
|
-
saved.push("network.har");
|
|
250
|
-
}
|
|
251
|
-
// Save screenshots if available and requested
|
|
252
|
-
if (options.screenshots && artifacts.screenshots) {
|
|
253
|
-
const screenshots = artifacts.screenshots;
|
|
254
|
-
for (const screenshot of screenshots) {
|
|
255
|
-
const screenshotName = screenshot.name || "screenshot.png";
|
|
256
|
-
let screenshotPath;
|
|
257
|
-
try {
|
|
258
|
-
screenshotPath = assertPathContainment(screenshotName, artifactsPath);
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
console.error(`Security: Rejected screenshot "${screenshotName}" -- path traversal outside artifacts directory.`);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
fs.writeFileSync(screenshotPath, Buffer.from(screenshot.data, "base64"));
|
|
265
|
-
saved.push(screenshotName);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// Save DOM snapshots if available and requested
|
|
269
|
-
if (options.domSnapshots && artifacts.domSnapshots) {
|
|
270
|
-
const snapshots = artifacts.domSnapshots;
|
|
271
|
-
for (const snapshot of snapshots) {
|
|
272
|
-
const snapshotName = snapshot.name || "snapshot.html";
|
|
273
|
-
let snapshotPath;
|
|
274
|
-
try {
|
|
275
|
-
snapshotPath = assertPathContainment(snapshotName, artifactsPath);
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
console.error(`Security: Rejected snapshot "${snapshotName}" -- path traversal outside artifacts directory.`);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
fs.writeFileSync(snapshotPath, snapshot.html);
|
|
282
|
-
saved.push(snapshotName);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (saved.length > 0 && !quiet) {
|
|
286
|
-
console.error(`Artifacts saved to: ${artifactsPath}`);
|
|
287
|
-
console.error(` - ${saved.join("\n - ")}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Count issues by severity level.
|
|
292
|
-
*/
|
|
293
|
-
function countIssuesBySeverity(issues) {
|
|
294
|
-
const counts = { error: 0, warning: 0, info: 0 };
|
|
295
|
-
for (const issue of issues) {
|
|
296
|
-
const sev = (issue.severity || "info").toLowerCase();
|
|
297
|
-
if (sev === "error" || sev === "critical") {
|
|
298
|
-
counts.error++;
|
|
299
|
-
}
|
|
300
|
-
else if (sev === "warning" || sev === "serious") {
|
|
301
|
-
counts.warning++;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
counts.info++;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return counts;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Save CI artifact bundle for GitHub Actions upload.
|
|
311
|
-
*
|
|
312
|
-
* Creates a complete evidence bundle:
|
|
313
|
-
* - results.json: Full audit results
|
|
314
|
-
* - results.sarif: SARIF for Code Scanning
|
|
315
|
-
* - report.html: HTML report for viewing
|
|
316
|
-
* - manifest.json: Metadata about the bundle
|
|
317
|
-
*/
|
|
318
|
-
function saveArtifactBundle(jobId, result, issues, exitCode, quiet) {
|
|
319
|
-
const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR, jobId);
|
|
320
|
-
// Ensure directory exists
|
|
321
|
-
if (!fs.existsSync(artifactsPath)) {
|
|
322
|
-
fs.mkdirSync(artifactsPath, { recursive: true });
|
|
323
|
-
}
|
|
324
|
-
const files = [];
|
|
325
|
-
// 1. Save JSON results
|
|
326
|
-
const jsonPath = path.join(artifactsPath, "results.json");
|
|
327
|
-
fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2), "utf-8");
|
|
328
|
-
files.push("results.json");
|
|
329
|
-
// 2. Save SARIF for Code Scanning
|
|
330
|
-
const sarifContent = formatSarif(result, {
|
|
331
|
-
workingDirectory: process.cwd(),
|
|
332
|
-
});
|
|
333
|
-
const sarifPath = path.join(artifactsPath, "results.sarif");
|
|
334
|
-
fs.writeFileSync(sarifPath, sarifContent, "utf-8");
|
|
335
|
-
files.push("results.sarif");
|
|
336
|
-
// 3. Save HTML report
|
|
337
|
-
const htmlContent = formatAuditHtml(result, {
|
|
338
|
-
interactive: true,
|
|
339
|
-
});
|
|
340
|
-
const htmlPath = path.join(artifactsPath, "report.html");
|
|
341
|
-
fs.writeFileSync(htmlPath, htmlContent, "utf-8");
|
|
342
|
-
files.push("report.html");
|
|
343
|
-
// 4. Save manifest
|
|
344
|
-
const manifest = {
|
|
345
|
-
job_id: jobId,
|
|
346
|
-
timestamp: new Date().toISOString(),
|
|
347
|
-
files,
|
|
348
|
-
audit_url: result.url,
|
|
349
|
-
issue_count: countIssuesBySeverity(issues),
|
|
350
|
-
exit_code: exitCode,
|
|
351
|
-
};
|
|
352
|
-
const manifestPath = path.join(artifactsPath, "manifest.json");
|
|
353
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
354
|
-
files.push("manifest.json");
|
|
355
|
-
if (!quiet) {
|
|
356
|
-
console.error(`Artifacts saved to: ${artifactsPath}`);
|
|
357
|
-
console.error(` - ${files.join("\n - ")}`);
|
|
358
|
-
}
|
|
359
|
-
return artifactsPath;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Output results in fire-and-forget mode (--no-wait).
|
|
363
|
-
*/
|
|
364
|
-
function outputFireAndForget(created, format, formatter, options, quiet) {
|
|
365
|
-
if (format === "json") {
|
|
366
|
-
if (options.output) {
|
|
367
|
-
const output = JSON.stringify(createEnvelope(created, "audit"), null, 2);
|
|
368
|
-
const filePath = writeOutputToFile(output, options.output);
|
|
369
|
-
if (filePath && !quiet) {
|
|
370
|
-
console.error(`Report written to: ${filePath}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
writeJsonOutput(created, "audit");
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
const output = formatter.formatResult(created);
|
|
379
|
-
const defaultPath = getDefaultOutputPath(format);
|
|
380
|
-
const filePath = writeOutputToFile(output, options.output, defaultPath);
|
|
381
|
-
if (filePath && !quiet) {
|
|
382
|
-
console.error(`Report written to: ${filePath}`);
|
|
383
|
-
}
|
|
384
|
-
else if (!filePath) {
|
|
385
|
-
writeStdout(output);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Load and resolve a policy file with branch-specific overrides.
|
|
391
|
-
*/
|
|
392
|
-
async function loadAndResolvePolicy(options, quiet) {
|
|
393
|
-
let resolvedPolicy = null;
|
|
394
|
-
let policyPath = null;
|
|
395
|
-
try {
|
|
396
|
-
const policyResult = await loadPolicy(options.policy ? path.dirname(options.policy) : undefined);
|
|
397
|
-
if (options.policy) {
|
|
398
|
-
const { loadPolicyFile } = await import("../policy/index.js");
|
|
399
|
-
resolvedPolicy = await loadPolicyFile(options.policy);
|
|
400
|
-
policyPath = options.policy;
|
|
401
|
-
}
|
|
402
|
-
else if (policyResult.path) {
|
|
403
|
-
resolvedPolicy = policyResult.policy;
|
|
404
|
-
policyPath = policyResult.path;
|
|
405
|
-
}
|
|
406
|
-
if (resolvedPolicy) {
|
|
407
|
-
const currentBranch = detectCurrentBranch();
|
|
408
|
-
if (currentBranch) {
|
|
409
|
-
resolvedPolicy = resolveBranchPolicy(resolvedPolicy, currentBranch);
|
|
410
|
-
}
|
|
411
|
-
if (!quiet && policyPath) {
|
|
412
|
-
console.error(chalk.dim(`Using policy: ${policyPath}`));
|
|
413
|
-
}
|
|
414
|
-
if (resolvedPolicy.required_version) {
|
|
415
|
-
if (!semver.satisfies(CLI_VERSION, resolvedPolicy.required_version)) {
|
|
416
|
-
console.error(chalk.red(`Policy requires CLI version ${resolvedPolicy.required_version}, ` +
|
|
417
|
-
`but current version is ${CLI_VERSION}`));
|
|
418
|
-
process.exit(ExitCode.ERROR);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
if (options.policy) {
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
if (!quiet) {
|
|
428
|
-
console.error(chalk.yellow(`Warning: Failed to load policy: ${error instanceof Error ? error.message : String(error)}`));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return { policy: resolvedPolicy, path: policyPath };
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Run the inline AI explanation for audit issues.
|
|
435
|
-
*/
|
|
436
|
-
async function runInlineExplanation(issues, result, targetUrl, options, config) {
|
|
437
|
-
try {
|
|
438
|
-
const explainBase = resolveApiBase(options.base);
|
|
439
|
-
const explainKey = getApiKey(config.apiKey);
|
|
440
|
-
const explainIssues = issues.map((i) => ({
|
|
441
|
-
id: i.id || null,
|
|
442
|
-
title: i.title || i.description || null,
|
|
443
|
-
description: i.description || null,
|
|
444
|
-
severity: i.severity || null,
|
|
445
|
-
category: i.category || null,
|
|
446
|
-
selector: i.selector || null,
|
|
447
|
-
wcag_reference: i.wcag_reference || null,
|
|
448
|
-
recommendation: i.recommendation || i.recommended_fix || null,
|
|
449
|
-
}));
|
|
450
|
-
const explainPayload = {
|
|
451
|
-
job_id: result.job_id || null,
|
|
452
|
-
url: targetUrl || null,
|
|
453
|
-
scores: result.scores || null,
|
|
454
|
-
issues: explainIssues,
|
|
455
|
-
};
|
|
456
|
-
const explainSpinner = createSpinner("Generating AI explanation...");
|
|
457
|
-
const explainResponse = await apiRequest(explainBase, "/cli/ai/explain", { method: "POST", body: { audit: explainPayload } }, explainKey);
|
|
458
|
-
succeedSpinner(explainSpinner, "Explanation ready");
|
|
459
|
-
console.error("");
|
|
460
|
-
console.error(chalk.bold("AI Explanation"));
|
|
461
|
-
console.error(chalk.dim("─".repeat(40)));
|
|
462
|
-
for (const bullet of explainResponse.data.summary) {
|
|
463
|
-
console.error(` ${chalk.cyan("*")} ${bullet}`);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
catch (explainErr) {
|
|
467
|
-
console.error(chalk.dim(`\n(AI explanation unavailable: ${explainErr instanceof Error ? explainErr.message : String(explainErr)})`));
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Print quality gate result to stderr.
|
|
472
|
-
*/
|
|
473
|
-
function outputQualityGateResult(gateResult) {
|
|
474
|
-
console.error("");
|
|
475
|
-
if (gateResult.bypassed) {
|
|
476
|
-
console.error(chalk.yellow(`Quality gate bypassed: ${gateResult.bypassReason}`));
|
|
477
|
-
}
|
|
478
|
-
else if (gateResult.passed) {
|
|
479
|
-
console.error(chalk.green("Quality gate: PASSED"));
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
console.error(chalk.red("Quality gate: FAILED"));
|
|
483
|
-
for (const violation of gateResult.violations) {
|
|
484
|
-
console.error(chalk.red(` - ${violation.message}`));
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
console.error("");
|
|
488
|
-
console.error(`New issues: ${gateResult.summary.newIssues.error} errors, ` +
|
|
489
|
-
`${gateResult.summary.newIssues.warning} warnings, ` +
|
|
490
|
-
`${gateResult.summary.newIssues.info} info`);
|
|
491
|
-
if (gateResult.summary.fixedIssues > 0) {
|
|
492
|
-
console.error(chalk.green(`Fixed: ${gateResult.summary.fixedIssues} issues`));
|
|
493
|
-
}
|
|
494
|
-
if (gateResult.summary.existingIssues > 0) {
|
|
495
|
-
console.error(chalk.dim(`Existing (baselined): ${gateResult.summary.existingIssues} issues`));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Write formatted audit results to output or stdout.
|
|
500
|
-
*/
|
|
501
|
-
function outputFormattedResults(filteredResult, format, formatter, groupBy, options, quiet) {
|
|
502
|
-
const formatOptions = {
|
|
503
|
-
human: {
|
|
504
|
-
groupBy,
|
|
505
|
-
showScores: true,
|
|
506
|
-
showSummary: true,
|
|
507
|
-
},
|
|
508
|
-
};
|
|
509
|
-
if (format === "json") {
|
|
510
|
-
if (options.output) {
|
|
511
|
-
const jsonStr = JSON.stringify(createEnvelope(filteredResult, "audit"), null, 2);
|
|
512
|
-
const filePath = writeOutputToFile(jsonStr, options.output);
|
|
513
|
-
if (filePath && !quiet) {
|
|
514
|
-
console.error(`Report written to: ${filePath}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
writeJsonOutput(filteredResult, "audit");
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
const output = formatter.formatResult(filteredResult, formatOptions);
|
|
523
|
-
const defaultPath = getDefaultOutputPath(format);
|
|
524
|
-
const filePath = writeOutputToFile(output, options.output, defaultPath);
|
|
525
|
-
if (filePath && !quiet) {
|
|
526
|
-
console.error(`Report written to: ${filePath}`);
|
|
527
|
-
}
|
|
528
|
-
else if (!filePath) {
|
|
529
|
-
writeStdout(output);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* Execute the audit command.
|
|
535
|
-
*/
|
|
536
|
-
async function executeAudit(targetUrl, options, config) {
|
|
537
|
-
// Resolve options with precedence: flags > env > config > defaults
|
|
538
|
-
const mode = options.mode || config.mode || "basic";
|
|
539
|
-
const timeout = options.timeout || config.timeout || 60000;
|
|
540
|
-
const interval = options.interval || config.interval || 5000;
|
|
541
|
-
const wait = options.wait ?? true; // Default to waiting for completion
|
|
542
|
-
const quiet = options.quiet ?? false;
|
|
543
|
-
const interactive = options.interactive ?? false;
|
|
544
|
-
// Interactive mode validation
|
|
545
|
-
if (interactive) {
|
|
546
|
-
if (!wait) {
|
|
547
|
-
throw new Error("--interactive requires --wait (audit must complete before interactive mode)");
|
|
548
|
-
}
|
|
549
|
-
if (!isInteractive()) {
|
|
550
|
-
throw new Error("Interactive mode requires a terminal. Use --format json in CI or piped environments.");
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// Resolve API settings
|
|
554
|
-
const base = resolveApiBase(options.base);
|
|
555
|
-
const apiKey = getApiKey(config.apiKey);
|
|
556
|
-
// Resolve output format using per-command registry
|
|
557
|
-
// --json flag is a shorthand for --format json (convenient for piping)
|
|
558
|
-
const machineMode = options.machine || false;
|
|
559
|
-
const explicitFormat = options.json ? "json" : (options.format || config.output?.format);
|
|
560
|
-
const validatedFormat = resolveCommandFormat("audit", explicitFormat, machineMode);
|
|
561
|
-
const format = validatedFormat;
|
|
562
|
-
const formatter = createOutput(format);
|
|
563
|
-
const groupBy = options.groupBy || config.output?.groupBy || "severity";
|
|
564
|
-
// Determine UI mode: dashboard (full-screen) vs spinner (inline)
|
|
565
|
-
const useDashboard = wait && !quiet && !machineMode && options.dashboard !== false;
|
|
566
|
-
const useSpinner = wait && isTTY() && !quiet && !useDashboard;
|
|
567
|
-
// Create dashboard renderer or fallback spinner
|
|
568
|
-
let renderer = null;
|
|
569
|
-
let keyboard = null;
|
|
570
|
-
let aborted = false;
|
|
571
|
-
const spinner = useSpinner
|
|
572
|
-
? createSpinner(`Auditing ${targetUrl}...`)
|
|
573
|
-
: null;
|
|
574
|
-
if (useDashboard) {
|
|
575
|
-
renderer = createRenderer("auto");
|
|
576
|
-
keyboard = createKeyboardHandler();
|
|
577
|
-
keyboard.on("quit", () => {
|
|
578
|
-
aborted = true;
|
|
579
|
-
renderer?.dispose();
|
|
580
|
-
keyboard?.dispose();
|
|
581
|
-
process.stderr.write("\nAudit aborted by user.\n");
|
|
582
|
-
process.exitCode = ExitCode.ERROR;
|
|
583
|
-
});
|
|
584
|
-
keyboard.start();
|
|
585
|
-
}
|
|
586
|
-
const auditStartTime = Date.now();
|
|
587
|
-
try {
|
|
588
|
-
// Start spinner (dashboard renders on first update)
|
|
589
|
-
spinner?.start();
|
|
590
|
-
// Route based on URL type: local URLs are captured client-side
|
|
591
|
-
let created;
|
|
592
|
-
if (isLocalUrl(targetUrl)) {
|
|
593
|
-
// Local URL — capture HTML on this machine, send to /analyze
|
|
594
|
-
if (!quiet) {
|
|
595
|
-
console.error(chalk.cyan("Local URL detected — capturing page locally..."));
|
|
596
|
-
console.error(chalk.dim("Note: Local audits use static analysis. Some checks " +
|
|
597
|
-
"(keyboard nav, visual hierarchy) are unavailable."));
|
|
598
|
-
}
|
|
599
|
-
if (spinner) {
|
|
600
|
-
updateSpinner(spinner, "Fetching local page...", 10, 100);
|
|
601
|
-
}
|
|
602
|
-
let captured;
|
|
603
|
-
try {
|
|
604
|
-
captured = await captureLocalPage(targetUrl, { timeoutMs: timeout });
|
|
605
|
-
}
|
|
606
|
-
catch (captureErr) {
|
|
607
|
-
throw new Error(`Cannot reach ${targetUrl}. Ensure your local server is running.\n` +
|
|
608
|
-
` ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
|
|
609
|
-
}
|
|
610
|
-
if (spinner) {
|
|
611
|
-
updateSpinner(spinner, "Analyzing captured page...", 40, 100);
|
|
612
|
-
}
|
|
613
|
-
created = await apiRequest(base, "/analyze", {
|
|
614
|
-
method: "POST",
|
|
615
|
-
body: { html: captured.html, url: targetUrl, mode },
|
|
616
|
-
}, apiKey);
|
|
617
|
-
}
|
|
618
|
-
else {
|
|
619
|
-
// Public URL — send to cloud API for full audit
|
|
620
|
-
created = await apiRequest(base, "/audit", {
|
|
621
|
-
method: "POST",
|
|
622
|
-
body: { url: targetUrl, mode },
|
|
623
|
-
}, apiKey);
|
|
624
|
-
}
|
|
625
|
-
// If not waiting, just output the job info
|
|
626
|
-
if (!wait) {
|
|
627
|
-
spinner?.stop();
|
|
628
|
-
renderer?.dispose();
|
|
629
|
-
keyboard?.dispose();
|
|
630
|
-
outputFireAndForget(created, format, formatter, options, quiet);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
// Determine if the server returned results synchronously (status 200)
|
|
634
|
-
// or queued the job for background processing (status 202, legacy servers)
|
|
635
|
-
const isSyncResponse = created.status === "completed" && created.scores;
|
|
636
|
-
let result;
|
|
637
|
-
if (created.status === "failed") {
|
|
638
|
-
// Server ran the audit synchronously but it failed
|
|
639
|
-
throw new Error(created.error || "Audit failed on server");
|
|
640
|
-
}
|
|
641
|
-
if (isSyncResponse) {
|
|
642
|
-
// Server already ran the audit — use the response directly
|
|
643
|
-
result = created;
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
// Legacy/async path: poll for completion
|
|
647
|
-
if (!created.job_id) {
|
|
648
|
-
throw new Error("Audit response missing job_id");
|
|
649
|
-
}
|
|
650
|
-
result = await waitForAudit(base, created.job_id, timeout, interval, apiKey, (progress, status) => {
|
|
651
|
-
if (aborted)
|
|
652
|
-
return;
|
|
653
|
-
if (renderer) {
|
|
654
|
-
const phase = mapStatusToPhase(status);
|
|
655
|
-
const state = {
|
|
656
|
-
phase,
|
|
657
|
-
phaseIndex: phaseIndex(phase),
|
|
658
|
-
phaseTotal: phaseTotal(),
|
|
659
|
-
url: targetUrl,
|
|
660
|
-
mode,
|
|
661
|
-
progress: { audit: progress },
|
|
662
|
-
totals: { audit: 100 },
|
|
663
|
-
issueCount: 0,
|
|
664
|
-
scorePreview: null,
|
|
665
|
-
verbose: false,
|
|
666
|
-
elapsed: Date.now() - auditStartTime,
|
|
667
|
-
};
|
|
668
|
-
renderer.update(state);
|
|
669
|
-
}
|
|
670
|
-
if (spinner) {
|
|
671
|
-
updateSpinner(spinner, `Auditing ${targetUrl}`, progress, 100);
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
// Finish dashboard or spinner
|
|
676
|
-
if (renderer) {
|
|
677
|
-
const overallScore = getOverallScoreFromResult(result);
|
|
678
|
-
const summaryResult = {
|
|
679
|
-
url: targetUrl,
|
|
680
|
-
mode,
|
|
681
|
-
overallScore: overallScore ?? 0,
|
|
682
|
-
scores: extractNumericScores(result.scores),
|
|
683
|
-
issueCount: countTotalIssues(result.issues),
|
|
684
|
-
passed: (overallScore ?? 0) >= 70,
|
|
685
|
-
elapsed: Date.now() - auditStartTime,
|
|
686
|
-
};
|
|
687
|
-
renderer.finish(summaryResult);
|
|
688
|
-
keyboard?.dispose();
|
|
689
|
-
}
|
|
690
|
-
if (spinner) {
|
|
691
|
-
succeedSpinner(spinner, `Audit complete: ${targetUrl}`);
|
|
692
|
-
}
|
|
693
|
-
// Save repro artifacts if requested
|
|
694
|
-
if (created.job_id) {
|
|
695
|
-
saveReproArtifacts(created.job_id, result, options, quiet);
|
|
696
|
-
}
|
|
697
|
-
// Apply filters to issues
|
|
698
|
-
let issues = normalizeIssues(result.issues);
|
|
699
|
-
if (options.severity) {
|
|
700
|
-
issues = filterBySeverity(issues, options.severity);
|
|
701
|
-
}
|
|
702
|
-
if (options.category) {
|
|
703
|
-
issues = filterByCategory(issues, options.category);
|
|
704
|
-
}
|
|
705
|
-
// Create filtered result for output
|
|
706
|
-
const filteredResult = {
|
|
707
|
-
...result,
|
|
708
|
-
issues,
|
|
709
|
-
};
|
|
710
|
-
// Load and apply policy (CICD-17)
|
|
711
|
-
await loadAndResolvePolicy(options, quiet);
|
|
712
|
-
// Build quality gate config and evaluate
|
|
713
|
-
const gateConfig = buildQualityGateConfig(config, options);
|
|
714
|
-
// Load baseline if provided
|
|
715
|
-
const baselinePath = options.baseline || config.baseline?.path;
|
|
716
|
-
const baseline = baselinePath ? await loadBaseline(baselinePath) : null;
|
|
717
|
-
// Detect PR labels for bypass check
|
|
718
|
-
const prLabels = detectPRLabels();
|
|
719
|
-
// Evaluate quality gate
|
|
720
|
-
const gateResult = evaluateQualityGate({
|
|
721
|
-
auditResult: {
|
|
722
|
-
issues: issues, // Use the normalized issues array
|
|
723
|
-
scores: result.scores,
|
|
724
|
-
},
|
|
725
|
-
baseline,
|
|
726
|
-
config: gateConfig,
|
|
727
|
-
labels: prLabels,
|
|
728
|
-
});
|
|
729
|
-
// Use quality gate exit code
|
|
730
|
-
const exitCode = gateResult.exitCode;
|
|
731
|
-
// Format and output results
|
|
732
|
-
outputFormattedResults(filteredResult, format, formatter, groupBy, options, quiet);
|
|
733
|
-
// Inline AI explanation (--explain flag, PROG-04)
|
|
734
|
-
if (options.explain && issues.length > 0) {
|
|
735
|
-
await runInlineExplanation(issues, result, targetUrl, options, config);
|
|
736
|
-
}
|
|
737
|
-
// Output quality gate result
|
|
738
|
-
if (!quiet) {
|
|
739
|
-
outputQualityGateResult(gateResult);
|
|
740
|
-
}
|
|
741
|
-
// Save CI artifact bundle if requested
|
|
742
|
-
if (options.uploadArtifacts && created.job_id) {
|
|
743
|
-
const artifactJobId = options.jobId || created.job_id;
|
|
744
|
-
saveArtifactBundle(artifactJobId, result, issues, exitCode, quiet);
|
|
745
|
-
}
|
|
746
|
-
// Interactive mode: run fix wizard if there are issues
|
|
747
|
-
if (interactive && issues.length > 0) {
|
|
748
|
-
await runFixWizard(issues, {
|
|
749
|
-
jobId: result.job_id,
|
|
750
|
-
url: targetUrl,
|
|
751
|
-
base: options.base,
|
|
752
|
-
config,
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
// Set exit code
|
|
756
|
-
if (exitCode !== ExitCode.SUCCESS) {
|
|
757
|
-
process.exitCode = exitCode;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
catch (error) {
|
|
761
|
-
// Stop dashboard or spinner with failure
|
|
762
|
-
renderer?.dispose();
|
|
763
|
-
keyboard?.dispose();
|
|
764
|
-
if (spinner) {
|
|
765
|
-
failSpinner(spinner, `Audit failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
766
|
-
}
|
|
767
|
-
throw error;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Map API audit status to TUI phase name.
|
|
772
|
-
*/
|
|
773
|
-
function mapStatusToPhase(status) {
|
|
774
|
-
switch (status) {
|
|
775
|
-
case "queued":
|
|
776
|
-
case "pending":
|
|
777
|
-
return AuditPhase.Connecting;
|
|
778
|
-
case "crawling":
|
|
779
|
-
return AuditPhase.Crawling;
|
|
780
|
-
case "running":
|
|
781
|
-
case "analyzing":
|
|
782
|
-
return AuditPhase.Analyzing;
|
|
783
|
-
case "scoring":
|
|
784
|
-
return AuditPhase.Scoring;
|
|
785
|
-
case "completed":
|
|
786
|
-
return AuditPhase.Done;
|
|
787
|
-
case "failed":
|
|
788
|
-
return AuditPhase.Failed;
|
|
789
|
-
default:
|
|
790
|
-
return AuditPhase.Analyzing;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Extract overall score from audit result.
|
|
795
|
-
*/
|
|
796
|
-
function getOverallScoreFromResult(result) {
|
|
797
|
-
if (!result.scores)
|
|
798
|
-
return null;
|
|
799
|
-
const scores = result.scores;
|
|
800
|
-
const direct = scores.overall ?? scores.ux ?? scores.total;
|
|
801
|
-
if (typeof direct === "number" && Number.isFinite(direct))
|
|
802
|
-
return direct;
|
|
803
|
-
const numeric = Object.values(scores)
|
|
804
|
-
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
805
|
-
if (numeric.length === 0)
|
|
806
|
-
return null;
|
|
807
|
-
return Math.round(numeric.reduce((a, b) => a + b, 0) / numeric.length);
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Extract numeric scores from result scores object.
|
|
811
|
-
*/
|
|
812
|
-
function extractNumericScores(scores) {
|
|
813
|
-
if (!scores)
|
|
814
|
-
return {};
|
|
815
|
-
const result = {};
|
|
816
|
-
for (const [key, value] of Object.entries(scores)) {
|
|
817
|
-
if (typeof value === "number" && key !== "overall") {
|
|
818
|
-
result[key] = value;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
return result;
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Count total issues from various result formats.
|
|
825
|
-
*/
|
|
826
|
-
function countTotalIssues(issues) {
|
|
827
|
-
if (Array.isArray(issues))
|
|
828
|
-
return issues.length;
|
|
829
|
-
if (issues && typeof issues === "object") {
|
|
830
|
-
return Object.values(issues)
|
|
831
|
-
.flatMap((v) => (Array.isArray(v) ? v : []))
|
|
832
|
-
.length;
|
|
833
|
-
}
|
|
834
|
-
return 0;
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Register the audit command with the Commander program.
|
|
838
|
-
*/
|
|
839
|
-
export function registerAuditCommand(program) {
|
|
840
|
-
program
|
|
841
|
-
.command("audit [url]")
|
|
842
|
-
.description("Run UX and accessibility audit. Localhost and private URLs are " +
|
|
843
|
-
"captured locally and analyzed via static HTML analysis.")
|
|
844
|
-
.option("-u, --url <url>", "URL to audit")
|
|
845
|
-
.option("--repo <repo>", "GitHub repository to audit (owner/repo)")
|
|
846
|
-
.option("--storybook <url>", "Storybook URL to audit")
|
|
847
|
-
.option("--routes <routes>", "Comma-separated list of routes to audit")
|
|
848
|
-
.option("--auth-profile <profile>", "Authentication profile for protected pages")
|
|
849
|
-
.option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
|
|
850
|
-
.option("--format <format>", "Output format: json|sarif|junit|html|human (default: human in terminal, auto-detected in CI)")
|
|
851
|
-
.option("--json", "Shorthand for --format json (convenient for piping)")
|
|
852
|
-
.option("-o, --output <path>", "Output file path")
|
|
853
|
-
.option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
|
|
854
|
-
.option("--wait", "Wait for audit completion (default)")
|
|
855
|
-
.option("--no-wait", "Don't wait for audit completion")
|
|
856
|
-
.option("--severity <levels>", "Filter issues by severity: error|warning|info (comma-separated)")
|
|
857
|
-
.option("--category <categories>", "Filter issues by category (comma-separated)")
|
|
858
|
-
.option("--fail-on <severity>", "Exit 1 if new issues at or above severity: error|warning|info|none", parseFailOn)
|
|
859
|
-
.option("--threshold <score>", "Exit 3 if overall score below threshold (0-100)", parseThreshold)
|
|
860
|
-
.option("--max-new-errors <n>", "Maximum allowed new error-severity issues (default: 0)", (v) => validateNumeric(v, "max-new-errors", { min: 0, integer: true }))
|
|
861
|
-
.option("--max-new-warnings <n>", "Maximum allowed new warning-severity issues (default: unlimited)", (v) => validateNumeric(v, "max-new-warnings", { min: 0, integer: true }))
|
|
862
|
-
.option("--fail-on-existing", "Also fail on existing issues (legacy mode)")
|
|
863
|
-
.option("--bypass-labels <labels>", "Comma-separated PR labels that bypass quality gate")
|
|
864
|
-
.option("--baseline <path>", "Path to baseline file for new issue detection")
|
|
865
|
-
.option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout)
|
|
866
|
-
.option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval)
|
|
867
|
-
.option("--interactive", "Step through issues interactively (requires --wait)")
|
|
868
|
-
// Repro artifact options (CLI-17)
|
|
869
|
-
.option("--save-trace", "Save Playwright trace for debugging")
|
|
870
|
-
.option("--save-har", "Save HAR network log")
|
|
871
|
-
.option("--screenshots", "Save page screenshots")
|
|
872
|
-
.option("--dom-snapshots", "Save DOM snapshots")
|
|
873
|
-
// Performance options (CLI-18)
|
|
874
|
-
.option("--concurrency <n>", "Number of concurrent audits (1-50, default: 3)", parseConcurrency)
|
|
875
|
-
.option("--cache", "Enable route caching to speed up repeated audits")
|
|
876
|
-
// CI artifact bundling (CICD-08)
|
|
877
|
-
.option("--upload-artifacts", "Save all outputs to .vertaaux/artifacts/ for CI upload")
|
|
878
|
-
.option("--job-id <id>", "Job identifier for artifact directory naming")
|
|
879
|
-
// Incremental mode (CICD-07)
|
|
880
|
-
.option("--incremental", "Only audit routes changed in PR")
|
|
881
|
-
.option("--base-branch <branch>", "Base branch for comparison (default: auto-detect)")
|
|
882
|
-
// Budget mode (CICD-13)
|
|
883
|
-
.option("--budget <mode>", "Budget mode: quick|standard|full", parseBudget)
|
|
884
|
-
// Monorepo options (CICD-16)
|
|
885
|
-
.option("--workspace <name>", "Audit specific workspace in monorepo")
|
|
886
|
-
.option("--all-workspaces", "Audit all workspaces in monorepo")
|
|
887
|
-
.option("--parallel", "Run workspace audits in parallel")
|
|
888
|
-
.option("--detect-matrix", "Output CI matrix config for monorepo (JSON)")
|
|
889
|
-
// Caching options (CICD-14)
|
|
890
|
-
.option("--no-cache", "Disable route caching")
|
|
891
|
-
.option("--cache-dir <path>", "Custom cache directory")
|
|
892
|
-
// Logging options (CICD-18)
|
|
893
|
-
.option("--json-logs", "Output structured JSON logs for CI")
|
|
894
|
-
// Policy options (CICD-17)
|
|
895
|
-
.option("--policy <file>", "Path to policy file (default: auto-detect vertaa.policy.yml)")
|
|
896
|
-
.option("--explain", "Append AI explanation to audit results")
|
|
897
|
-
.action(async (urlArg, cmdOptions, command) => {
|
|
898
|
-
try {
|
|
899
|
-
// Initialize structured logger
|
|
900
|
-
const logger = createLogger({
|
|
901
|
-
json: cmdOptions.jsonLogs || false,
|
|
902
|
-
level: cmdOptions.quiet ? "error" : "info",
|
|
903
|
-
});
|
|
904
|
-
// Load config (supports --config global option)
|
|
905
|
-
const globalOpts = command.optsWithGlobals();
|
|
906
|
-
const config = await resolveConfig(globalOpts.config);
|
|
907
|
-
// Propagate global --machine flag to command options
|
|
908
|
-
cmdOptions.machine = globalOpts.machine || false;
|
|
909
|
-
// Handle monorepo detection and matrix output (CICD-16)
|
|
910
|
-
if (cmdOptions.detectMatrix || cmdOptions.allWorkspaces || cmdOptions.workspace) {
|
|
911
|
-
const monorepo = await detectMonorepo();
|
|
912
|
-
if (monorepo.type === "none") {
|
|
913
|
-
if (cmdOptions.detectMatrix) {
|
|
914
|
-
// Output empty matrix for non-monorepo
|
|
915
|
-
process.stdout.write(JSON.stringify({ include: [] }) + "\n");
|
|
916
|
-
process.exit(ExitCode.SUCCESS);
|
|
917
|
-
}
|
|
918
|
-
// Not a monorepo, continue with normal audit
|
|
919
|
-
}
|
|
920
|
-
else {
|
|
921
|
-
logger.info(`Detected ${monorepo.type} monorepo`, {
|
|
922
|
-
workspaces: monorepo.workspaces.length,
|
|
923
|
-
});
|
|
924
|
-
const apps = getAuditableApps(monorepo);
|
|
925
|
-
logger.info(`Found ${apps.length} auditable apps`, {
|
|
926
|
-
apps: apps.map((a) => a.name),
|
|
927
|
-
});
|
|
928
|
-
// Output matrix config for CI
|
|
929
|
-
if (cmdOptions.detectMatrix) {
|
|
930
|
-
const matrix = generateMatrixConfig(apps);
|
|
931
|
-
process.stdout.write(JSON.stringify(matrix) + "\n");
|
|
932
|
-
process.exit(ExitCode.SUCCESS);
|
|
933
|
-
}
|
|
934
|
-
// Audit specific workspace
|
|
935
|
-
if (cmdOptions.workspace) {
|
|
936
|
-
const workspace = monorepo.workspaces.find((w) => w.name === cmdOptions.workspace);
|
|
937
|
-
if (!workspace) {
|
|
938
|
-
console.error(`Error: Workspace "${cmdOptions.workspace}" not found`);
|
|
939
|
-
console.error(`Available: ${monorepo.workspaces.map((w) => w.name).join(", ")}`);
|
|
940
|
-
process.exit(ExitCode.ERROR);
|
|
941
|
-
}
|
|
942
|
-
// Use workspace URL or infer from type
|
|
943
|
-
if (workspace.url) {
|
|
944
|
-
urlArg = workspace.url;
|
|
945
|
-
logger.info(`Auditing workspace`, { workspace: workspace.name, url: urlArg });
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
// Audit all workspaces
|
|
949
|
-
if (cmdOptions.allWorkspaces) {
|
|
950
|
-
console.error(chalk.dim(`Auditing ${apps.length} workspaces in ${monorepo.type} monorepo`));
|
|
951
|
-
const results = [];
|
|
952
|
-
const startTime = Date.now();
|
|
953
|
-
if (cmdOptions.parallel) {
|
|
954
|
-
// Parallel execution
|
|
955
|
-
const promises = apps.map(async (app) => {
|
|
956
|
-
const wsStartTime = Date.now();
|
|
957
|
-
logger.info(`Starting audit`, { workspace: app.name });
|
|
958
|
-
try {
|
|
959
|
-
// Note: In production, this would call executeAudit
|
|
960
|
-
// For now, we return a placeholder result
|
|
961
|
-
return {
|
|
962
|
-
workspace: app,
|
|
963
|
-
auditResult: { url: app.url, issues: [], status: "complete" },
|
|
964
|
-
duration: Date.now() - wsStartTime,
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
catch (error) {
|
|
968
|
-
return {
|
|
969
|
-
workspace: app,
|
|
970
|
-
auditResult: null,
|
|
971
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
972
|
-
duration: Date.now() - wsStartTime,
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
});
|
|
976
|
-
results.push(...(await Promise.all(promises)));
|
|
977
|
-
}
|
|
978
|
-
else {
|
|
979
|
-
// Sequential execution
|
|
980
|
-
for (const app of apps) {
|
|
981
|
-
const wsStartTime = Date.now();
|
|
982
|
-
logger.info(`Auditing workspace`, { workspace: app.name });
|
|
983
|
-
try {
|
|
984
|
-
results.push({
|
|
985
|
-
workspace: app,
|
|
986
|
-
auditResult: { url: app.url, issues: [], status: "complete" },
|
|
987
|
-
duration: Date.now() - wsStartTime,
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
catch (error) {
|
|
991
|
-
results.push({
|
|
992
|
-
workspace: app,
|
|
993
|
-
auditResult: null,
|
|
994
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
995
|
-
duration: Date.now() - wsStartTime,
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
// Aggregate and output results
|
|
1001
|
-
const aggregated = aggregateResults(results);
|
|
1002
|
-
aggregated.totalDuration = Date.now() - startTime;
|
|
1003
|
-
if (cmdOptions.json || cmdOptions.format === "json") {
|
|
1004
|
-
writeJsonOutput(aggregated, "audit");
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
writeStdout(formatAggregatedResults(aggregated));
|
|
1008
|
-
}
|
|
1009
|
-
// Exit with error if any failed
|
|
1010
|
-
if (aggregated.failedAudits > 0) {
|
|
1011
|
-
process.exit(ExitCode.ERROR);
|
|
1012
|
-
}
|
|
1013
|
-
if (aggregated.issuesBySeverity.error > 0) {
|
|
1014
|
-
process.exit(ExitCode.ISSUES_FOUND);
|
|
1015
|
-
}
|
|
1016
|
-
process.exit(ExitCode.SUCCESS);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
// Handle incremental mode (CICD-07)
|
|
1021
|
-
if (cmdOptions.incremental) {
|
|
1022
|
-
const baseBranch = validateBranchName(cmdOptions.baseBranch || detectBaseBranch());
|
|
1023
|
-
const changedResult = await getChangedRoutes({
|
|
1024
|
-
baseBranch,
|
|
1025
|
-
routePatterns: [], // Use default patterns
|
|
1026
|
-
});
|
|
1027
|
-
if (!changedResult.hasChanges) {
|
|
1028
|
-
console.error(chalk.green("No relevant route changes detected. Skipping audit."));
|
|
1029
|
-
process.exit(ExitCode.SUCCESS);
|
|
1030
|
-
}
|
|
1031
|
-
// Log which routes will be audited
|
|
1032
|
-
console.error(chalk.dim(`Incremental mode: Auditing ${changedResult.routes.length} changed routes`));
|
|
1033
|
-
for (const route of changedResult.routes) {
|
|
1034
|
-
console.error(chalk.dim(` - ${route}`));
|
|
1035
|
-
}
|
|
1036
|
-
// If routes were detected, use them instead of URL argument
|
|
1037
|
-
if (!config.defaultUrl) {
|
|
1038
|
-
console.error("Error: --incremental requires defaultUrl in config to construct full URLs.");
|
|
1039
|
-
process.exit(ExitCode.ERROR);
|
|
1040
|
-
}
|
|
1041
|
-
// Set routes option for executeAudit
|
|
1042
|
-
cmdOptions.routes = changedResult.routes.join(",");
|
|
1043
|
-
}
|
|
1044
|
-
// Handle budget mode (CICD-13)
|
|
1045
|
-
if (cmdOptions.budget) {
|
|
1046
|
-
const budgetConfig = getBudgetConfig(cmdOptions.budget);
|
|
1047
|
-
// Apply budget constraints to options
|
|
1048
|
-
if (!cmdOptions.concurrency) {
|
|
1049
|
-
cmdOptions.concurrency = budgetConfig.concurrency;
|
|
1050
|
-
}
|
|
1051
|
-
if (!cmdOptions.timeout) {
|
|
1052
|
-
cmdOptions.timeout = budgetConfig.maxTime;
|
|
1053
|
-
}
|
|
1054
|
-
console.error(chalk.dim(`Budget mode: ${cmdOptions.budget} (max ${budgetConfig.maxPages} pages, ${budgetConfig.maxTime / 1000}s timeout, ${budgetConfig.concurrency} concurrent)`));
|
|
1055
|
-
}
|
|
1056
|
-
// Resolve target URL
|
|
1057
|
-
// Priority: positional > --url > --repo > --storybook > --routes > config default
|
|
1058
|
-
let targetUrl;
|
|
1059
|
-
if (urlArg) {
|
|
1060
|
-
targetUrl = urlArg;
|
|
1061
|
-
}
|
|
1062
|
-
else if (cmdOptions.url) {
|
|
1063
|
-
targetUrl = cmdOptions.url;
|
|
1064
|
-
}
|
|
1065
|
-
else if (cmdOptions.repo) {
|
|
1066
|
-
// For repo audits, construct a special URL or handle differently
|
|
1067
|
-
// For now, we'll just pass it as a marker
|
|
1068
|
-
targetUrl = `repo:${cmdOptions.repo}`;
|
|
1069
|
-
}
|
|
1070
|
-
else if (cmdOptions.storybook) {
|
|
1071
|
-
targetUrl = cmdOptions.storybook;
|
|
1072
|
-
}
|
|
1073
|
-
else if (cmdOptions.routes) {
|
|
1074
|
-
// Routes require a base URL from config
|
|
1075
|
-
if (!config.defaultUrl) {
|
|
1076
|
-
console.error("Error: --routes requires defaultUrl in config or a base URL.");
|
|
1077
|
-
process.exit(ExitCode.ERROR);
|
|
1078
|
-
}
|
|
1079
|
-
// For routes, we'll handle them as comma-separated paths
|
|
1080
|
-
const routes = cmdOptions.routes.split(",").map((r) => r.trim());
|
|
1081
|
-
targetUrl = `${config.defaultUrl}${routes[0]}`; // First route for now
|
|
1082
|
-
}
|
|
1083
|
-
else if (config.defaultUrl) {
|
|
1084
|
-
targetUrl = config.defaultUrl;
|
|
1085
|
-
}
|
|
1086
|
-
if (!targetUrl) {
|
|
1087
|
-
console.error("Error: URL is required. Provide as argument, --url flag, or defaultUrl in config.");
|
|
1088
|
-
process.exit(ExitCode.ERROR);
|
|
1089
|
-
}
|
|
1090
|
-
await executeAudit(targetUrl, cmdOptions, config);
|
|
1091
|
-
}
|
|
1092
|
-
catch (error) {
|
|
1093
|
-
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
1094
|
-
process.exit(ExitCode.ERROR);
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1
|
+
// cli/src/commands/audit.ts — barrel re-export
|
|
2
|
+
export { registerAuditCommand } from "./audit/index.js";
|