@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/index.js
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* VertaaUX CLI - UX audits, accessibility checks, and CI gating.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Slim entry point: sets up Commander.js program and delegates to command modules.
|
|
6
|
+
* Legacy commands (a11y, scan, status, fix, fix-all, verify) are in commands/.
|
|
7
|
+
* Modern commands are registered via registerXCommand() from commands/*.
|
|
7
8
|
*/
|
|
8
9
|
import dotenv from "dotenv";
|
|
9
10
|
import fs from "fs";
|
|
10
11
|
import path from "path";
|
|
11
12
|
import { fileURLToPath } from "url";
|
|
12
13
|
import { Command } from "commander";
|
|
13
|
-
import { setColorEnabled, shouldUseColor } from "@vertaaux/tui";
|
|
14
|
-
import {
|
|
14
|
+
import { setColorEnabled, shouldUseColor, setPlainMode, renderError, setSymbolTier, detectSymbolTier } from "@vertaaux/tui";
|
|
15
|
+
import { animateBanner, getVersion } from "./ui/banner.js";
|
|
16
|
+
// Modern command registrations
|
|
15
17
|
import { registerAuditCommand } from "./commands/audit.js";
|
|
16
18
|
import { registerBaselineCommand } from "./commands/baseline.js";
|
|
17
19
|
import { registerCommentCommand } from "./commands/comment.js";
|
|
@@ -30,10 +32,45 @@ import { registerPatchReviewCommand } from "./commands/patch-review.js";
|
|
|
30
32
|
import { registerCompareCommand } from "./commands/compare.js";
|
|
31
33
|
import { registerReleaseNotesCommand } from "./commands/release-notes.js";
|
|
32
34
|
import { registerDocCommand } from "./commands/doc.js";
|
|
35
|
+
// Legacy command registrations (backward compat)
|
|
36
|
+
import { registerA11yCommand } from "./commands/a11y.js";
|
|
37
|
+
import { registerScanCommand } from "./commands/scan.js";
|
|
38
|
+
import { registerStatusCommand } from "./commands/status.js";
|
|
39
|
+
import { registerFixCommand } from "./commands/fix.js";
|
|
40
|
+
import { registerFixAllCommand } from "./commands/fix-all.js";
|
|
41
|
+
import { registerVerifyCommand } from "./commands/verify.js";
|
|
33
42
|
import { ExitCode } from "./utils/exit-codes.js";
|
|
34
43
|
import { formatCommanderError } from "./ui/diagnostics.js";
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
44
|
+
import { isInteractive } from "./interactive/prompts.js";
|
|
45
|
+
import { InteractiveApp } from "./app/interactive-app.js";
|
|
46
|
+
import { MenuView } from "./app/menu/menu-view.js";
|
|
47
|
+
// HelpOverlayView is toggled by InteractiveApp's global H key handler
|
|
48
|
+
// import { HelpOverlayView } from "./app/views/help-overlay.js";
|
|
49
|
+
import { CommandRunnerView } from "./app/views/command-runner.js";
|
|
50
|
+
import { recordRecent } from "./app/menu/recent.js";
|
|
51
|
+
// Detect best symbol tier for this terminal (nerd > unicode > ascii)
|
|
52
|
+
setSymbolTier(detectSymbolTier());
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Global error handlers — installed before anything else runs
|
|
55
|
+
// Ensures no user ever sees a raw Node.js stack trace
|
|
56
|
+
// ============================================================================
|
|
57
|
+
process.on("uncaughtException", (err) => {
|
|
58
|
+
process.stderr.write(renderError({
|
|
59
|
+
message: err.message || "An unexpected error occurred",
|
|
60
|
+
suggestion: "vertaa doctor",
|
|
61
|
+
exitCode: ExitCode.ERROR,
|
|
62
|
+
}) + "\n");
|
|
63
|
+
process.exit(ExitCode.ERROR);
|
|
64
|
+
});
|
|
65
|
+
process.on("unhandledRejection", (reason) => {
|
|
66
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
67
|
+
process.stderr.write(renderError({
|
|
68
|
+
message,
|
|
69
|
+
suggestion: "vertaa doctor",
|
|
70
|
+
exitCode: ExitCode.ERROR,
|
|
71
|
+
}) + "\n");
|
|
72
|
+
process.exit(ExitCode.ERROR);
|
|
73
|
+
});
|
|
37
74
|
const __filename = fileURLToPath(import.meta.url);
|
|
38
75
|
const __dirname = path.dirname(__filename);
|
|
39
76
|
// Load environment variables from multiple locations
|
|
@@ -45,8 +82,10 @@ const envCandidates = [
|
|
|
45
82
|
path.resolve(__dirname, "../../.env.local"),
|
|
46
83
|
path.resolve(__dirname, "../../.env"),
|
|
47
84
|
];
|
|
48
|
-
// Silence any dotenv stdout output during loading
|
|
85
|
+
// Silence any dotenv stdout output during loading.
|
|
86
|
+
// eslint-disable-next-line no-console -- intentional: monkey-patching console.log to suppress dotenv banner output from a third-party lib
|
|
49
87
|
const originalLog = console.log;
|
|
88
|
+
// eslint-disable-next-line no-console -- intentional: temporarily replacing console.log to filter dotenv's [dotenv@x.y.z] banners
|
|
50
89
|
console.log = (...args) => {
|
|
51
90
|
const msg = String(args[0] || "");
|
|
52
91
|
if (msg.includes("[dotenv"))
|
|
@@ -58,805 +97,8 @@ for (const candidate of envCandidates) {
|
|
|
58
97
|
dotenv.config({ path: candidate, override: false });
|
|
59
98
|
}
|
|
60
99
|
}
|
|
100
|
+
// eslint-disable-next-line no-console -- intentional: restoring original console.log after dotenv suppression
|
|
61
101
|
console.log = originalLog;
|
|
62
|
-
const DEFAULT_API_BASE = "https://vertaaux.ai/v1";
|
|
63
|
-
// ============================================================================
|
|
64
|
-
// Legacy helper functions (kept for backward compatibility with other commands)
|
|
65
|
-
// ============================================================================
|
|
66
|
-
function parseArgs(args) {
|
|
67
|
-
const positional = [];
|
|
68
|
-
const flags = {};
|
|
69
|
-
for (let i = 0; i < args.length; i++) {
|
|
70
|
-
const arg = args[i];
|
|
71
|
-
if (arg === "--") {
|
|
72
|
-
positional.push(...args.slice(i + 1));
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
if (arg.startsWith("--")) {
|
|
76
|
-
const [rawKey, rawValue] = arg.slice(2).split("=", 2);
|
|
77
|
-
if (rawValue !== undefined) {
|
|
78
|
-
flags[rawKey] = rawValue;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const next = args[i + 1];
|
|
82
|
-
if (next && !next.startsWith("-")) {
|
|
83
|
-
flags[rawKey] = next;
|
|
84
|
-
i += 1;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
flags[rawKey] = true;
|
|
88
|
-
}
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (arg.startsWith("-") && arg.length > 1) {
|
|
92
|
-
const key = arg.slice(1);
|
|
93
|
-
if (key === "u") {
|
|
94
|
-
const next = args[i + 1];
|
|
95
|
-
if (next && !next.startsWith("-")) {
|
|
96
|
-
flags.url = next;
|
|
97
|
-
i += 1;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
flags.url = true;
|
|
101
|
-
}
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (key === "b") {
|
|
105
|
-
const next = args[i + 1];
|
|
106
|
-
if (next && !next.startsWith("-")) {
|
|
107
|
-
flags.base = next;
|
|
108
|
-
i += 1;
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
flags.base = true;
|
|
112
|
-
}
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (key === "f") {
|
|
116
|
-
const next = args[i + 1];
|
|
117
|
-
if (next && !next.startsWith("-")) {
|
|
118
|
-
flags.format = next;
|
|
119
|
-
i += 1;
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
flags.format = true;
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (key === "h") {
|
|
127
|
-
flags.help = true;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (key === "q") {
|
|
131
|
-
flags.quiet = true;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const next = args[i + 1];
|
|
135
|
-
if (next && !next.startsWith("-")) {
|
|
136
|
-
flags[key] = next;
|
|
137
|
-
i += 1;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
flags[key] = true;
|
|
141
|
-
}
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
positional.push(arg);
|
|
145
|
-
}
|
|
146
|
-
return { positional, flags };
|
|
147
|
-
}
|
|
148
|
-
function getString(flags, key) {
|
|
149
|
-
const value = flags[key];
|
|
150
|
-
if (typeof value === "string")
|
|
151
|
-
return value;
|
|
152
|
-
// Fallback: try camelCase version (Commander.js converts --multi-word to multiWord)
|
|
153
|
-
const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
154
|
-
if (camelKey !== key) {
|
|
155
|
-
const camelValue = flags[camelKey];
|
|
156
|
-
if (typeof camelValue === "string")
|
|
157
|
-
return camelValue;
|
|
158
|
-
}
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
function getBool(flags, key) {
|
|
162
|
-
if (flags[key] === true)
|
|
163
|
-
return true;
|
|
164
|
-
// Fallback: try camelCase version (Commander.js converts --multi-word to multiWord)
|
|
165
|
-
const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
166
|
-
if (camelKey !== key)
|
|
167
|
-
return flags[camelKey] === true;
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
function getNumber(flags, key) {
|
|
171
|
-
const raw = getString(flags, key);
|
|
172
|
-
if (raw === undefined)
|
|
173
|
-
return undefined;
|
|
174
|
-
const parsed = Number(raw);
|
|
175
|
-
if (Number.isNaN(parsed)) {
|
|
176
|
-
throw new Error(`Invalid --${key} value: ${raw}`);
|
|
177
|
-
}
|
|
178
|
-
return parsed;
|
|
179
|
-
}
|
|
180
|
-
function resolveFormat(flags) {
|
|
181
|
-
if (getBool(flags, "json"))
|
|
182
|
-
return "json";
|
|
183
|
-
if (getBool(flags, "md") || getBool(flags, "markdown"))
|
|
184
|
-
return "md";
|
|
185
|
-
const raw = getString(flags, "format") || "json";
|
|
186
|
-
if (raw === "md" || raw === "markdown")
|
|
187
|
-
return "md";
|
|
188
|
-
return "json";
|
|
189
|
-
}
|
|
190
|
-
function resolveApiBase(flags) {
|
|
191
|
-
const raw = getString(flags, "base") || process.env.VERTAAUX_API_BASE || DEFAULT_API_BASE;
|
|
192
|
-
return raw.replace(/\/$/, "");
|
|
193
|
-
}
|
|
194
|
-
function getApiKey() {
|
|
195
|
-
const key = process.env.VERTAAUX_API_KEY;
|
|
196
|
-
if (!key) {
|
|
197
|
-
throw new Error("VERTAAUX_API_KEY is required");
|
|
198
|
-
}
|
|
199
|
-
return key;
|
|
200
|
-
}
|
|
201
|
-
async function apiRequest(base, reqPath, options) {
|
|
202
|
-
const apiKey = getApiKey();
|
|
203
|
-
const url = `${base}${reqPath}`;
|
|
204
|
-
const res = await fetch(url, {
|
|
205
|
-
method: options.method,
|
|
206
|
-
headers: {
|
|
207
|
-
"Content-Type": "application/json",
|
|
208
|
-
"X-API-Key": apiKey,
|
|
209
|
-
},
|
|
210
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
211
|
-
});
|
|
212
|
-
if (!res.ok) {
|
|
213
|
-
let detail = res.statusText;
|
|
214
|
-
try {
|
|
215
|
-
const data = (await res.json());
|
|
216
|
-
detail =
|
|
217
|
-
(data && (data.error || data.message)) ||
|
|
218
|
-
JSON.stringify(data) ||
|
|
219
|
-
detail;
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
try {
|
|
223
|
-
detail = await res.text();
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// ignore
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
throw new Error(`HTTP ${res.status}: ${detail}`);
|
|
230
|
-
}
|
|
231
|
-
return (await res.json().catch(() => ({})));
|
|
232
|
-
}
|
|
233
|
-
function sleep(ms) {
|
|
234
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
235
|
-
}
|
|
236
|
-
async function waitForAudit(base, jobId, timeoutMs, intervalMs) {
|
|
237
|
-
const start = Date.now();
|
|
238
|
-
while (true) {
|
|
239
|
-
const status = await apiRequest(base, `/audit/${jobId}`, {
|
|
240
|
-
method: "GET",
|
|
241
|
-
});
|
|
242
|
-
if (status.status === "completed")
|
|
243
|
-
return status;
|
|
244
|
-
if (status.status === "failed")
|
|
245
|
-
return status;
|
|
246
|
-
if (Date.now() - start > timeoutMs) {
|
|
247
|
-
throw new Error(`Timed out waiting for audit ${jobId}`);
|
|
248
|
-
}
|
|
249
|
-
await sleep(intervalMs);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function normalizeIssues(issues) {
|
|
253
|
-
if (Array.isArray(issues))
|
|
254
|
-
return issues;
|
|
255
|
-
if (issues && typeof issues === "object") {
|
|
256
|
-
const values = Object.values(issues);
|
|
257
|
-
return values.flatMap((value) => (Array.isArray(value) ? value : []));
|
|
258
|
-
}
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
function toNumber(value) {
|
|
262
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
263
|
-
return value;
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
function getOverallScore(scores) {
|
|
267
|
-
if (!scores)
|
|
268
|
-
return null;
|
|
269
|
-
const direct = toNumber(scores.overall ?? scores.ux ?? scores.total);
|
|
270
|
-
if (direct !== null)
|
|
271
|
-
return direct;
|
|
272
|
-
const numeric = Object.values(scores)
|
|
273
|
-
.map((value) => toNumber(value))
|
|
274
|
-
.filter((value) => value !== null);
|
|
275
|
-
if (numeric.length === 0)
|
|
276
|
-
return null;
|
|
277
|
-
const avg = numeric.reduce((sum, value) => sum + value, 0) / numeric.length;
|
|
278
|
-
return Math.round(avg);
|
|
279
|
-
}
|
|
280
|
-
function getCategoryScore(scores, key) {
|
|
281
|
-
if (!scores)
|
|
282
|
-
return null;
|
|
283
|
-
return toNumber(scores[key]);
|
|
284
|
-
}
|
|
285
|
-
function formatScoresTable(scores) {
|
|
286
|
-
if (!scores)
|
|
287
|
-
return "No scores available.";
|
|
288
|
-
const entries = Object.entries(scores).filter(([, value]) => typeof value === "number");
|
|
289
|
-
if (entries.length === 0)
|
|
290
|
-
return "No scores available.";
|
|
291
|
-
const lines = ["| Category | Score |", "| --- | --- |"];
|
|
292
|
-
for (const [key, value] of entries) {
|
|
293
|
-
lines.push(`| ${key} | ${value} |`);
|
|
294
|
-
}
|
|
295
|
-
return lines.join("\n");
|
|
296
|
-
}
|
|
297
|
-
function formatIssuesList(issues, limit = 5) {
|
|
298
|
-
if (!issues.length)
|
|
299
|
-
return "No issues.";
|
|
300
|
-
const lines = issues.slice(0, limit).map((issue, index) => {
|
|
301
|
-
const label = issue.title || issue.id || "Issue";
|
|
302
|
-
const severity = issue.severity ? issue.severity.toUpperCase() : "INFO";
|
|
303
|
-
return `${index + 1}. [${severity}] ${label}`;
|
|
304
|
-
});
|
|
305
|
-
if (issues.length > limit) {
|
|
306
|
-
lines.push(`...and ${issues.length - limit} more`);
|
|
307
|
-
}
|
|
308
|
-
return lines.join("\n");
|
|
309
|
-
}
|
|
310
|
-
function formatAuditMarkdown(result) {
|
|
311
|
-
const status = result.status || "unknown";
|
|
312
|
-
const lines = [`## Audit Results`, `- Status: ${status}`];
|
|
313
|
-
if (result.job_id)
|
|
314
|
-
lines.push(`- Job ID: ${result.job_id}`);
|
|
315
|
-
if (result.url)
|
|
316
|
-
lines.push(`- URL: ${result.url}`);
|
|
317
|
-
if (result.mode)
|
|
318
|
-
lines.push(`- Mode: ${result.mode}`);
|
|
319
|
-
if (typeof result.progress === "number" && status !== "completed") {
|
|
320
|
-
lines.push(`- Progress: ${result.progress}%`);
|
|
321
|
-
}
|
|
322
|
-
if (status !== "completed")
|
|
323
|
-
return lines.join("\n");
|
|
324
|
-
const overall = getOverallScore(result.scores);
|
|
325
|
-
if (overall !== null)
|
|
326
|
-
lines.push(`- Overall score: ${overall}`);
|
|
327
|
-
lines.push("\n### Scores");
|
|
328
|
-
lines.push(formatScoresTable(result.scores));
|
|
329
|
-
const issues = normalizeIssues(result.issues);
|
|
330
|
-
lines.push("\n### Issues");
|
|
331
|
-
lines.push(`Total issues: ${issues.length}`);
|
|
332
|
-
lines.push(formatIssuesList(issues));
|
|
333
|
-
return lines.join("\n");
|
|
334
|
-
}
|
|
335
|
-
function formatA11yMarkdown(result) {
|
|
336
|
-
const lines = [`## Accessibility Audit`, `- Status: ${result.status || "unknown"}`];
|
|
337
|
-
if (result.url)
|
|
338
|
-
lines.push(`- URL: ${result.url}`);
|
|
339
|
-
if (result.mode)
|
|
340
|
-
lines.push(`- Mode: ${result.mode}`);
|
|
341
|
-
if (result.status !== "completed")
|
|
342
|
-
return lines.join("\n");
|
|
343
|
-
const a11yScore = getCategoryScore(result.scores, "accessibility");
|
|
344
|
-
if (a11yScore !== null)
|
|
345
|
-
lines.push(`- Accessibility score: ${a11yScore}`);
|
|
346
|
-
const issues = normalizeIssues(result.issues).filter((issue) => {
|
|
347
|
-
const category = (issue.category || "").toLowerCase();
|
|
348
|
-
return (category.includes("accessibility") ||
|
|
349
|
-
category.includes("a11y") ||
|
|
350
|
-
category.includes("wcag"));
|
|
351
|
-
});
|
|
352
|
-
lines.push("\n### Accessibility Issues");
|
|
353
|
-
lines.push(`Total accessibility issues: ${issues.length}`);
|
|
354
|
-
lines.push(formatIssuesList(issues));
|
|
355
|
-
return lines.join("\n");
|
|
356
|
-
}
|
|
357
|
-
function formatCompareMarkdown(compare) {
|
|
358
|
-
const lines = ["## Audit Comparison"];
|
|
359
|
-
lines.push(`- URL A: ${compare.urlA}`);
|
|
360
|
-
lines.push(`- URL B: ${compare.urlB}`);
|
|
361
|
-
lines.push(`- Job A: ${compare.jobA}`);
|
|
362
|
-
lines.push(`- Job B: ${compare.jobB}`);
|
|
363
|
-
lines.push("\n### Overall Scores");
|
|
364
|
-
lines.push("| Site | Score | Delta |");
|
|
365
|
-
lines.push("| --- | --- | --- |");
|
|
366
|
-
lines.push(`| A | ${compare.overallA ?? "n/a"} | ${compare.delta ?? "n/a"} |`);
|
|
367
|
-
lines.push(`| B | ${compare.overallB ?? "n/a"} | ${compare.delta ?? "n/a"} |`);
|
|
368
|
-
lines.push("\n### Category Scores");
|
|
369
|
-
lines.push("| Category | A | B | Delta |");
|
|
370
|
-
lines.push("| --- | --- | --- | --- |");
|
|
371
|
-
for (const [key, values] of Object.entries(compare.categoryDeltas)) {
|
|
372
|
-
lines.push(`| ${key} | ${values.a ?? "n/a"} | ${values.b ?? "n/a"} | ${values.delta ?? "n/a"} |`);
|
|
373
|
-
}
|
|
374
|
-
lines.push("\n### Issue Counts");
|
|
375
|
-
lines.push(`- A: ${compare.issuesA}`);
|
|
376
|
-
lines.push(`- B: ${compare.issuesB}`);
|
|
377
|
-
return lines.join("\n");
|
|
378
|
-
}
|
|
379
|
-
function formatExplainMarkdown(issue) {
|
|
380
|
-
const lines = ["## Issue Explanation"];
|
|
381
|
-
if (issue.id)
|
|
382
|
-
lines.push(`- ID: ${issue.id}`);
|
|
383
|
-
if (issue.severity)
|
|
384
|
-
lines.push(`- Severity: ${issue.severity}`);
|
|
385
|
-
if (issue.category)
|
|
386
|
-
lines.push(`- Category: ${issue.category}`);
|
|
387
|
-
if (issue.selector)
|
|
388
|
-
lines.push(`- Selector: ${issue.selector}`);
|
|
389
|
-
if (issue.description) {
|
|
390
|
-
lines.push("\n### Description");
|
|
391
|
-
lines.push(issue.description);
|
|
392
|
-
}
|
|
393
|
-
const recommendation = issue.recommendation || issue.recommended_fix;
|
|
394
|
-
if (recommendation) {
|
|
395
|
-
lines.push("\n### Recommendation");
|
|
396
|
-
lines.push(recommendation);
|
|
397
|
-
}
|
|
398
|
-
if (issue.wcag_reference) {
|
|
399
|
-
lines.push("\n### WCAG Reference");
|
|
400
|
-
lines.push(issue.wcag_reference);
|
|
401
|
-
}
|
|
402
|
-
return lines.join("\n");
|
|
403
|
-
}
|
|
404
|
-
function formatPatchMarkdown(patch) {
|
|
405
|
-
if (!patch)
|
|
406
|
-
return "No patch generated.";
|
|
407
|
-
const lines = [
|
|
408
|
-
"## Patch Generated",
|
|
409
|
-
"",
|
|
410
|
-
`**Confidence:** ${patch.confidence.label} (${patch.confidence.percentage}%)`,
|
|
411
|
-
`**Classification:** ${patch.classification}`,
|
|
412
|
-
"",
|
|
413
|
-
"### Explanation",
|
|
414
|
-
patch.explanation,
|
|
415
|
-
"",
|
|
416
|
-
"### Search",
|
|
417
|
-
"```",
|
|
418
|
-
patch.search,
|
|
419
|
-
"```",
|
|
420
|
-
"",
|
|
421
|
-
"### Replace",
|
|
422
|
-
"```",
|
|
423
|
-
patch.replace,
|
|
424
|
-
"```",
|
|
425
|
-
];
|
|
426
|
-
return lines.join("\n");
|
|
427
|
-
}
|
|
428
|
-
function formatVerifyMarkdown(result) {
|
|
429
|
-
if (!result.success || !result.verification) {
|
|
430
|
-
return `## Verification Failed\n\n${result.error?.message || "Unknown error"}`;
|
|
431
|
-
}
|
|
432
|
-
const v = result.verification;
|
|
433
|
-
const deltaSign = v.score_delta.delta >= 0 ? "+" : "";
|
|
434
|
-
const lines = [
|
|
435
|
-
"## Verification Result",
|
|
436
|
-
"",
|
|
437
|
-
`**Issue Fixed:** ${v.issue_fixed ? "Yes" : "No"}`,
|
|
438
|
-
`**Score Delta:** ${v.score_delta.before} -> ${v.score_delta.after} (${deltaSign}${v.score_delta.delta})`,
|
|
439
|
-
`**Component:** ${v.component_audited}`,
|
|
440
|
-
`**Duration:** ${v.duration_ms}ms`,
|
|
441
|
-
];
|
|
442
|
-
if (v.regressions.length > 0) {
|
|
443
|
-
lines.push("", "### Regressions");
|
|
444
|
-
for (const r of v.regressions) {
|
|
445
|
-
lines.push(`- [${r.impact.toUpperCase()}] ${r.rule_id}: ${r.description}`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
lines.push("", "### Regressions", "None");
|
|
450
|
-
}
|
|
451
|
-
if (v.error) {
|
|
452
|
-
lines.push("", `**Warning:** ${v.error}`);
|
|
453
|
-
}
|
|
454
|
-
return lines.join("\n");
|
|
455
|
-
}
|
|
456
|
-
// Auto-fixable issue types (from lib/patch/classifier.ts)
|
|
457
|
-
const AUTO_FIXABLE_TYPES = new Set([
|
|
458
|
-
"label",
|
|
459
|
-
"input-missing-label",
|
|
460
|
-
"form-field-label",
|
|
461
|
-
"button-name",
|
|
462
|
-
"missing-button-name",
|
|
463
|
-
"link-name",
|
|
464
|
-
"html-has-lang",
|
|
465
|
-
"missing-lang-attribute",
|
|
466
|
-
"html-lang-valid",
|
|
467
|
-
"document-title",
|
|
468
|
-
"bypass",
|
|
469
|
-
"region",
|
|
470
|
-
"landmark-one-main",
|
|
471
|
-
"color-contrast",
|
|
472
|
-
"autocomplete-valid",
|
|
473
|
-
"aria-required-attr",
|
|
474
|
-
"aria-valid-attr",
|
|
475
|
-
"aria-valid-attr-value",
|
|
476
|
-
]);
|
|
477
|
-
function isAutoFixable(issueType) {
|
|
478
|
-
const normalized = issueType.toLowerCase().replace(/^(axe|wcag)-/, "");
|
|
479
|
-
return AUTO_FIXABLE_TYPES.has(normalized);
|
|
480
|
-
}
|
|
481
|
-
function formatBatchMarkdown(results) {
|
|
482
|
-
const lines = [
|
|
483
|
-
"## Batch Patch Results",
|
|
484
|
-
"",
|
|
485
|
-
`**Total Requested:** ${results.totalRequested}`,
|
|
486
|
-
`**Succeeded:** ${results.successes.length}`,
|
|
487
|
-
`**Failed:** ${results.failures.length}`,
|
|
488
|
-
`**Skipped:** ${results.totalSkipped}`,
|
|
489
|
-
];
|
|
490
|
-
if (results.successes.length > 0) {
|
|
491
|
-
lines.push("", "### Successful Patches");
|
|
492
|
-
for (const s of results.successes) {
|
|
493
|
-
const conf = s.patch?.confidence;
|
|
494
|
-
const cls = s.patch?.classification || "unknown";
|
|
495
|
-
const confStr = conf ? `${conf.label} (${conf.percentage}%)` : "n/a";
|
|
496
|
-
lines.push(`- ${s.issueId}: ${confStr} [${cls}]`);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (results.failures.length > 0) {
|
|
500
|
-
lines.push("", "### Failed");
|
|
501
|
-
for (const f of results.failures) {
|
|
502
|
-
const details = f.details ? ` - ${f.details}` : "";
|
|
503
|
-
lines.push(`- ${f.issueId}: ${f.reason}${details}`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return lines.join("\n");
|
|
507
|
-
}
|
|
508
|
-
function printOutput(format, data, markdown) {
|
|
509
|
-
if (format === "md") {
|
|
510
|
-
console.log(markdown || "");
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
console.log(JSON.stringify(data, null, 2));
|
|
514
|
-
}
|
|
515
|
-
// ============================================================================
|
|
516
|
-
// Legacy command handlers (a11y, scan, compare, status, explain, fix, etc.)
|
|
517
|
-
// ============================================================================
|
|
518
|
-
async function runAuditCommand(base, url, flags, label) {
|
|
519
|
-
const mode = getString(flags, "mode") || "basic";
|
|
520
|
-
const timeout = getNumber(flags, "timeout");
|
|
521
|
-
const userAgent = getString(flags, "user-agent");
|
|
522
|
-
const engineVersion = getString(flags, "engine-version");
|
|
523
|
-
const failOnScore = getNumber(flags, "fail-on-score");
|
|
524
|
-
const created = await apiRequest(base, "/audit", {
|
|
525
|
-
method: "POST",
|
|
526
|
-
body: {
|
|
527
|
-
url,
|
|
528
|
-
mode,
|
|
529
|
-
timeout,
|
|
530
|
-
user_agent: userAgent,
|
|
531
|
-
engine_version: engineVersion,
|
|
532
|
-
fail_on_score: failOnScore,
|
|
533
|
-
},
|
|
534
|
-
});
|
|
535
|
-
const format = resolveFormat(flags);
|
|
536
|
-
if (!getBool(flags, "wait")) {
|
|
537
|
-
const markdown = `## Audit queued\n- Job ID: ${created.job_id}\n- URL: ${created.url}\n- Mode: ${created.mode}`;
|
|
538
|
-
printOutput(format, created, markdown);
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (!created.job_id) {
|
|
542
|
-
throw new Error("Audit response missing job_id");
|
|
543
|
-
}
|
|
544
|
-
const waitTimeout = getNumber(flags, "timeout") || 60000;
|
|
545
|
-
const interval = getNumber(flags, "interval") || 5000;
|
|
546
|
-
const result = await waitForAudit(base, created.job_id, waitTimeout, interval);
|
|
547
|
-
if (label === "a11y") {
|
|
548
|
-
const a11yScore = getCategoryScore(result.scores, "accessibility");
|
|
549
|
-
if (failOnScore !== undefined && a11yScore !== null && a11yScore < failOnScore) {
|
|
550
|
-
process.exitCode = 1;
|
|
551
|
-
}
|
|
552
|
-
const issues = normalizeIssues(result.issues).filter((issue) => {
|
|
553
|
-
const category = (issue.category || "").toLowerCase();
|
|
554
|
-
return (category.includes("accessibility") ||
|
|
555
|
-
category.includes("a11y") ||
|
|
556
|
-
category.includes("wcag"));
|
|
557
|
-
});
|
|
558
|
-
const payload = {
|
|
559
|
-
...result,
|
|
560
|
-
accessibility_score: a11yScore,
|
|
561
|
-
accessibility_issues: issues,
|
|
562
|
-
};
|
|
563
|
-
printOutput(format, payload, formatA11yMarkdown(result));
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const overall = getOverallScore(result.scores);
|
|
567
|
-
if (failOnScore !== undefined && overall !== null && overall < failOnScore) {
|
|
568
|
-
process.exitCode = 1;
|
|
569
|
-
}
|
|
570
|
-
printOutput(format, result, formatAuditMarkdown(result));
|
|
571
|
-
}
|
|
572
|
-
async function runCompareCommand(base, urls, flags) {
|
|
573
|
-
const mode = getString(flags, "mode") || "basic";
|
|
574
|
-
const wait = getBool(flags, "wait");
|
|
575
|
-
const format = resolveFormat(flags);
|
|
576
|
-
const [urlA, urlB] = urls;
|
|
577
|
-
const jobA = await apiRequest(base, "/audit", {
|
|
578
|
-
method: "POST",
|
|
579
|
-
body: { url: urlA, mode },
|
|
580
|
-
});
|
|
581
|
-
const jobB = await apiRequest(base, "/audit", {
|
|
582
|
-
method: "POST",
|
|
583
|
-
body: { url: urlB, mode },
|
|
584
|
-
});
|
|
585
|
-
if (!wait) {
|
|
586
|
-
const payload = { job_a: jobA, job_b: jobB };
|
|
587
|
-
const markdown = `## Audit comparison queued\n- Job A: ${jobA.job_id}\n- Job B: ${jobB.job_id}`;
|
|
588
|
-
printOutput(format, payload, markdown);
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
if (!jobA.job_id || !jobB.job_id) {
|
|
592
|
-
throw new Error("Compare response missing job_id");
|
|
593
|
-
}
|
|
594
|
-
const timeout = getNumber(flags, "timeout") || 60000;
|
|
595
|
-
const interval = getNumber(flags, "interval") || 5000;
|
|
596
|
-
const resultA = await waitForAudit(base, jobA.job_id, timeout, interval);
|
|
597
|
-
const resultB = await waitForAudit(base, jobB.job_id, timeout, interval);
|
|
598
|
-
const overallA = getOverallScore(resultA.scores);
|
|
599
|
-
const overallB = getOverallScore(resultB.scores);
|
|
600
|
-
const delta = overallA !== null && overallB !== null ? overallB - overallA : null;
|
|
601
|
-
const scoresA = resultA.scores || {};
|
|
602
|
-
const scoresB = resultB.scores || {};
|
|
603
|
-
const keys = new Set([...Object.keys(scoresA), ...Object.keys(scoresB)]);
|
|
604
|
-
const categoryDeltas = {};
|
|
605
|
-
for (const key of keys) {
|
|
606
|
-
const a = toNumber(scoresA[key]);
|
|
607
|
-
const b = toNumber(scoresB[key]);
|
|
608
|
-
categoryDeltas[key] = {
|
|
609
|
-
a,
|
|
610
|
-
b,
|
|
611
|
-
delta: a !== null && b !== null ? b - a : null,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
const issuesA = normalizeIssues(resultA.issues).length;
|
|
615
|
-
const issuesB = normalizeIssues(resultB.issues).length;
|
|
616
|
-
const compare = {
|
|
617
|
-
urlA,
|
|
618
|
-
urlB,
|
|
619
|
-
jobA: resultA.job_id || "",
|
|
620
|
-
jobB: resultB.job_id || "",
|
|
621
|
-
overallA,
|
|
622
|
-
overallB,
|
|
623
|
-
delta,
|
|
624
|
-
categoryDeltas,
|
|
625
|
-
issuesA,
|
|
626
|
-
issuesB,
|
|
627
|
-
};
|
|
628
|
-
const failOnScore = getNumber(flags, "fail-on-score");
|
|
629
|
-
if (failOnScore !== undefined &&
|
|
630
|
-
((overallA !== null && overallA < failOnScore) ||
|
|
631
|
-
(overallB !== null && overallB < failOnScore))) {
|
|
632
|
-
process.exitCode = 1;
|
|
633
|
-
}
|
|
634
|
-
printOutput(format, compare, formatCompareMarkdown(compare));
|
|
635
|
-
}
|
|
636
|
-
async function runFixCommand(base, jobId, issueId, flags, globalFlags = {}) {
|
|
637
|
-
const fileContent = getString(flags, "file-content");
|
|
638
|
-
const format = resolveFormat(flags);
|
|
639
|
-
if (!fileContent) {
|
|
640
|
-
throw new Error("fix requires --file-content <code>");
|
|
641
|
-
}
|
|
642
|
-
const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
|
|
643
|
-
if (isTTYOutput) {
|
|
644
|
-
process.stderr.write("Generating patch...\r");
|
|
645
|
-
}
|
|
646
|
-
const result = await apiRequest(base, "/patch", {
|
|
647
|
-
method: "POST",
|
|
648
|
-
body: {
|
|
649
|
-
job_id: jobId,
|
|
650
|
-
issue_id: issueId,
|
|
651
|
-
file_content: fileContent,
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
if (isTTYOutput) {
|
|
655
|
-
process.stderr.write(" \r");
|
|
656
|
-
}
|
|
657
|
-
if (!result.success) {
|
|
658
|
-
process.exitCode = 1;
|
|
659
|
-
}
|
|
660
|
-
if (globalFlags.dryRun && isTTYOutput) {
|
|
661
|
-
process.stderr.write("[dry-run] Showing patch preview — no changes applied.\n\n");
|
|
662
|
-
}
|
|
663
|
-
if (format === "json") {
|
|
664
|
-
const output = globalFlags.dryRun ? { ...result, dry_run: true } : result;
|
|
665
|
-
console.log(JSON.stringify(output, null, 2));
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
if (result.success && result.patch) {
|
|
669
|
-
console.log(formatPatchMarkdown(result.patch));
|
|
670
|
-
}
|
|
671
|
-
else {
|
|
672
|
-
console.log(`## Patch Generation Failed\n\n${result.error?.message || "Unknown error"}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
async function runVerifyCommand(base, flags) {
|
|
677
|
-
const search = getString(flags, "search");
|
|
678
|
-
const replace = getString(flags, "replace");
|
|
679
|
-
const issueId = getString(flags, "issue");
|
|
680
|
-
const url = getString(flags, "url");
|
|
681
|
-
const selector = getString(flags, "selector");
|
|
682
|
-
const timeoutMs = getNumber(flags, "timeout");
|
|
683
|
-
const format = resolveFormat(flags);
|
|
684
|
-
if (!search || !replace || !issueId || !url || !selector) {
|
|
685
|
-
throw new Error("verify requires --search, --replace, --issue, --url, and --selector");
|
|
686
|
-
}
|
|
687
|
-
const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
|
|
688
|
-
if (isTTYOutput) {
|
|
689
|
-
process.stderr.write("Running verification...\r");
|
|
690
|
-
}
|
|
691
|
-
const result = await apiRequest(base, "/verify", {
|
|
692
|
-
method: "POST",
|
|
693
|
-
body: {
|
|
694
|
-
patch: {
|
|
695
|
-
search,
|
|
696
|
-
replace,
|
|
697
|
-
issue_id: issueId,
|
|
698
|
-
},
|
|
699
|
-
url,
|
|
700
|
-
selector,
|
|
701
|
-
timeout_ms: timeoutMs || 60000,
|
|
702
|
-
},
|
|
703
|
-
});
|
|
704
|
-
if (isTTYOutput) {
|
|
705
|
-
process.stderr.write(" \r");
|
|
706
|
-
}
|
|
707
|
-
// Exit code: 1 if verification failed OR issue not fixed OR regressions
|
|
708
|
-
if (!result.success) {
|
|
709
|
-
process.exitCode = 1;
|
|
710
|
-
}
|
|
711
|
-
else if (result.verification &&
|
|
712
|
-
(!result.verification.issue_fixed || result.verification.regressions.length > 0)) {
|
|
713
|
-
process.exitCode = 1;
|
|
714
|
-
}
|
|
715
|
-
if (format === "json") {
|
|
716
|
-
console.log(JSON.stringify(result, null, 2));
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
console.log(formatVerifyMarkdown(result));
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
const BATCH_LIMIT = 10;
|
|
723
|
-
async function runFixAllCommand(base, jobId, flags, globalFlags = {}) {
|
|
724
|
-
const fileContent = getString(flags, "file-content");
|
|
725
|
-
const autoFixOnly = getBool(flags, "auto-fix-only");
|
|
726
|
-
const format = resolveFormat(flags);
|
|
727
|
-
if (!fileContent) {
|
|
728
|
-
throw new Error("fix-all requires --file-content <code>");
|
|
729
|
-
}
|
|
730
|
-
const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
|
|
731
|
-
// Fetch audit to get issues
|
|
732
|
-
if (isTTYOutput) {
|
|
733
|
-
process.stderr.write("Fetching audit...\r");
|
|
734
|
-
}
|
|
735
|
-
const audit = await apiRequest(base, `/audit/${jobId}`, {
|
|
736
|
-
method: "GET",
|
|
737
|
-
});
|
|
738
|
-
if (audit.status !== "completed") {
|
|
739
|
-
throw new Error(`Audit ${jobId} is not completed (status: ${audit.status})`);
|
|
740
|
-
}
|
|
741
|
-
// Normalize and filter issues
|
|
742
|
-
let issues = normalizeIssues(audit.issues);
|
|
743
|
-
const totalRequested = issues.length;
|
|
744
|
-
if (autoFixOnly) {
|
|
745
|
-
issues = issues.filter((issue) => {
|
|
746
|
-
const typeId = issue.id || "";
|
|
747
|
-
return isAutoFixable(typeId);
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
// Respect batch limit
|
|
751
|
-
const totalSkipped = Math.max(0, issues.length - BATCH_LIMIT);
|
|
752
|
-
const issuesToProcess = issues.slice(0, BATCH_LIMIT);
|
|
753
|
-
if (isTTYOutput) {
|
|
754
|
-
process.stderr.write(" \r");
|
|
755
|
-
}
|
|
756
|
-
// --dry-run: show what would be processed and exit
|
|
757
|
-
if (globalFlags.dryRun) {
|
|
758
|
-
process.stderr.write(`[dry-run] Would generate patches for ${issuesToProcess.length} issues:\n`);
|
|
759
|
-
for (const issue of issuesToProcess) {
|
|
760
|
-
process.stderr.write(` - ${issue.id || "unknown"}: ${issue.title || issue.description || "(no title)"}\n`);
|
|
761
|
-
}
|
|
762
|
-
if (totalSkipped > 0) {
|
|
763
|
-
process.stderr.write(` (${totalSkipped} additional issues skipped — batch limit ${BATCH_LIMIT})\n`);
|
|
764
|
-
}
|
|
765
|
-
process.stderr.write("\nNo patches generated. Remove --dry-run to execute.\n");
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
// --yes is available for future interactive confirmation steps
|
|
769
|
-
// Currently fix-all runs non-interactively, but --yes suppresses any
|
|
770
|
-
// confirmation prompts that may be added (e.g., "Apply all N patches?")
|
|
771
|
-
const results = {
|
|
772
|
-
successes: [],
|
|
773
|
-
failures: [],
|
|
774
|
-
totalRequested,
|
|
775
|
-
totalSkipped,
|
|
776
|
-
};
|
|
777
|
-
// Process issues sequentially to avoid overwhelming LLM API
|
|
778
|
-
for (let i = 0; i < issuesToProcess.length; i++) {
|
|
779
|
-
const issue = issuesToProcess[i];
|
|
780
|
-
const curIssueId = issue.id || `issue-${i}`;
|
|
781
|
-
if (isTTYOutput) {
|
|
782
|
-
process.stderr.write(`Generating patch ${i + 1}/${issuesToProcess.length}...\r`);
|
|
783
|
-
}
|
|
784
|
-
try {
|
|
785
|
-
const patchResult = await apiRequest(base, "/patch", {
|
|
786
|
-
method: "POST",
|
|
787
|
-
body: {
|
|
788
|
-
job_id: jobId,
|
|
789
|
-
issue_id: curIssueId,
|
|
790
|
-
file_content: fileContent,
|
|
791
|
-
},
|
|
792
|
-
});
|
|
793
|
-
if (patchResult.success && patchResult.patch) {
|
|
794
|
-
results.successes.push({
|
|
795
|
-
issueId: curIssueId,
|
|
796
|
-
patch: patchResult.patch,
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
results.failures.push({
|
|
801
|
-
issueId: curIssueId,
|
|
802
|
-
reason: patchResult.error?.code || "PATCH_FAILED",
|
|
803
|
-
details: patchResult.error?.message,
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
catch (err) {
|
|
808
|
-
results.failures.push({
|
|
809
|
-
issueId: curIssueId,
|
|
810
|
-
reason: "REQUEST_ERROR",
|
|
811
|
-
details: err instanceof Error ? err.message : String(err),
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
// Clear progress line
|
|
816
|
-
if (isTTYOutput) {
|
|
817
|
-
process.stderr.write(" \r");
|
|
818
|
-
}
|
|
819
|
-
// Exit code 1 if any failures
|
|
820
|
-
if (results.failures.length > 0) {
|
|
821
|
-
process.exitCode = 1;
|
|
822
|
-
}
|
|
823
|
-
if (format === "json") {
|
|
824
|
-
console.log(JSON.stringify(results, null, 2));
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
console.log(formatBatchMarkdown(results));
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
async function runExplainCommand(base, flags) {
|
|
831
|
-
const format = resolveFormat(flags);
|
|
832
|
-
const issueJson = getString(flags, "issue");
|
|
833
|
-
const issueFile = getString(flags, "file");
|
|
834
|
-
const jobId = getString(flags, "job") || getString(flags, "job-id");
|
|
835
|
-
const issueId = getString(flags, "issue-id") || getString(flags, "id");
|
|
836
|
-
let issue = null;
|
|
837
|
-
if (issueJson) {
|
|
838
|
-
issue = JSON.parse(issueJson);
|
|
839
|
-
}
|
|
840
|
-
else if (issueFile) {
|
|
841
|
-
const raw = fs.readFileSync(issueFile, "utf-8");
|
|
842
|
-
issue = JSON.parse(raw);
|
|
843
|
-
}
|
|
844
|
-
else if (jobId && issueId) {
|
|
845
|
-
const result = await apiRequest(base, `/audit/${jobId}`, {
|
|
846
|
-
method: "GET",
|
|
847
|
-
});
|
|
848
|
-
const issues = normalizeIssues(result.issues);
|
|
849
|
-
issue = issues.find((item) => item.id === issueId) || null;
|
|
850
|
-
if (!issue) {
|
|
851
|
-
throw new Error(`Issue ${issueId} not found in job ${jobId}`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
if (!issue) {
|
|
855
|
-
throw new Error("Provide --issue, --file, or --job with --issue-id");
|
|
856
|
-
}
|
|
857
|
-
const payload = { issue };
|
|
858
|
-
printOutput(format, payload, formatExplainMarkdown(issue));
|
|
859
|
-
}
|
|
860
102
|
// ============================================================================
|
|
861
103
|
// Commander.js CLI setup
|
|
862
104
|
// ============================================================================
|
|
@@ -874,11 +116,18 @@ program
|
|
|
874
116
|
.option("--machine", "Strict machine-readable output (JSON stdout, diagnostics stderr)")
|
|
875
117
|
.option("--color", "Force color output")
|
|
876
118
|
.option("--no-color", "Disable color output")
|
|
119
|
+
.option("--plain", "Strip all color and use ASCII-only output (auto-enabled when stdout is not a TTY)")
|
|
877
120
|
.option("--dashboard", "Force live dashboard during audit --wait")
|
|
878
121
|
.option("--no-dashboard", "Disable live dashboard (use spinner instead)")
|
|
879
122
|
.option("--dry-run", "Show what would happen without executing")
|
|
880
123
|
.option("-y, --yes", "Auto-confirm all interactive prompts")
|
|
881
124
|
.option("--verbose", "Expand output with additional details")
|
|
125
|
+
.addHelpText("after", `
|
|
126
|
+
Authentication:
|
|
127
|
+
SDK handles auth by default (reads VERTAAUX_API_KEY or ~/.vertaaux/credentials.json).
|
|
128
|
+
Use --api-key <key> or VERTAAUX_API_KEY env var to override for scripts and CI.
|
|
129
|
+
Run "vertaa login" to set up default credentials.
|
|
130
|
+
`)
|
|
882
131
|
.configureOutput({
|
|
883
132
|
outputError: (str, _write) => {
|
|
884
133
|
const formatted = formatCommanderError(str);
|
|
@@ -895,31 +144,41 @@ program
|
|
|
895
144
|
// For other Commander errors (help, version), use Commander's default exit code
|
|
896
145
|
process.exit(err.exitCode);
|
|
897
146
|
})
|
|
898
|
-
.hook("preAction", (thisCommand) => {
|
|
147
|
+
.hook("preAction", async (thisCommand) => {
|
|
899
148
|
const opts = thisCommand.optsWithGlobals();
|
|
900
149
|
const machineMode = opts.machine || false;
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
150
|
+
// Plain mode: explicit --plain or auto-detect when stdout is not a TTY
|
|
151
|
+
// Precedence: explicit --plain > explicit --color/--no-color > NO_COLOR/FORCE_COLOR > TTY check
|
|
152
|
+
const explicitColor = opts.color === true;
|
|
153
|
+
const autoPlain = !process.stdout.isTTY && !explicitColor;
|
|
154
|
+
const plain = Boolean(opts.plain) || autoPlain;
|
|
155
|
+
if (plain && !machineMode) {
|
|
156
|
+
setPlainMode(true);
|
|
907
157
|
}
|
|
908
158
|
else {
|
|
909
|
-
|
|
159
|
+
// Apply color settings from flags or environment
|
|
160
|
+
if (opts.color === false || process.env.NO_COLOR !== undefined) {
|
|
161
|
+
setColorEnabled(false);
|
|
162
|
+
}
|
|
163
|
+
else if (opts.color === true || process.env.FORCE_COLOR !== undefined) {
|
|
164
|
+
setColorEnabled(true);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
setColorEnabled(shouldUseColor());
|
|
168
|
+
}
|
|
910
169
|
}
|
|
911
170
|
if (machineMode) {
|
|
912
171
|
opts.quiet = true;
|
|
913
172
|
opts.banner = false;
|
|
914
173
|
setColorEnabled(false);
|
|
915
174
|
}
|
|
916
|
-
|
|
175
|
+
await animateBanner({
|
|
917
176
|
version,
|
|
918
177
|
quiet: opts.quiet || machineMode,
|
|
919
178
|
noBanner: opts.banner === false || machineMode,
|
|
920
179
|
});
|
|
921
180
|
});
|
|
922
|
-
// Register
|
|
181
|
+
// Register modern Commander.js commands
|
|
923
182
|
registerAuditCommand(program);
|
|
924
183
|
registerBaselineCommand(program);
|
|
925
184
|
registerCommentCommand(program);
|
|
@@ -938,187 +197,63 @@ registerPatchReviewCommand(program);
|
|
|
938
197
|
registerCompareCommand(program);
|
|
939
198
|
registerReleaseNotesCommand(program);
|
|
940
199
|
registerDocCommand(program);
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
program
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
.action(async (url, cmdOptions) => {
|
|
952
|
-
try {
|
|
953
|
-
const base = resolveApiBase(cmdOptions);
|
|
954
|
-
await runAuditCommand(base, url, { ...cmdOptions, wait: cmdOptions.wait ?? true }, "a11y");
|
|
955
|
-
}
|
|
956
|
-
catch (error) {
|
|
957
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
958
|
-
process.exit(ExitCode.ERROR);
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
program
|
|
962
|
-
.command("scan <url>")
|
|
963
|
-
.description("Run UX scan (alias for audit)")
|
|
964
|
-
.option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
|
|
965
|
-
.option("--wait", "Wait for audit completion")
|
|
966
|
-
.option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout, 60000)
|
|
967
|
-
.option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval, 5000)
|
|
968
|
-
.option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
|
|
969
|
-
.action(async (url, cmdOptions) => {
|
|
970
|
-
try {
|
|
971
|
-
const base = resolveApiBase(cmdOptions);
|
|
972
|
-
await runAuditCommand(base, url, { ...cmdOptions, wait: cmdOptions.wait ?? true }, "scan");
|
|
973
|
-
}
|
|
974
|
-
catch (error) {
|
|
975
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
976
|
-
process.exit(ExitCode.ERROR);
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
// compare is now registered via registerCompareCommand (Phase 60)
|
|
980
|
-
program
|
|
981
|
-
.command("status <jobId>")
|
|
982
|
-
.description("Get status of an audit job")
|
|
983
|
-
.option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
|
|
984
|
-
.action(async (jobId, cmdOptions) => {
|
|
985
|
-
try {
|
|
986
|
-
const base = resolveApiBase(cmdOptions);
|
|
987
|
-
const result = await apiRequest(base, `/audit/${jobId}`, {
|
|
988
|
-
method: "GET",
|
|
989
|
-
});
|
|
990
|
-
const format = resolveFormat(cmdOptions);
|
|
991
|
-
const overall = getOverallScore(result.scores);
|
|
992
|
-
const payload = { ...result, overall_score: overall };
|
|
993
|
-
const markdown = formatAuditMarkdown(result);
|
|
994
|
-
const failOnScore = getNumber(cmdOptions, "fail-on-score");
|
|
995
|
-
if (failOnScore !== undefined && overall !== null && overall < failOnScore) {
|
|
996
|
-
process.exitCode = 1;
|
|
997
|
-
}
|
|
998
|
-
printOutput(format, payload, markdown);
|
|
999
|
-
}
|
|
1000
|
-
catch (error) {
|
|
1001
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1002
|
-
process.exit(ExitCode.ERROR);
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
// Legacy explain command removed - using new registerExplainCommand
|
|
1006
|
-
program
|
|
1007
|
-
.command("fix <jobId>")
|
|
1008
|
-
.description("Generate a fix patch for an issue")
|
|
1009
|
-
.requiredOption("--issue <id>", "Issue ID to fix")
|
|
1010
|
-
.requiredOption("--file-content <code>", "Source code content")
|
|
1011
|
-
.action(async (jobId, cmdOptions, command) => {
|
|
1012
|
-
try {
|
|
1013
|
-
const globalOpts = command.optsWithGlobals();
|
|
1014
|
-
const base = resolveApiBase(cmdOptions);
|
|
1015
|
-
const issueId = getString(cmdOptions, "issue");
|
|
1016
|
-
if (!issueId)
|
|
1017
|
-
throw new Error("--issue is required");
|
|
1018
|
-
await runFixCommand(base, jobId, issueId, cmdOptions, { dryRun: !!globalOpts.dryRun });
|
|
1019
|
-
}
|
|
1020
|
-
catch (error) {
|
|
1021
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1022
|
-
process.exit(ExitCode.ERROR);
|
|
1023
|
-
}
|
|
1024
|
-
});
|
|
1025
|
-
program
|
|
1026
|
-
.command("fix-all <jobId>")
|
|
1027
|
-
.description("Generate fix patches for all issues in an audit")
|
|
1028
|
-
.requiredOption("--file-content <code>", "Source code content")
|
|
1029
|
-
.option("--auto-fix-only", "Only process auto-fixable issues")
|
|
1030
|
-
.action(async (jobId, cmdOptions, command) => {
|
|
1031
|
-
try {
|
|
1032
|
-
const globalOpts = command.optsWithGlobals();
|
|
1033
|
-
const base = resolveApiBase(cmdOptions);
|
|
1034
|
-
await runFixAllCommand(base, jobId, cmdOptions, { dryRun: !!globalOpts.dryRun, yes: !!globalOpts.yes });
|
|
1035
|
-
}
|
|
1036
|
-
catch (error) {
|
|
1037
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1038
|
-
process.exit(ExitCode.ERROR);
|
|
1039
|
-
}
|
|
1040
|
-
});
|
|
1041
|
-
program
|
|
1042
|
-
.command("verify")
|
|
1043
|
-
.description("Verify that a patch fixes an issue")
|
|
1044
|
-
.requiredOption("--search <text>", "Search pattern")
|
|
1045
|
-
.requiredOption("--replace <text>", "Replace pattern")
|
|
1046
|
-
.requiredOption("--issue <id>", "Issue ID")
|
|
1047
|
-
.requiredOption("--url <url>", "Target URL")
|
|
1048
|
-
.requiredOption("--selector <css>", "CSS selector")
|
|
1049
|
-
.option("--timeout <ms>", "Timeout in milliseconds (1-300000)", parseTimeout, 60000)
|
|
1050
|
-
.action(async (cmdOptions) => {
|
|
1051
|
-
try {
|
|
1052
|
-
const base = resolveApiBase(cmdOptions);
|
|
1053
|
-
await runVerifyCommand(base, cmdOptions);
|
|
1054
|
-
}
|
|
1055
|
-
catch (error) {
|
|
1056
|
-
process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1057
|
-
process.exit(ExitCode.ERROR);
|
|
1058
|
-
}
|
|
1059
|
-
});
|
|
200
|
+
// Register legacy commands (backward compatibility)
|
|
201
|
+
registerA11yCommand(program);
|
|
202
|
+
registerScanCommand(program);
|
|
203
|
+
registerStatusCommand(program);
|
|
204
|
+
registerFixCommand(program);
|
|
205
|
+
registerFixAllCommand(program);
|
|
206
|
+
registerVerifyCommand(program);
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Interactive app (when no command given in TTY)
|
|
209
|
+
// ============================================================================
|
|
1060
210
|
/**
|
|
1061
|
-
*
|
|
211
|
+
* Launch the full InteractiveApp — categorized menu, search, recent commands,
|
|
212
|
+
* help overlay, and command dispatch within the persistent 3-section layout.
|
|
213
|
+
*
|
|
214
|
+
* Replaces the previous showInteractiveMenu() (5-item select prompt) with the
|
|
215
|
+
* full interactive experience designed in Phase 102.
|
|
1062
216
|
*/
|
|
1063
|
-
async function
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
},
|
|
1089
|
-
]);
|
|
1090
|
-
switch (action) {
|
|
1091
|
-
case "audit":
|
|
1092
|
-
// Re-parse with audit command
|
|
1093
|
-
process.argv.push("audit");
|
|
1094
|
-
program.parse();
|
|
1095
|
-
break;
|
|
1096
|
-
case "init":
|
|
1097
|
-
// Re-parse with init command
|
|
1098
|
-
process.argv.push("init");
|
|
1099
|
-
program.parse();
|
|
1100
|
-
break;
|
|
1101
|
-
case "doctor":
|
|
1102
|
-
process.argv.push("doctor");
|
|
1103
|
-
program.parse();
|
|
1104
|
-
break;
|
|
1105
|
-
case "help":
|
|
1106
|
-
program.outputHelp();
|
|
1107
|
-
break;
|
|
1108
|
-
case "exit":
|
|
1109
|
-
process.exit(0);
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
217
|
+
async function launchInteractiveApp() {
|
|
218
|
+
const app = new InteractiveApp();
|
|
219
|
+
/** Create a menu onSelect handler */
|
|
220
|
+
const makeOnSelect = () => {
|
|
221
|
+
return (command) => {
|
|
222
|
+
void (async () => {
|
|
223
|
+
await recordRecent(command);
|
|
224
|
+
// CommandRunnerView handles its own suspend/resume lifecycle:
|
|
225
|
+
// - Collects args on alt screen (raw mode, no stdin transition)
|
|
226
|
+
// - Suspends only when running the command
|
|
227
|
+
// - Resumes and returns to menu after user reads output
|
|
228
|
+
const runner = new CommandRunnerView(command, app, returnToMenu);
|
|
229
|
+
await app.setView(runner);
|
|
230
|
+
})();
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
/** Navigate back to the menu from any view */
|
|
234
|
+
const returnToMenu = () => {
|
|
235
|
+
const menu = new MenuView(makeOnSelect());
|
|
236
|
+
void app.setView(menu);
|
|
237
|
+
};
|
|
238
|
+
// Start with the menu
|
|
239
|
+
const initialMenu = new MenuView(makeOnSelect());
|
|
240
|
+
await app.setView(initialMenu);
|
|
241
|
+
await app.run();
|
|
1112
242
|
}
|
|
243
|
+
// ============================================================================
|
|
1113
244
|
// Parse and execute
|
|
1114
|
-
//
|
|
245
|
+
// ============================================================================
|
|
1115
246
|
const args = process.argv.slice(2);
|
|
1116
247
|
const hasCommand = args.length > 0 && !args[0].startsWith("-");
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
process.stderr.write(
|
|
248
|
+
const hasGlobalFlag = args.some((a) => ["-h", "--help", "-v", "--version"].includes(a));
|
|
249
|
+
if (!hasCommand && !hasGlobalFlag && isInteractive()) {
|
|
250
|
+
// InteractiveApp renders its own banner in the header section — no pre-banner needed
|
|
251
|
+
launchInteractiveApp().catch((error) => {
|
|
252
|
+
process.stderr.write(renderError({
|
|
253
|
+
message: error instanceof Error ? error.message : String(error),
|
|
254
|
+
suggestion: "vertaa doctor",
|
|
255
|
+
exitCode: ExitCode.ERROR,
|
|
256
|
+
}) + "\n");
|
|
1122
257
|
process.exit(ExitCode.ERROR);
|
|
1123
258
|
});
|
|
1124
259
|
}
|