@vertaaux/cli 0.3.2 → 0.4.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 +8 -1
- package/dist/auth/ci-token.d.ts +6 -0
- package/dist/auth/ci-token.d.ts.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +216 -157
- package/dist/commands/explain.js +2 -2
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +8 -6
- package/dist/output/factory.d.ts +7 -0
- package/dist/output/factory.d.ts.map +1 -1
- package/dist/output/human.d.ts +7 -0
- package/dist/output/human.d.ts.map +1 -1
- package/dist/output/human.js +14 -0
- package/dist/ui/banner.d.ts +3 -3
- package/dist/ui/banner.d.ts.map +1 -1
- package/dist/ui/banner.js +32 -42
- package/dist/utils/local-capture.d.ts +25 -0
- package/dist/utils/local-capture.d.ts.map +1 -0
- package/dist/utils/local-capture.js +57 -0
- package/dist/utils/url-classify.d.ts +18 -0
- package/dist/utils/url-classify.d.ts.map +1 -0
- package/dist/utils/url-classify.js +85 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ The `--machine` global flag enables strict machine-readable mode:
|
|
|
147
147
|
```json
|
|
148
148
|
{
|
|
149
149
|
"meta": {
|
|
150
|
-
"version": "0.
|
|
150
|
+
"version": "0.4.0",
|
|
151
151
|
"timestamp": "2026-02-08T12:00:00.000Z",
|
|
152
152
|
"command": "audit",
|
|
153
153
|
"args": ["https://example.com", "--format", "json"]
|
|
@@ -210,6 +210,10 @@ These options work with any command:
|
|
|
210
210
|
| `-q, --quiet` | Suppress banner and non-essential output |
|
|
211
211
|
| `--no-banner` | Hide the V-mark banner |
|
|
212
212
|
| `--machine` | Strict machine-readable output mode |
|
|
213
|
+
| `--color` | Force colored output |
|
|
214
|
+
| `--no-color` | Disable colored output |
|
|
215
|
+
| `--dashboard` | Force live dashboard during audit --wait |
|
|
216
|
+
| `--no-dashboard` | Disable live dashboard (use spinner instead) |
|
|
213
217
|
| `--dry-run` | Show what would happen without executing |
|
|
214
218
|
| `-y, --yes` | Auto-confirm all interactive prompts |
|
|
215
219
|
| `--verbose` | Expand output with additional details |
|
|
@@ -338,6 +342,9 @@ JSON envelope output automatically filters CLI arguments containing API keys or
|
|
|
338
342
|
| `VERTAAUX_API_KEY` | API authentication key |
|
|
339
343
|
| `VERTAAUX_TOKEN` | Alternative auth token (checked first) |
|
|
340
344
|
| `VERTAAUX_API_BASE` | API base URL override |
|
|
345
|
+
| `VERTAAUX_AUTH_BASE` | Auth endpoint override (default: `https://vertaaux.ai`) |
|
|
346
|
+
| `VERTAAUX_LOG_LEVEL` | Log verbosity: `debug\|info\|warn\|error` (default: `info`) |
|
|
347
|
+
| `VERTAAUX_LOG_JSON` | Structured JSON logs (default: `false`) |
|
|
341
348
|
| `NO_COLOR` | Disable colored output |
|
|
342
349
|
| `FORCE_COLOR` | Force colored output |
|
|
343
350
|
|
package/dist/auth/ci-token.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ interface TokenValidationResponse {
|
|
|
20
20
|
valid: boolean;
|
|
21
21
|
/** User or organization ID */
|
|
22
22
|
user_id?: string;
|
|
23
|
+
/** User display name */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** User email */
|
|
26
|
+
email?: string;
|
|
27
|
+
/** Subscription tier */
|
|
28
|
+
tier?: string;
|
|
23
29
|
/** Organization name */
|
|
24
30
|
organization?: string;
|
|
25
31
|
/** Scopes granted to token */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ci-token.d.ts","sourceRoot":"","sources":["../../src/auth/ci-token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAQ1C;AAED;;GAEG;AACH,UAAU,uBAAuB;IAC/B,6BAA6B;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAkBzC"}
|
|
1
|
+
{"version":3,"file":"ci-token.d.ts","sourceRoot":"","sources":["../../src/auth/ci-token.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAQ1C;AAED;;GAEG;AACH,UAAU,uBAAuB;IAC/B,6BAA6B;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,MAAyB,GACjC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAkBzC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2FpC,MAAM,WAAW,mBAAmB;IAElC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IAG5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAGhB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IAGvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA8/BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsT3D"}
|
package/dist/commands/audit.js
CHANGED
|
@@ -26,6 +26,8 @@ import { loadPolicy, resolveBranchPolicy, } from "../policy/index.js";
|
|
|
26
26
|
import { detectMonorepo, getAuditableApps, generateMatrixConfig, aggregateResults, formatAggregatedResults, } from "../monorepo/index.js";
|
|
27
27
|
import { createLogger } from "../utils/logger.js";
|
|
28
28
|
import { validateBranchName, assertPathContainment } from "../utils/sanitize.js";
|
|
29
|
+
import { isLocalUrl } from "../utils/url-classify.js";
|
|
30
|
+
import { captureLocalPage } from "../utils/local-capture.js";
|
|
29
31
|
import chalk from "chalk";
|
|
30
32
|
import semver from "semver";
|
|
31
33
|
// Artifact directory
|
|
@@ -356,6 +358,178 @@ function saveArtifactBundle(jobId, result, issues, exitCode, quiet) {
|
|
|
356
358
|
}
|
|
357
359
|
return artifactsPath;
|
|
358
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
|
+
}
|
|
359
533
|
/**
|
|
360
534
|
* Execute the audit command.
|
|
361
535
|
*/
|
|
@@ -413,39 +587,47 @@ async function executeAudit(targetUrl, options, config) {
|
|
|
413
587
|
try {
|
|
414
588
|
// Start spinner (dashboard renders on first update)
|
|
415
589
|
spinner?.start();
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
}
|
|
421
625
|
// If not waiting, just output the job info
|
|
422
626
|
if (!wait) {
|
|
423
627
|
spinner?.stop();
|
|
424
628
|
renderer?.dispose();
|
|
425
629
|
keyboard?.dispose();
|
|
426
|
-
|
|
427
|
-
if (options.output) {
|
|
428
|
-
const output = JSON.stringify(createEnvelope(created, "audit"), null, 2);
|
|
429
|
-
const filePath = writeOutputToFile(output, options.output);
|
|
430
|
-
if (filePath && !quiet) {
|
|
431
|
-
console.error(`Report written to: ${filePath}`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
writeJsonOutput(created, "audit");
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
else {
|
|
439
|
-
const output = formatter.formatResult(created);
|
|
440
|
-
const defaultPath = getDefaultOutputPath(format);
|
|
441
|
-
const filePath = writeOutputToFile(output, options.output, defaultPath);
|
|
442
|
-
if (filePath && !quiet) {
|
|
443
|
-
console.error(`Report written to: ${filePath}`);
|
|
444
|
-
}
|
|
445
|
-
else if (!filePath) {
|
|
446
|
-
writeStdout(output);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
630
|
+
outputFireAndForget(created, format, formatter, options, quiet);
|
|
449
631
|
return;
|
|
450
632
|
}
|
|
451
633
|
// Determine if the server returned results synchronously (status 200)
|
|
@@ -526,48 +708,7 @@ async function executeAudit(targetUrl, options, config) {
|
|
|
526
708
|
issues,
|
|
527
709
|
};
|
|
528
710
|
// Load and apply policy (CICD-17)
|
|
529
|
-
|
|
530
|
-
let policyPath = null;
|
|
531
|
-
try {
|
|
532
|
-
const policyResult = await loadPolicy(options.policy ? path.dirname(options.policy) : undefined);
|
|
533
|
-
if (options.policy) {
|
|
534
|
-
// User specified a policy file, load it specifically
|
|
535
|
-
const { loadPolicyFile } = await import("../policy/index.js");
|
|
536
|
-
resolvedPolicy = await loadPolicyFile(options.policy);
|
|
537
|
-
policyPath = options.policy;
|
|
538
|
-
}
|
|
539
|
-
else if (policyResult.path) {
|
|
540
|
-
resolvedPolicy = policyResult.policy;
|
|
541
|
-
policyPath = policyResult.path;
|
|
542
|
-
}
|
|
543
|
-
// If policy found, resolve branch-specific overrides
|
|
544
|
-
if (resolvedPolicy) {
|
|
545
|
-
const currentBranch = detectCurrentBranch();
|
|
546
|
-
if (currentBranch) {
|
|
547
|
-
resolvedPolicy = resolveBranchPolicy(resolvedPolicy, currentBranch);
|
|
548
|
-
}
|
|
549
|
-
if (!quiet && policyPath) {
|
|
550
|
-
console.error(chalk.dim(`Using policy: ${policyPath}`));
|
|
551
|
-
}
|
|
552
|
-
// Check required CLI version
|
|
553
|
-
if (resolvedPolicy.required_version) {
|
|
554
|
-
if (!semver.satisfies(CLI_VERSION, resolvedPolicy.required_version)) {
|
|
555
|
-
console.error(chalk.red(`Policy requires CLI version ${resolvedPolicy.required_version}, ` +
|
|
556
|
-
`but current version is ${CLI_VERSION}`));
|
|
557
|
-
process.exit(ExitCode.ERROR);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
// Policy loading errors should be reported but not fatal if no explicit policy specified
|
|
564
|
-
if (options.policy) {
|
|
565
|
-
throw error; // Re-throw if user explicitly specified a policy
|
|
566
|
-
}
|
|
567
|
-
if (!quiet) {
|
|
568
|
-
console.error(chalk.yellow(`Warning: Failed to load policy: ${error instanceof Error ? error.message : String(error)}`));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
711
|
+
await loadAndResolvePolicy(options, quiet);
|
|
571
712
|
// Build quality gate config and evaluate
|
|
572
713
|
const gateConfig = buildQualityGateConfig(config, options);
|
|
573
714
|
// Load baseline if provided
|
|
@@ -588,97 +729,14 @@ async function executeAudit(targetUrl, options, config) {
|
|
|
588
729
|
// Use quality gate exit code
|
|
589
730
|
const exitCode = gateResult.exitCode;
|
|
590
731
|
// Format and output results
|
|
591
|
-
|
|
592
|
-
human: {
|
|
593
|
-
groupBy,
|
|
594
|
-
showScores: true,
|
|
595
|
-
showSummary: true,
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
if (format === "json") {
|
|
599
|
-
if (options.output) {
|
|
600
|
-
const jsonStr = JSON.stringify(createEnvelope(filteredResult, "audit"), null, 2);
|
|
601
|
-
const filePath = writeOutputToFile(jsonStr, options.output);
|
|
602
|
-
if (filePath && !quiet) {
|
|
603
|
-
console.error(`Report written to: ${filePath}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
writeJsonOutput(filteredResult, "audit");
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
const output = formatter.formatResult(filteredResult, formatOptions);
|
|
612
|
-
const defaultPath = getDefaultOutputPath(format);
|
|
613
|
-
const filePath = writeOutputToFile(output, options.output, defaultPath);
|
|
614
|
-
if (filePath && !quiet) {
|
|
615
|
-
console.error(`Report written to: ${filePath}`);
|
|
616
|
-
}
|
|
617
|
-
else if (!filePath) {
|
|
618
|
-
writeStdout(output);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
732
|
+
outputFormattedResults(filteredResult, format, formatter, groupBy, options, quiet);
|
|
621
733
|
// Inline AI explanation (--explain flag, PROG-04)
|
|
622
734
|
if (options.explain && issues.length > 0) {
|
|
623
|
-
|
|
624
|
-
const explainBase = resolveApiBase(options.base);
|
|
625
|
-
const explainKey = getApiKey(config.apiKey);
|
|
626
|
-
const explainIssues = issues.map((i) => ({
|
|
627
|
-
id: i.id || null,
|
|
628
|
-
title: i.title || i.description || null,
|
|
629
|
-
description: i.description || null,
|
|
630
|
-
severity: i.severity || null,
|
|
631
|
-
category: i.category || null,
|
|
632
|
-
selector: i.selector || null,
|
|
633
|
-
wcag_reference: i.wcag_reference || null,
|
|
634
|
-
recommendation: i.recommendation || i.recommended_fix || null,
|
|
635
|
-
}));
|
|
636
|
-
const explainPayload = {
|
|
637
|
-
job_id: result.job_id || null,
|
|
638
|
-
url: targetUrl || null,
|
|
639
|
-
scores: result.scores || null,
|
|
640
|
-
issues: explainIssues,
|
|
641
|
-
};
|
|
642
|
-
const explainSpinner = createSpinner("Generating AI explanation...");
|
|
643
|
-
const explainResponse = await apiRequest(explainBase, "/cli/ai/explain", { method: "POST", body: { audit: explainPayload } }, explainKey);
|
|
644
|
-
succeedSpinner(explainSpinner, "Explanation ready");
|
|
645
|
-
console.error("");
|
|
646
|
-
console.error(chalk.bold("AI Explanation"));
|
|
647
|
-
console.error(chalk.dim("─".repeat(40)));
|
|
648
|
-
for (const bullet of explainResponse.data.summary) {
|
|
649
|
-
console.error(` ${chalk.cyan("*")} ${bullet}`);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
catch (explainErr) {
|
|
653
|
-
console.error(chalk.dim(`\n(AI explanation unavailable: ${explainErr instanceof Error ? explainErr.message : String(explainErr)})`));
|
|
654
|
-
}
|
|
735
|
+
await runInlineExplanation(issues, result, targetUrl, options, config);
|
|
655
736
|
}
|
|
656
737
|
// Output quality gate result
|
|
657
738
|
if (!quiet) {
|
|
658
|
-
|
|
659
|
-
if (gateResult.bypassed) {
|
|
660
|
-
console.error(chalk.yellow(`Quality gate bypassed: ${gateResult.bypassReason}`));
|
|
661
|
-
}
|
|
662
|
-
else if (gateResult.passed) {
|
|
663
|
-
console.error(chalk.green("Quality gate: PASSED"));
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
console.error(chalk.red("Quality gate: FAILED"));
|
|
667
|
-
for (const violation of gateResult.violations) {
|
|
668
|
-
console.error(chalk.red(` - ${violation.message}`));
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
// Summary
|
|
672
|
-
console.error("");
|
|
673
|
-
console.error(`New issues: ${gateResult.summary.newIssues.error} errors, ` +
|
|
674
|
-
`${gateResult.summary.newIssues.warning} warnings, ` +
|
|
675
|
-
`${gateResult.summary.newIssues.info} info`);
|
|
676
|
-
if (gateResult.summary.fixedIssues > 0) {
|
|
677
|
-
console.error(chalk.green(`Fixed: ${gateResult.summary.fixedIssues} issues`));
|
|
678
|
-
}
|
|
679
|
-
if (gateResult.summary.existingIssues > 0) {
|
|
680
|
-
console.error(chalk.dim(`Existing (baselined): ${gateResult.summary.existingIssues} issues`));
|
|
681
|
-
}
|
|
739
|
+
outputQualityGateResult(gateResult);
|
|
682
740
|
}
|
|
683
741
|
// Save CI artifact bundle if requested
|
|
684
742
|
if (options.uploadArtifacts && created.job_id) {
|
|
@@ -781,7 +839,8 @@ function countTotalIssues(issues) {
|
|
|
781
839
|
export function registerAuditCommand(program) {
|
|
782
840
|
program
|
|
783
841
|
.command("audit [url]")
|
|
784
|
-
.description("Run UX and accessibility audit"
|
|
842
|
+
.description("Run UX and accessibility audit. Localhost and private URLs are " +
|
|
843
|
+
"captured locally and analyzed via static HTML analysis.")
|
|
785
844
|
.option("-u, --url <url>", "URL to audit")
|
|
786
845
|
.option("--repo <repo>", "GitHub repository to audit (owner/repo)")
|
|
787
846
|
.option("--storybook <url>", "Storybook URL to audit")
|
package/dist/commands/explain.js
CHANGED
|
@@ -278,7 +278,7 @@ async function handleAiMode(options, config, format, verbose) {
|
|
|
278
278
|
let rawIssues = [];
|
|
279
279
|
if (options.job) {
|
|
280
280
|
// Fetch full audit from API
|
|
281
|
-
const base = resolveApiBase(options.base
|
|
281
|
+
const base = resolveApiBase(options.base);
|
|
282
282
|
const apiKey = getApiKey(config.apiKey);
|
|
283
283
|
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
284
284
|
rawIssues = normalizeIssues(result.issues);
|
|
@@ -337,7 +337,7 @@ async function handleAiMode(options, config, format, verbose) {
|
|
|
337
337
|
process.exit(ExitCode.ERROR);
|
|
338
338
|
}
|
|
339
339
|
// Call the LLM explain API
|
|
340
|
-
const base = resolveApiBase(options.base
|
|
340
|
+
const base = resolveApiBase(options.base);
|
|
341
341
|
const apiKey = getApiKey(config.apiKey);
|
|
342
342
|
const spinner = createSpinner("Analyzing findings...");
|
|
343
343
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/commands/login.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgPzC;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+B3D"}
|
package/dist/commands/login.js
CHANGED
|
@@ -167,16 +167,18 @@ async function handleWhoami(options) {
|
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
// Display auth status
|
|
170
|
-
|
|
170
|
+
const displayName = info.name || info.email || info.user_id || "Unknown";
|
|
171
|
+
console.error(chalk.green(`Authenticated as ${chalk.bold(displayName)}`));
|
|
171
172
|
console.error("");
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
if (info.email) {
|
|
174
|
+
console.error(` Email: ${info.email}`);
|
|
175
|
+
}
|
|
176
|
+
if (info.tier) {
|
|
177
|
+
console.error(` Plan: ${info.tier.charAt(0).toUpperCase() + info.tier.slice(1)}`);
|
|
178
|
+
}
|
|
174
179
|
if (info.organization) {
|
|
175
180
|
console.error(` Organization: ${info.organization}`);
|
|
176
181
|
}
|
|
177
|
-
if (info.user_id) {
|
|
178
|
-
console.error(` User ID: ${info.user_id}`);
|
|
179
|
-
}
|
|
180
182
|
if (info.scopes && info.scopes.length > 0) {
|
|
181
183
|
console.error(` Scopes: ${info.scopes.join(", ")}`);
|
|
182
184
|
}
|
package/dist/output/factory.d.ts
CHANGED
|
@@ -22,6 +22,13 @@ export interface AuditResult {
|
|
|
22
22
|
scores?: Record<string, unknown>;
|
|
23
23
|
issues?: unknown;
|
|
24
24
|
error?: string;
|
|
25
|
+
analysis_type?: string;
|
|
26
|
+
metadata?: {
|
|
27
|
+
partial?: boolean;
|
|
28
|
+
source?: string;
|
|
29
|
+
unavailable_checks?: string[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
25
32
|
}
|
|
26
33
|
export interface FormatOptions {
|
|
27
34
|
/** Human output options */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/output/factory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/output/factory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,0BAA0B;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,0BAA0B;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,iCAAiC;IACjC,MAAM,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;CACpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,YAAY,GAAG,MAAe,GAAG,SAAS,CA4B9E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,WAAW,EACnB,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAIR;AAGD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/output/human.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface AuditResult {
|
|
|
17
17
|
scores?: Record<string, unknown>;
|
|
18
18
|
issues?: unknown;
|
|
19
19
|
error?: string;
|
|
20
|
+
analysis_type?: string;
|
|
21
|
+
metadata?: {
|
|
22
|
+
partial?: boolean;
|
|
23
|
+
source?: string;
|
|
24
|
+
unavailable_checks?: string[];
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
20
27
|
}
|
|
21
28
|
export interface FormatHumanOptions {
|
|
22
29
|
groupBy?: "severity" | "category" | "route";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"human.d.ts","sourceRoot":"","sources":["../../src/output/human.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,EAOL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"human.d.ts","sourceRoot":"","sources":["../../src/output/human.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,EAOL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IAC5C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAuND;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CA4DR"}
|
package/dist/output/human.js
CHANGED
|
@@ -198,5 +198,19 @@ export function formatAuditHuman(result, options = {}) {
|
|
|
198
198
|
if (showSummary) {
|
|
199
199
|
sections.push(formatSummary(issues, result.scores));
|
|
200
200
|
}
|
|
201
|
+
// Partial result annotation for local/static analysis audits
|
|
202
|
+
if (result.metadata?.partial || result.analysis_type === "static_html") {
|
|
203
|
+
const checks = result.metadata?.unavailable_checks ?? [];
|
|
204
|
+
const lines = [];
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push(text.dim("─".repeat(Math.min(getTerminalWidth(), 80))));
|
|
207
|
+
lines.push(text.dim("This audit ran in static analysis mode (local URL)."));
|
|
208
|
+
if (checks.length > 0) {
|
|
209
|
+
lines.push(text.dim("Unavailable checks: " +
|
|
210
|
+
checks.map((c) => c.replace(/_/g, " ")).join(", ")));
|
|
211
|
+
}
|
|
212
|
+
lines.push(text.dim("Deploy to a public URL for a full browser-based audit."));
|
|
213
|
+
sections.push(lines.join("\n"));
|
|
214
|
+
}
|
|
201
215
|
return sections.join("\n") + "\n";
|
|
202
216
|
}
|
package/dist/ui/banner.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Arrow-mark ASCII banner for VertaaUX CLI.
|
|
3
3
|
*
|
|
4
|
-
* Displays a
|
|
4
|
+
* Displays a compact arrow wedge in teal/mint gradient.
|
|
5
5
|
* Can be suppressed via --quiet, --no-banner, or non-TTY environments.
|
|
6
6
|
*/
|
|
7
7
|
export interface BannerOptions {
|
|
@@ -13,7 +13,7 @@ export interface BannerOptions {
|
|
|
13
13
|
noBanner?: boolean;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Show the
|
|
16
|
+
* Show the arrow-mark banner with version and working directory.
|
|
17
17
|
*
|
|
18
18
|
* Banner is suppressed if:
|
|
19
19
|
* - --quiet or -q flag is set
|
package/dist/ui/banner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/ui/banner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/ui/banner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAqBH,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CA+BvD;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAOnC"}
|
package/dist/ui/banner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Arrow-mark ASCII banner for VertaaUX CLI.
|
|
3
3
|
*
|
|
4
|
-
* Displays a
|
|
4
|
+
* Displays a compact arrow wedge in teal/mint gradient.
|
|
5
5
|
* Can be suppressed via --quiet, --no-banner, or non-TTY environments.
|
|
6
6
|
*/
|
|
7
7
|
import chalk from "chalk";
|
|
@@ -9,35 +9,18 @@ import { createRequire } from "module";
|
|
|
9
9
|
import { isTTY, shouldUseColor } from "../utils/detect-env.js";
|
|
10
10
|
// For JSON imports in ESM
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
-
//
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"#51CCCE", // Cyan-ish teal
|
|
12
|
+
// Arrow-mark ASCII art (compact 5-line wedge matching brand)
|
|
13
|
+
const ARROW_MARK = [
|
|
14
|
+
" ██████████▀",
|
|
15
|
+
" ████████▀",
|
|
16
|
+
" ▀██████████▀",
|
|
17
|
+
" ▀██████▀",
|
|
18
|
+
" ▀██▀",
|
|
20
19
|
];
|
|
20
|
+
const TEAL = "#5EE4C4";
|
|
21
|
+
const MINT = "#90EECE";
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
function applyGradient(text, colors) {
|
|
25
|
-
const colorCount = colors.length;
|
|
26
|
-
let result = "";
|
|
27
|
-
let colorIndex = 0;
|
|
28
|
-
for (const char of text) {
|
|
29
|
-
if (char !== " ") {
|
|
30
|
-
result += chalk.hex(colors[colorIndex % colorCount])(char);
|
|
31
|
-
colorIndex++;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
result += char;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Show the V-mark banner with version and working directory.
|
|
23
|
+
* Show the arrow-mark banner with version and working directory.
|
|
41
24
|
*
|
|
42
25
|
* Banner is suppressed if:
|
|
43
26
|
* - --quiet or -q flag is set
|
|
@@ -54,20 +37,27 @@ export function showBanner(options) {
|
|
|
54
37
|
}
|
|
55
38
|
const useColor = shouldUseColor();
|
|
56
39
|
const cwd = process.cwd();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`${
|
|
61
|
-
`${
|
|
62
|
-
`${
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
`${V_MARK[0]} VertaaUX v${version}`,
|
|
66
|
-
`${V_MARK[1]} ${cwd}`,
|
|
67
|
-
`${V_MARK[2]}`,
|
|
40
|
+
if (useColor) {
|
|
41
|
+
const logo = ARROW_MARK.map((line) => chalk.hex(TEAL)(line));
|
|
42
|
+
const lines = [
|
|
43
|
+
`${logo[0]} ${chalk.hex(MINT).bold("VertaaUX")} v${version}`,
|
|
44
|
+
`${logo[1]} ${chalk.dim(cwd)}`,
|
|
45
|
+
`${logo[2]}`,
|
|
46
|
+
`${logo[3]}`,
|
|
47
|
+
`${logo[4]}`,
|
|
68
48
|
];
|
|
69
|
-
|
|
70
|
-
|
|
49
|
+
process.stderr.write(lines.join("\n") + "\n\n");
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const lines = [
|
|
53
|
+
`${ARROW_MARK[0]} VertaaUX v${version}`,
|
|
54
|
+
`${ARROW_MARK[1]} ${cwd}`,
|
|
55
|
+
`${ARROW_MARK[2]}`,
|
|
56
|
+
`${ARROW_MARK[3]}`,
|
|
57
|
+
`${ARROW_MARK[4]}`,
|
|
58
|
+
];
|
|
59
|
+
process.stderr.write(lines.join("\n") + "\n\n");
|
|
60
|
+
}
|
|
71
61
|
}
|
|
72
62
|
/**
|
|
73
63
|
* Get version from package.json.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local page capture for localhost/private URL auditing.
|
|
3
|
+
*
|
|
4
|
+
* Fetches HTML content from a local URL using Node's native fetch,
|
|
5
|
+
* so the CLI can send pre-captured content to the cloud API for analysis.
|
|
6
|
+
*/
|
|
7
|
+
export interface CapturedPage {
|
|
8
|
+
html: string;
|
|
9
|
+
url: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
headers: Record<string, string>;
|
|
12
|
+
fetchTimeMs: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Fetch a local/private URL and capture its rendered HTML.
|
|
16
|
+
*
|
|
17
|
+
* @param url - The local URL to capture (e.g., http://localhost:3000/)
|
|
18
|
+
* @param options - Optional configuration
|
|
19
|
+
* @returns Captured page content and metadata
|
|
20
|
+
* @throws Error with user-friendly message on connection failures
|
|
21
|
+
*/
|
|
22
|
+
export declare function captureLocalPage(url: string, options?: {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}): Promise<CapturedPage>;
|
|
25
|
+
//# sourceMappingURL=local-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-capture.d.ts","sourceRoot":"","sources":["../../src/utils/local-capture.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,YAAY,CAAC,CAmDvB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local page capture for localhost/private URL auditing.
|
|
3
|
+
*
|
|
4
|
+
* Fetches HTML content from a local URL using Node's native fetch,
|
|
5
|
+
* so the CLI can send pre-captured content to the cloud API for analysis.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Fetch a local/private URL and capture its rendered HTML.
|
|
9
|
+
*
|
|
10
|
+
* @param url - The local URL to capture (e.g., http://localhost:3000/)
|
|
11
|
+
* @param options - Optional configuration
|
|
12
|
+
* @returns Captured page content and metadata
|
|
13
|
+
* @throws Error with user-friendly message on connection failures
|
|
14
|
+
*/
|
|
15
|
+
export async function captureLocalPage(url, options) {
|
|
16
|
+
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
17
|
+
const start = Date.now();
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, {
|
|
22
|
+
signal: controller.signal,
|
|
23
|
+
headers: {
|
|
24
|
+
"User-Agent": "VertaaUX-CLI (local-capture)",
|
|
25
|
+
Accept: "text/html,application/xhtml+xml,*/*",
|
|
26
|
+
},
|
|
27
|
+
redirect: "follow",
|
|
28
|
+
});
|
|
29
|
+
const html = await response.text();
|
|
30
|
+
const headers = {};
|
|
31
|
+
response.headers.forEach((value, key) => {
|
|
32
|
+
headers[key] = value;
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
html,
|
|
36
|
+
url,
|
|
37
|
+
statusCode: response.status,
|
|
38
|
+
headers,
|
|
39
|
+
fetchTimeMs: Date.now() - start,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
44
|
+
throw new Error(`Timed out after ${timeoutMs}ms trying to reach ${url}. ` +
|
|
45
|
+
"Ensure your local server is running and responsive.");
|
|
46
|
+
}
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
if (message.includes("ECONNREFUSED")) {
|
|
49
|
+
throw new Error(`Connection refused at ${url}. ` +
|
|
50
|
+
"Ensure your local development server is running.");
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Failed to capture local page at ${url}: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL classification for local vs. public URLs.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the SSRF guard logic from lib/ssrf.ts on the client side
|
|
5
|
+
* to determine routing strategy (cloud API vs local capture).
|
|
6
|
+
*/
|
|
7
|
+
export type UrlKind = "public" | "localhost" | "private";
|
|
8
|
+
/**
|
|
9
|
+
* Classify a URL as public, localhost, or private-network.
|
|
10
|
+
* Used to decide whether the CLI should capture the page locally.
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyUrl(urlString: string): UrlKind;
|
|
13
|
+
/**
|
|
14
|
+
* Check if a URL points to a local or private address
|
|
15
|
+
* (unreachable from the cloud API).
|
|
16
|
+
*/
|
|
17
|
+
export declare function isLocalUrl(urlString: string): boolean;
|
|
18
|
+
//# sourceMappingURL=url-classify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-classify.d.ts","sourceRoot":"","sources":["../../src/utils/url-classify.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CA2BtD;AA4CD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAGrD"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL classification for local vs. public URLs.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the SSRF guard logic from lib/ssrf.ts on the client side
|
|
5
|
+
* to determine routing strategy (cloud API vs local capture).
|
|
6
|
+
*/
|
|
7
|
+
import net from "net";
|
|
8
|
+
/**
|
|
9
|
+
* Classify a URL as public, localhost, or private-network.
|
|
10
|
+
* Used to decide whether the CLI should capture the page locally.
|
|
11
|
+
*/
|
|
12
|
+
export function classifyUrl(urlString) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(urlString);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return "public"; // Let the API decide for malformed URLs
|
|
19
|
+
}
|
|
20
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
21
|
+
// Localhost variants (URL parser keeps brackets on IPv6)
|
|
22
|
+
if (hostname === "localhost" ||
|
|
23
|
+
hostname === "::1" ||
|
|
24
|
+
hostname === "[::1]" ||
|
|
25
|
+
hostname.endsWith(".localhost")) {
|
|
26
|
+
return "localhost";
|
|
27
|
+
}
|
|
28
|
+
// IP literal check
|
|
29
|
+
if (net.isIP(hostname)) {
|
|
30
|
+
if (isPrivateIp(hostname))
|
|
31
|
+
return "private";
|
|
32
|
+
return "public";
|
|
33
|
+
}
|
|
34
|
+
return "public";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check common private/reserved IPv4 ranges.
|
|
38
|
+
*/
|
|
39
|
+
function isPrivateIp(address) {
|
|
40
|
+
const version = net.isIP(address);
|
|
41
|
+
if (version === 6) {
|
|
42
|
+
const lower = address.toLowerCase();
|
|
43
|
+
return (lower === "::" ||
|
|
44
|
+
lower === "::1" ||
|
|
45
|
+
lower.startsWith("fc") ||
|
|
46
|
+
lower.startsWith("fd") ||
|
|
47
|
+
lower.startsWith("fe8") ||
|
|
48
|
+
lower.startsWith("fe9") ||
|
|
49
|
+
lower.startsWith("fea") ||
|
|
50
|
+
lower.startsWith("feb"));
|
|
51
|
+
}
|
|
52
|
+
if (version !== 4)
|
|
53
|
+
return false;
|
|
54
|
+
const parts = address.split(".").map(Number);
|
|
55
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
|
|
56
|
+
return false;
|
|
57
|
+
const [a, b] = parts;
|
|
58
|
+
// 127.0.0.0/8 — loopback
|
|
59
|
+
if (a === 127)
|
|
60
|
+
return true;
|
|
61
|
+
// 10.0.0.0/8
|
|
62
|
+
if (a === 10)
|
|
63
|
+
return true;
|
|
64
|
+
// 172.16.0.0/12
|
|
65
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
66
|
+
return true;
|
|
67
|
+
// 192.168.0.0/16
|
|
68
|
+
if (a === 192 && b === 168)
|
|
69
|
+
return true;
|
|
70
|
+
// 169.254.0.0/16 — link-local
|
|
71
|
+
if (a === 169 && b === 254)
|
|
72
|
+
return true;
|
|
73
|
+
// 0.0.0.0/8
|
|
74
|
+
if (a === 0)
|
|
75
|
+
return true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a URL points to a local or private address
|
|
80
|
+
* (unreachable from the cloud API).
|
|
81
|
+
*/
|
|
82
|
+
export function isLocalUrl(urlString) {
|
|
83
|
+
const kind = classifyUrl(urlString);
|
|
84
|
+
return kind === "localhost" || kind === "private";
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertaaux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Run automated UX audits, accessibility checks, and performance analysis from the terminal or CI pipelines. Supports policy gating, SARIF output, and multi-page crawling. See https://github.com/PetriLahdelma/vertaa/tree/main/cli#readme for full docs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|