@vertaaux/cli 0.2.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/README.md +345 -0
- package/dist/auth/ci-token.d.ts +49 -0
- package/dist/auth/ci-token.d.ts.map +1 -0
- package/dist/auth/ci-token.js +83 -0
- package/dist/auth/device-flow.d.ts +66 -0
- package/dist/auth/device-flow.d.ts.map +1 -0
- package/dist/auth/device-flow.js +156 -0
- package/dist/auth/token-store.d.ts +53 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +78 -0
- package/dist/baseline/diff.d.ts +57 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +152 -0
- package/dist/baseline/hash.d.ts +54 -0
- package/dist/baseline/hash.d.ts.map +1 -0
- package/dist/baseline/hash.js +66 -0
- package/dist/baseline/manager.d.ts +89 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +157 -0
- package/dist/cache/index.d.ts +8 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +7 -0
- package/dist/cache/route-cache.d.ts +119 -0
- package/dist/cache/route-cache.d.ts.map +1 -0
- package/dist/cache/route-cache.js +213 -0
- package/dist/ci/changed-routes.d.ts +95 -0
- package/dist/ci/changed-routes.d.ts.map +1 -0
- package/dist/ci/changed-routes.js +304 -0
- package/dist/ci/github-api.d.ts +68 -0
- package/dist/ci/github-api.d.ts.map +1 -0
- package/dist/ci/github-api.js +138 -0
- package/dist/ci/gitlab-api.d.ts +75 -0
- package/dist/ci/gitlab-api.d.ts.map +1 -0
- package/dist/ci/gitlab-api.js +180 -0
- package/dist/ci/index.d.ts +6 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +4 -0
- package/dist/commands/audit.d.ts +58 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +862 -0
- package/dist/commands/baseline.d.ts +22 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +210 -0
- package/dist/commands/comment.d.ts +14 -0
- package/dist/commands/comment.d.ts.map +1 -0
- package/dist/commands/comment.js +363 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +196 -0
- package/dist/commands/doctor.d.ts +58 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +338 -0
- package/dist/commands/download.d.ts +12 -0
- package/dist/commands/download.d.ts.map +1 -0
- package/dist/commands/download.js +183 -0
- package/dist/commands/explain.d.ts +62 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +302 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +212 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +222 -0
- package/dist/commands/policy.d.ts +13 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +347 -0
- package/dist/commands/upload.d.ts +12 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +158 -0
- package/dist/config/defaults.d.ts +21 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +49 -0
- package/dist/config/loader.d.ts +66 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +167 -0
- package/dist/config/schema.d.ts +55 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +6 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1090 -0
- package/dist/interactive/fix-wizard.d.ts +44 -0
- package/dist/interactive/fix-wizard.d.ts.map +1 -0
- package/dist/interactive/fix-wizard.js +286 -0
- package/dist/interactive/init-wizard.d.ts +32 -0
- package/dist/interactive/init-wizard.d.ts.map +1 -0
- package/dist/interactive/init-wizard.js +193 -0
- package/dist/interactive/prompts.d.ts +62 -0
- package/dist/interactive/prompts.d.ts.map +1 -0
- package/dist/interactive/prompts.js +78 -0
- package/dist/monorepo/detector.d.ts +70 -0
- package/dist/monorepo/detector.d.ts.map +1 -0
- package/dist/monorepo/detector.js +278 -0
- package/dist/monorepo/index.d.ts +9 -0
- package/dist/monorepo/index.d.ts.map +1 -0
- package/dist/monorepo/index.js +8 -0
- package/dist/monorepo/workspace.d.ts +142 -0
- package/dist/monorepo/workspace.d.ts.map +1 -0
- package/dist/monorepo/workspace.js +171 -0
- package/dist/output/envelope.d.ts +21 -0
- package/dist/output/envelope.d.ts.map +1 -0
- package/dist/output/envelope.js +27 -0
- package/dist/output/factory.d.ts +73 -0
- package/dist/output/factory.d.ts.map +1 -0
- package/dist/output/factory.js +60 -0
- package/dist/output/formats.d.ts +11 -0
- package/dist/output/formats.d.ts.map +1 -0
- package/dist/output/formats.js +41 -0
- package/dist/output/html.d.ts +45 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/html.js +607 -0
- package/dist/output/human.d.ts +41 -0
- package/dist/output/human.d.ts.map +1 -0
- package/dist/output/human.js +274 -0
- package/dist/output/json.d.ts +42 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +37 -0
- package/dist/output/junit.d.ts +56 -0
- package/dist/output/junit.d.ts.map +1 -0
- package/dist/output/junit.js +135 -0
- package/dist/output/markdown.d.ts +77 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +411 -0
- package/dist/output/sarif.d.ts +160 -0
- package/dist/output/sarif.d.ts.map +1 -0
- package/dist/output/sarif.js +207 -0
- package/dist/policy/evaluator.d.ts +111 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +362 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +11 -0
- package/dist/policy/loader.d.ts +97 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +281 -0
- package/dist/policy/schema.d.ts +297 -0
- package/dist/policy/schema.d.ts.map +1 -0
- package/dist/policy/schema.js +230 -0
- package/dist/quality-gate/evaluator.d.ts +58 -0
- package/dist/quality-gate/evaluator.d.ts.map +1 -0
- package/dist/quality-gate/evaluator.js +274 -0
- package/dist/quality-gate/index.d.ts +10 -0
- package/dist/quality-gate/index.d.ts.map +1 -0
- package/dist/quality-gate/index.js +7 -0
- package/dist/quality-gate/types.d.ts +103 -0
- package/dist/quality-gate/types.d.ts.map +1 -0
- package/dist/quality-gate/types.js +23 -0
- package/dist/templates/azure-devops.d.ts +25 -0
- package/dist/templates/azure-devops.d.ts.map +1 -0
- package/dist/templates/azure-devops.js +109 -0
- package/dist/templates/circleci.d.ts +28 -0
- package/dist/templates/circleci.d.ts.map +1 -0
- package/dist/templates/circleci.js +86 -0
- package/dist/templates/github-actions.d.ts +81 -0
- package/dist/templates/github-actions.d.ts.map +1 -0
- package/dist/templates/github-actions.js +393 -0
- package/dist/templates/gitlab-ci.d.ts +26 -0
- package/dist/templates/gitlab-ci.d.ts.map +1 -0
- package/dist/templates/gitlab-ci.js +70 -0
- package/dist/templates/index.d.ts +72 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +112 -0
- package/dist/templates/jenkins.d.ts +26 -0
- package/dist/templates/jenkins.d.ts.map +1 -0
- package/dist/templates/jenkins.js +110 -0
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +84 -0
- package/dist/ui/diagnostics.d.ts +39 -0
- package/dist/ui/diagnostics.d.ts.map +1 -0
- package/dist/ui/diagnostics.js +153 -0
- package/dist/ui/spinner.d.ts +61 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +101 -0
- package/dist/ui/table.d.ts +63 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +236 -0
- package/dist/utils/client.d.ts +82 -0
- package/dist/utils/client.d.ts.map +1 -0
- package/dist/utils/client.js +128 -0
- package/dist/utils/detect-env.d.ts +59 -0
- package/dist/utils/detect-env.d.ts.map +1 -0
- package/dist/utils/detect-env.js +115 -0
- package/dist/utils/exit-codes.d.ts +47 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +61 -0
- package/dist/utils/logger.d.ts +87 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +185 -0
- package/dist/utils/sanitize.d.ts +36 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +64 -0
- package/dist/utils/validators.d.ts +41 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +123 -0
- package/package.json +63 -0
- package/schemas/vertaaux.config.schema.json +103 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Syncs audit results from VertaaUX cloud to local.
|
|
5
|
+
* Enables pulling results from CI runs or shared audits.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import { loadToken } from "../auth/token-store.js";
|
|
12
|
+
import { getCIToken } from "../auth/ci-token.js";
|
|
13
|
+
import { resolveApiBase } from "../utils/client.js";
|
|
14
|
+
import { resolveConfig } from "../config/loader.js";
|
|
15
|
+
import { saveBaseline } from "../baseline/manager.js";
|
|
16
|
+
import { isInteractive, confirmAction } from "../interactive/prompts.js";
|
|
17
|
+
import { ExitCode } from "../utils/exit-codes.js";
|
|
18
|
+
import { assertPathContainment } from "../utils/sanitize.js";
|
|
19
|
+
/**
|
|
20
|
+
* Default output directory.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_OUTPUT_DIR = ".vertaaux";
|
|
23
|
+
/**
|
|
24
|
+
* Get authentication token from stored credentials or environment.
|
|
25
|
+
*/
|
|
26
|
+
async function getAuthToken() {
|
|
27
|
+
// Check stored token first
|
|
28
|
+
const storedToken = await loadToken();
|
|
29
|
+
if (storedToken?.accessToken) {
|
|
30
|
+
return storedToken.accessToken;
|
|
31
|
+
}
|
|
32
|
+
// Check environment
|
|
33
|
+
return getCIToken();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Handle the download command.
|
|
37
|
+
*/
|
|
38
|
+
async function handleDownload(jobId, options) {
|
|
39
|
+
// Get auth token
|
|
40
|
+
const token = await getAuthToken();
|
|
41
|
+
if (!token) {
|
|
42
|
+
console.error(chalk.red("Error: Not authenticated."));
|
|
43
|
+
console.error("Run `vertaa login` to authenticate or set VERTAAUX_TOKEN environment variable.");
|
|
44
|
+
process.exit(ExitCode.ERROR);
|
|
45
|
+
}
|
|
46
|
+
// Load config for API base (supports --config global option)
|
|
47
|
+
const config = await resolveConfig(options.configPath);
|
|
48
|
+
const apiBase = resolveApiBase(options.base);
|
|
49
|
+
const outputDir = options.output || DEFAULT_OUTPUT_DIR;
|
|
50
|
+
const spinner = ora(`Downloading audit ${jobId}...`).start();
|
|
51
|
+
try {
|
|
52
|
+
// Build request URL
|
|
53
|
+
let url = `${apiBase}/sync/download/${jobId}`;
|
|
54
|
+
if (options.baseline) {
|
|
55
|
+
url += "?include_baseline=true";
|
|
56
|
+
}
|
|
57
|
+
// Make API request
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
method: "GET",
|
|
60
|
+
headers: {
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
"X-API-Key": token,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
67
|
+
throw new Error(error.error?.message || `HTTP ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
const result = (await response.json());
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
throw new Error(result.error?.message || "Download failed");
|
|
72
|
+
}
|
|
73
|
+
// Create output directory
|
|
74
|
+
const resolvedOutputDir = path.resolve(process.cwd(), outputDir);
|
|
75
|
+
if (!fs.existsSync(resolvedOutputDir)) {
|
|
76
|
+
fs.mkdirSync(resolvedOutputDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
// Save audit results
|
|
79
|
+
if (result.audit) {
|
|
80
|
+
const auditPath = path.join(resolvedOutputDir, `audit-${jobId}.json`);
|
|
81
|
+
// Check for existing file
|
|
82
|
+
if (fs.existsSync(auditPath) && !options.force) {
|
|
83
|
+
if (isInteractive()) {
|
|
84
|
+
const overwrite = await confirmAction(`File ${auditPath} already exists. Overwrite?`, false);
|
|
85
|
+
if (!overwrite) {
|
|
86
|
+
spinner.info("Skipping audit results (file exists).");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
fs.writeFileSync(auditPath, JSON.stringify(result.audit, null, 2), "utf-8");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
spinner.warn(`Skipping ${auditPath} (already exists, use --force to overwrite)`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
fs.writeFileSync(auditPath, JSON.stringify(result.audit, null, 2), "utf-8");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Save baseline if included
|
|
101
|
+
if (options.baseline && result.baseline) {
|
|
102
|
+
const baselinePath = path.join(resolvedOutputDir, "baseline.json");
|
|
103
|
+
// Check for existing baseline
|
|
104
|
+
if (fs.existsSync(baselinePath) && !options.force) {
|
|
105
|
+
if (isInteractive()) {
|
|
106
|
+
const overwrite = await confirmAction(`Baseline file already exists. Overwrite?`, false);
|
|
107
|
+
if (!overwrite) {
|
|
108
|
+
spinner.info("Skipping baseline (file exists).");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await saveBaseline(result.baseline, baselinePath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
spinner.warn(`Skipping baseline (already exists, use --force to overwrite)`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await saveBaseline(result.baseline, baselinePath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Save artifacts if included
|
|
123
|
+
if (result.artifacts) {
|
|
124
|
+
const artifactsDir = path.join(resolvedOutputDir, "artifacts", jobId);
|
|
125
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
126
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
for (const [filename, content] of Object.entries(result.artifacts)) {
|
|
129
|
+
let filePath;
|
|
130
|
+
try {
|
|
131
|
+
filePath = assertPathContainment(filename, artifactsDir);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
console.error(`Security: Rejected artifact "${filename}" -- path traversal outside output directory.`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const ext = path.extname(filename).toLowerCase();
|
|
138
|
+
const isBinary = [".png", ".jpg", ".jpeg", ".gif", ".zip"].includes(ext);
|
|
139
|
+
if (isBinary) {
|
|
140
|
+
fs.writeFileSync(filePath, Buffer.from(content, "base64"));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
spinner.succeed("Download complete!");
|
|
148
|
+
console.error("");
|
|
149
|
+
console.error(` Job ID: ${result.job_id}`);
|
|
150
|
+
console.error(` Output: ${resolvedOutputDir}`);
|
|
151
|
+
if (result.audit) {
|
|
152
|
+
console.error(` Status: ${result.audit.status}`);
|
|
153
|
+
if (result.audit.url) {
|
|
154
|
+
console.error(` URL: ${result.audit.url}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (options.baseline && result.baseline) {
|
|
158
|
+
console.error(` Baseline: ${result.baseline.issues.length} issues`);
|
|
159
|
+
}
|
|
160
|
+
console.error("");
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
spinner.fail("Download failed");
|
|
164
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
165
|
+
process.exit(ExitCode.ERROR);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Register the download command with the Commander program.
|
|
170
|
+
*/
|
|
171
|
+
export function registerDownloadCommand(program) {
|
|
172
|
+
program
|
|
173
|
+
.command("download <job-id>")
|
|
174
|
+
.description("Download audit results from VertaaUX cloud")
|
|
175
|
+
.option("--baseline", "Also download baseline file")
|
|
176
|
+
.option("-o, --output <path>", "Output directory (default: .vertaaux)")
|
|
177
|
+
.option("-b, --base <url>", "API base URL")
|
|
178
|
+
.option("-f, --force", "Overwrite existing files without prompting")
|
|
179
|
+
.action(async (jobId, options, command) => {
|
|
180
|
+
const globalOpts = command.optsWithGlobals();
|
|
181
|
+
await handleDownload(jobId, { ...options, configPath: globalOpts.config });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explain command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shows full evidence bundle for a specific finding:
|
|
5
|
+
* - Description
|
|
6
|
+
* - Selector
|
|
7
|
+
* - WCAG reference
|
|
8
|
+
* - Recommendation
|
|
9
|
+
* - Related artifacts (screenshots, DOM)
|
|
10
|
+
*
|
|
11
|
+
* Supports loading issues from:
|
|
12
|
+
* - Active job via --job flag
|
|
13
|
+
* - Local JSON file via --file flag
|
|
14
|
+
* - Recent audits from .vertaaux/recent.json
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import type { Issue } from "../baseline/hash.js";
|
|
18
|
+
/**
|
|
19
|
+
* Extended issue type with additional evidence fields.
|
|
20
|
+
*/
|
|
21
|
+
export interface EvidenceIssue extends Issue {
|
|
22
|
+
/** DOM snippet showing the issue */
|
|
23
|
+
html?: string;
|
|
24
|
+
/** Element HTML snippet */
|
|
25
|
+
element?: string;
|
|
26
|
+
/** Screenshot path or URL */
|
|
27
|
+
screenshot?: string;
|
|
28
|
+
/** Help URL for the rule */
|
|
29
|
+
helpUrl?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Format a full evidence bundle for display.
|
|
33
|
+
*
|
|
34
|
+
* @param issue - Issue with evidence
|
|
35
|
+
* @returns Formatted string for terminal display
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatEvidenceBundle(issue: EvidenceIssue): string;
|
|
38
|
+
/**
|
|
39
|
+
* Format evidence as JSON.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatEvidenceJson(issue: EvidenceIssue): string;
|
|
42
|
+
/**
|
|
43
|
+
* Export for reuse in fix-wizard.
|
|
44
|
+
*
|
|
45
|
+
* @param issue - Issue to explain
|
|
46
|
+
* @returns Formatted evidence string
|
|
47
|
+
*/
|
|
48
|
+
export declare function explainIssue(issue: EvidenceIssue): string;
|
|
49
|
+
/**
|
|
50
|
+
* Command options for explain.
|
|
51
|
+
*/
|
|
52
|
+
export interface ExplainCommandOptions {
|
|
53
|
+
job?: string;
|
|
54
|
+
file?: string;
|
|
55
|
+
format?: "json" | "human";
|
|
56
|
+
base?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register the explain command with the Commander program.
|
|
60
|
+
*/
|
|
61
|
+
export declare function registerExplainCommand(program: Command): void;
|
|
62
|
+
//# sourceMappingURL=explain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAyDjE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAgB/D;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgK7D"}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explain command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shows full evidence bundle for a specific finding:
|
|
5
|
+
* - Description
|
|
6
|
+
* - Selector
|
|
7
|
+
* - WCAG reference
|
|
8
|
+
* - Recommendation
|
|
9
|
+
* - Related artifacts (screenshots, DOM)
|
|
10
|
+
*
|
|
11
|
+
* Supports loading issues from:
|
|
12
|
+
* - Active job via --job flag
|
|
13
|
+
* - Local JSON file via --file flag
|
|
14
|
+
* - Recent audits from .vertaaux/recent.json
|
|
15
|
+
*/
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import { ExitCode } from "../utils/exit-codes.js";
|
|
20
|
+
import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
21
|
+
import { resolveConfig } from "../config/loader.js";
|
|
22
|
+
import { writeJsonOutput, writeOutput } from "../output/envelope.js";
|
|
23
|
+
import { resolveCommandFormat } from "../output/formats.js";
|
|
24
|
+
/**
|
|
25
|
+
* Path to recent audits cache.
|
|
26
|
+
*/
|
|
27
|
+
const RECENT_AUDITS_PATH = ".vertaaux/recent.json";
|
|
28
|
+
/**
|
|
29
|
+
* Load recent audits from cache file.
|
|
30
|
+
*/
|
|
31
|
+
function loadRecentAudits() {
|
|
32
|
+
const filePath = path.resolve(process.cwd(), RECENT_AUDITS_PATH);
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
38
|
+
return JSON.parse(content);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Normalize issues from various API response formats.
|
|
46
|
+
*/
|
|
47
|
+
function normalizeIssues(issues) {
|
|
48
|
+
if (Array.isArray(issues))
|
|
49
|
+
return issues;
|
|
50
|
+
if (issues && typeof issues === "object") {
|
|
51
|
+
const values = Object.values(issues);
|
|
52
|
+
return values.flatMap((value) => Array.isArray(value) ? value : []);
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get display-friendly rule ID from issue.
|
|
58
|
+
*/
|
|
59
|
+
function getRuleId(issue) {
|
|
60
|
+
return issue.ruleId || issue.rule_id || issue.id || "unknown";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get severity label with appropriate color.
|
|
64
|
+
*/
|
|
65
|
+
function coloredSeverity(severity) {
|
|
66
|
+
const sev = (severity || "info").toLowerCase();
|
|
67
|
+
switch (sev) {
|
|
68
|
+
case "critical":
|
|
69
|
+
case "error":
|
|
70
|
+
return chalk.red.bold(sev.toUpperCase());
|
|
71
|
+
case "serious":
|
|
72
|
+
case "warning":
|
|
73
|
+
return chalk.yellow.bold(sev.toUpperCase());
|
|
74
|
+
case "moderate":
|
|
75
|
+
case "minor":
|
|
76
|
+
return chalk.cyan(sev.toUpperCase());
|
|
77
|
+
default:
|
|
78
|
+
return chalk.dim(sev.toUpperCase());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Format a full evidence bundle for display.
|
|
83
|
+
*
|
|
84
|
+
* @param issue - Issue with evidence
|
|
85
|
+
* @returns Formatted string for terminal display
|
|
86
|
+
*/
|
|
87
|
+
export function formatEvidenceBundle(issue) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
// Header
|
|
90
|
+
const ruleId = getRuleId(issue);
|
|
91
|
+
const severity = coloredSeverity(issue.severity);
|
|
92
|
+
lines.push(chalk.bold(`ISSUE: ${ruleId} (${severity})`));
|
|
93
|
+
lines.push("");
|
|
94
|
+
// Description
|
|
95
|
+
lines.push(chalk.cyan.bold("DESCRIPTION"));
|
|
96
|
+
lines.push(issue.description || issue.title || "No description available");
|
|
97
|
+
lines.push("");
|
|
98
|
+
// Selector
|
|
99
|
+
if (issue.selector) {
|
|
100
|
+
lines.push(chalk.cyan.bold("SELECTOR"));
|
|
101
|
+
lines.push(chalk.gray(issue.selector));
|
|
102
|
+
lines.push("");
|
|
103
|
+
}
|
|
104
|
+
// WCAG Reference
|
|
105
|
+
if (issue.wcag_reference) {
|
|
106
|
+
lines.push(chalk.cyan.bold("WCAG REFERENCE"));
|
|
107
|
+
lines.push(issue.wcag_reference);
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
// Recommendation
|
|
111
|
+
const recommendation = issue.recommendation || issue.recommended_fix;
|
|
112
|
+
if (recommendation) {
|
|
113
|
+
lines.push(chalk.cyan.bold("RECOMMENDATION"));
|
|
114
|
+
lines.push(recommendation);
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
// Evidence section
|
|
118
|
+
const hasEvidence = issue.screenshot || issue.html || issue.element || issue.helpUrl;
|
|
119
|
+
if (hasEvidence) {
|
|
120
|
+
lines.push(chalk.cyan.bold("EVIDENCE"));
|
|
121
|
+
if (issue.screenshot) {
|
|
122
|
+
lines.push(`- Screenshot: ${chalk.underline(issue.screenshot)}`);
|
|
123
|
+
}
|
|
124
|
+
if (issue.html || issue.element) {
|
|
125
|
+
const snippet = issue.html || issue.element || "";
|
|
126
|
+
lines.push(`- DOM Snapshot: ${chalk.dim(truncate(snippet, 100))}`);
|
|
127
|
+
}
|
|
128
|
+
if (issue.helpUrl) {
|
|
129
|
+
lines.push(`- Help: ${chalk.underline(issue.helpUrl)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Format evidence as JSON.
|
|
136
|
+
*/
|
|
137
|
+
export function formatEvidenceJson(issue) {
|
|
138
|
+
const output = {
|
|
139
|
+
ruleId: getRuleId(issue),
|
|
140
|
+
severity: issue.severity,
|
|
141
|
+
category: issue.category,
|
|
142
|
+
description: issue.description || issue.title,
|
|
143
|
+
selector: issue.selector,
|
|
144
|
+
wcagReference: issue.wcag_reference,
|
|
145
|
+
recommendation: issue.recommendation || issue.recommended_fix,
|
|
146
|
+
evidence: {
|
|
147
|
+
screenshot: issue.screenshot,
|
|
148
|
+
html: issue.html || issue.element,
|
|
149
|
+
helpUrl: issue.helpUrl,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
return JSON.stringify(output, null, 2);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Truncate string with ellipsis.
|
|
156
|
+
*/
|
|
157
|
+
function truncate(str, maxLength) {
|
|
158
|
+
if (str.length <= maxLength)
|
|
159
|
+
return str;
|
|
160
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Export for reuse in fix-wizard.
|
|
164
|
+
*
|
|
165
|
+
* @param issue - Issue to explain
|
|
166
|
+
* @returns Formatted evidence string
|
|
167
|
+
*/
|
|
168
|
+
export function explainIssue(issue) {
|
|
169
|
+
return formatEvidenceBundle(issue);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Register the explain command with the Commander program.
|
|
173
|
+
*/
|
|
174
|
+
export function registerExplainCommand(program) {
|
|
175
|
+
program
|
|
176
|
+
.command("explain <finding-id>")
|
|
177
|
+
.description("Show full evidence bundle for a finding")
|
|
178
|
+
.option("--job <job-id>", "Job ID containing the finding")
|
|
179
|
+
.option("--file <path>", "Load findings from local JSON file")
|
|
180
|
+
.option("-f, --format <format>", "Output format: json | human", "human")
|
|
181
|
+
.action(async (findingId, options, command) => {
|
|
182
|
+
try {
|
|
183
|
+
// Load config (supports --config global option)
|
|
184
|
+
const globalOpts = command.optsWithGlobals();
|
|
185
|
+
const config = await resolveConfig(globalOpts.config);
|
|
186
|
+
// Validate format using per-command registry
|
|
187
|
+
const machineMode = globalOpts.machine || false;
|
|
188
|
+
const format = resolveCommandFormat("explain", options.format, machineMode);
|
|
189
|
+
let issue = null;
|
|
190
|
+
if (options.file) {
|
|
191
|
+
// Load from local file
|
|
192
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
193
|
+
if (!fs.existsSync(filePath)) {
|
|
194
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
195
|
+
process.exit(ExitCode.ERROR);
|
|
196
|
+
}
|
|
197
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
198
|
+
const data = JSON.parse(content);
|
|
199
|
+
// Handle different file structures
|
|
200
|
+
let issues;
|
|
201
|
+
if (Array.isArray(data)) {
|
|
202
|
+
issues = data;
|
|
203
|
+
}
|
|
204
|
+
else if (data.issues) {
|
|
205
|
+
issues = normalizeIssues(data.issues);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// Single issue object
|
|
209
|
+
if (data.id === findingId || data.ruleId === findingId) {
|
|
210
|
+
issue = data;
|
|
211
|
+
}
|
|
212
|
+
issues = [];
|
|
213
|
+
}
|
|
214
|
+
if (!issue) {
|
|
215
|
+
issue =
|
|
216
|
+
issues.find((i) => i.id === findingId ||
|
|
217
|
+
getRuleId(i) === findingId ||
|
|
218
|
+
i.id?.startsWith(findingId) ||
|
|
219
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (options.job) {
|
|
223
|
+
// Fetch from API
|
|
224
|
+
const base = resolveApiBase(options.base);
|
|
225
|
+
const apiKey = getApiKey(config.apiKey);
|
|
226
|
+
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
227
|
+
const issues = normalizeIssues(result.issues);
|
|
228
|
+
issue =
|
|
229
|
+
issues.find((i) => i.id === findingId ||
|
|
230
|
+
getRuleId(i) === findingId ||
|
|
231
|
+
i.id?.startsWith(findingId) ||
|
|
232
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Try recent audits
|
|
236
|
+
const recent = loadRecentAudits();
|
|
237
|
+
if (recent.length === 0) {
|
|
238
|
+
console.error("Error: No recent audits found. Provide --job or --file option.");
|
|
239
|
+
process.exit(ExitCode.ERROR);
|
|
240
|
+
}
|
|
241
|
+
// Try each recent audit
|
|
242
|
+
const base = resolveApiBase(options.base);
|
|
243
|
+
const apiKey = getApiKey(config.apiKey);
|
|
244
|
+
for (const audit of recent) {
|
|
245
|
+
try {
|
|
246
|
+
const result = await apiRequest(base, `/audit/${audit.jobId}`, { method: "GET" }, apiKey);
|
|
247
|
+
const issues = normalizeIssues(result.issues);
|
|
248
|
+
issue =
|
|
249
|
+
issues.find((i) => i.id === findingId ||
|
|
250
|
+
getRuleId(i) === findingId ||
|
|
251
|
+
i.id?.startsWith(findingId) ||
|
|
252
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
253
|
+
if (issue)
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Try next audit
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!issue) {
|
|
263
|
+
console.error(`Error: Finding "${findingId}" not found.`);
|
|
264
|
+
if (options.job) {
|
|
265
|
+
console.error(`Looked in job: ${options.job}`);
|
|
266
|
+
}
|
|
267
|
+
else if (options.file) {
|
|
268
|
+
console.error(`Looked in file: ${options.file}`);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.error("Try specifying --job or --file to narrow the search.");
|
|
272
|
+
}
|
|
273
|
+
process.exit(ExitCode.ERROR);
|
|
274
|
+
}
|
|
275
|
+
// Output
|
|
276
|
+
if (format === "json") {
|
|
277
|
+
const evidenceData = {
|
|
278
|
+
ruleId: getRuleId(issue),
|
|
279
|
+
severity: issue.severity,
|
|
280
|
+
category: issue.category,
|
|
281
|
+
description: issue.description || issue.title,
|
|
282
|
+
selector: issue.selector,
|
|
283
|
+
wcagReference: issue.wcag_reference,
|
|
284
|
+
recommendation: issue.recommendation || issue.recommended_fix,
|
|
285
|
+
evidence: {
|
|
286
|
+
screenshot: issue.screenshot,
|
|
287
|
+
html: issue.html || issue.element,
|
|
288
|
+
helpUrl: issue.helpUrl,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
writeJsonOutput(evidenceData, "explain");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
writeOutput(formatEvidenceBundle(issue));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
299
|
+
process.exit(ExitCode.ERROR);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Creates project configuration with guided wizard or defaults.
|
|
5
|
+
* Generates CI templates for popular platforms.
|
|
6
|
+
*/
|
|
7
|
+
import type { Command } from "commander";
|
|
8
|
+
/**
|
|
9
|
+
* Register the init command with the Commander program.
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerInitCommand(program: Command): void;
|
|
12
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6NzC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgC1D"}
|