@vertaaux/cli 0.2.1 → 0.2.3
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/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +40 -6
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +4 -4
- package/dist/commands/client.d.ts +14 -0
- package/dist/commands/client.d.ts.map +1 -0
- package/dist/commands/client.js +362 -0
- package/dist/commands/drift.d.ts +15 -0
- package/dist/commands/drift.d.ts.map +1 -0
- package/dist/commands/drift.js +309 -0
- package/dist/commands/protect.d.ts +16 -0
- package/dist/commands/protect.d.ts.map +1 -0
- package/dist/commands/protect.js +323 -0
- package/dist/commands/report.d.ts +15 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +214 -0
- package/dist/index.js +0 -0
- package/dist/output/formats.d.ts.map +1 -1
- package/dist/output/formats.js +4 -2
- package/dist/policy/sync.d.ts +67 -0
- package/dist/policy/sync.d.ts.map +1 -0
- package/dist/policy/sync.js +147 -0
- package/dist/utils/validators.d.ts +3 -3
- package/dist/utils/validators.js +11 -11
- package/package.json +3 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report generation command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Generates consolidated multi-client reports by delegating
|
|
5
|
+
* data aggregation to the server-side consolidated report API.
|
|
6
|
+
* The CLI handles formatting only -- no data computation.
|
|
7
|
+
*
|
|
8
|
+
* Implements 46-06: CLI report command.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve API connection settings.
|
|
15
|
+
*/
|
|
16
|
+
function resolveConnection() {
|
|
17
|
+
return {
|
|
18
|
+
base: resolveApiBase(),
|
|
19
|
+
apiKey: getApiKey(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get a traffic-light status indicator for a client based on scores and policy.
|
|
24
|
+
*/
|
|
25
|
+
function getStatusIndicator(client) {
|
|
26
|
+
// Red: any URL with score < 50 or policy violations
|
|
27
|
+
const hasLowScore = client.urls.some((u) => u.overall !== null && u.overall < 50);
|
|
28
|
+
if (hasLowScore || client.policyViolations > 0) {
|
|
29
|
+
return chalk.red("\u25cf");
|
|
30
|
+
}
|
|
31
|
+
// Yellow: some URLs below threshold (< 70)
|
|
32
|
+
const hasMediumScore = client.urls.some((u) => u.overall !== null && u.overall < 70);
|
|
33
|
+
if (hasMediumScore) {
|
|
34
|
+
return chalk.yellow("\u25cf");
|
|
35
|
+
}
|
|
36
|
+
// Green: all URLs passing or above 70
|
|
37
|
+
return chalk.green("\u25cf");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get trend arrow for display.
|
|
41
|
+
*/
|
|
42
|
+
function getTrendArrow(trend) {
|
|
43
|
+
switch (trend) {
|
|
44
|
+
case "improving":
|
|
45
|
+
return chalk.green("\u2191");
|
|
46
|
+
case "declining":
|
|
47
|
+
return chalk.red("\u2193");
|
|
48
|
+
case "stable":
|
|
49
|
+
return chalk.dim("-");
|
|
50
|
+
default:
|
|
51
|
+
return chalk.dim("?");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pad a string to the right to a given width.
|
|
56
|
+
*/
|
|
57
|
+
function padRight(str, width) {
|
|
58
|
+
if (str.length >= width)
|
|
59
|
+
return str;
|
|
60
|
+
return str + " ".repeat(width - str.length);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Truncate a string to a maximum length with ellipsis.
|
|
64
|
+
*/
|
|
65
|
+
function truncate(str, maxLen) {
|
|
66
|
+
if (str.length <= maxLen)
|
|
67
|
+
return str;
|
|
68
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Format the consolidated report as human-readable output.
|
|
72
|
+
*/
|
|
73
|
+
function formatHumanReport(report) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
// Header
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(chalk.bold(`Consolidated Report: ${report.orgName}`));
|
|
78
|
+
lines.push(chalk.dim(`Generated: ${new Date(report.generatedAt).toISOString().split("T")[0]}`));
|
|
79
|
+
lines.push("");
|
|
80
|
+
// Per-client sections
|
|
81
|
+
for (const client of report.clients) {
|
|
82
|
+
const indicator = getStatusIndicator(client);
|
|
83
|
+
const avgStr = client.averageScore !== null
|
|
84
|
+
? String(client.averageScore)
|
|
85
|
+
: "n/a";
|
|
86
|
+
lines.push(`${indicator} ${chalk.bold(client.name)} (avg: ${avgStr})`);
|
|
87
|
+
// URL table
|
|
88
|
+
const urlWidth = 40;
|
|
89
|
+
const scoreWidth = 8;
|
|
90
|
+
const policyWidth = 10;
|
|
91
|
+
const trendWidth = 6;
|
|
92
|
+
const header = " " +
|
|
93
|
+
padRight("URL", urlWidth) +
|
|
94
|
+
padRight("Score", scoreWidth) +
|
|
95
|
+
padRight("Policy", policyWidth) +
|
|
96
|
+
padRight("Trend", trendWidth);
|
|
97
|
+
lines.push(chalk.dim(header));
|
|
98
|
+
lines.push(chalk.dim(" " + "-".repeat(urlWidth + scoreWidth + policyWidth + trendWidth)));
|
|
99
|
+
for (const u of client.urls) {
|
|
100
|
+
const scoreStr = u.overall !== null ? String(u.overall) : "n/a";
|
|
101
|
+
const policyStr = u.policyStatus === "passing"
|
|
102
|
+
? chalk.green("pass")
|
|
103
|
+
: u.policyStatus === "failing"
|
|
104
|
+
? chalk.red("fail")
|
|
105
|
+
: chalk.dim("none");
|
|
106
|
+
const trendStr = getTrendArrow(u.trend);
|
|
107
|
+
const row = " " +
|
|
108
|
+
padRight(truncate(u.url, urlWidth - 2), urlWidth) +
|
|
109
|
+
padRight(scoreStr, scoreWidth) +
|
|
110
|
+
padRight(policyStr, policyWidth) +
|
|
111
|
+
trendStr;
|
|
112
|
+
lines.push(row);
|
|
113
|
+
}
|
|
114
|
+
lines.push("");
|
|
115
|
+
}
|
|
116
|
+
// Summary
|
|
117
|
+
lines.push(chalk.bold("Summary"));
|
|
118
|
+
lines.push(` Clients: ${report.summary.totalClients} | URLs: ${report.summary.totalUrls} | Avg Score: ${report.summary.averageScore ?? "n/a"}`);
|
|
119
|
+
lines.push(` Policies: ${chalk.green(String(report.summary.passingPolicies) + " passing")} ${chalk.red(String(report.summary.failingPolicies) + " failing")}`);
|
|
120
|
+
lines.push("");
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format the consolidated report as CSV.
|
|
125
|
+
*/
|
|
126
|
+
function formatCsvReport(report) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
// Header
|
|
129
|
+
lines.push("Client,URL,Overall,Accessibility,Performance,UX,Policy,Trend");
|
|
130
|
+
// Data rows
|
|
131
|
+
for (const client of report.clients) {
|
|
132
|
+
for (const u of client.urls) {
|
|
133
|
+
const overall = u.overall !== null ? String(u.overall) : "";
|
|
134
|
+
const accessibility = u.scores.accessibility !== undefined
|
|
135
|
+
? String(u.scores.accessibility)
|
|
136
|
+
: "";
|
|
137
|
+
const performance = u.scores.performance !== undefined
|
|
138
|
+
? String(u.scores.performance)
|
|
139
|
+
: "";
|
|
140
|
+
const ux = u.scores.ux !== undefined ? String(u.scores.ux) : "";
|
|
141
|
+
const policy = u.policyStatus;
|
|
142
|
+
const trend = u.trend;
|
|
143
|
+
// Escape client name and URL for CSV (wrap in quotes if contains comma)
|
|
144
|
+
const escapeCsv = (s) => s.includes(",") || s.includes('"')
|
|
145
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
146
|
+
: s;
|
|
147
|
+
lines.push([
|
|
148
|
+
escapeCsv(client.name),
|
|
149
|
+
escapeCsv(u.url),
|
|
150
|
+
overall,
|
|
151
|
+
accessibility,
|
|
152
|
+
performance,
|
|
153
|
+
ux,
|
|
154
|
+
policy,
|
|
155
|
+
trend,
|
|
156
|
+
].join(","));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n") + "\n";
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Register the report command with the Commander program.
|
|
163
|
+
*/
|
|
164
|
+
export function registerReportCommand(program) {
|
|
165
|
+
program
|
|
166
|
+
.command("report")
|
|
167
|
+
.description("Generate consolidated reports")
|
|
168
|
+
.option("--clients", "Generate multi-client consolidated report")
|
|
169
|
+
.option("--client <names>", "Comma-separated client names/slugs (default: all)")
|
|
170
|
+
.option("--format <format>", "Output format: human|json|csv", "human")
|
|
171
|
+
.option("--output <path>", "Write report to file instead of stdout")
|
|
172
|
+
.action(async (options) => {
|
|
173
|
+
try {
|
|
174
|
+
// Currently only consolidated client reports are supported
|
|
175
|
+
if (!options.clients) {
|
|
176
|
+
process.stderr.write(chalk.yellow("The report command requires --clients flag.\n"));
|
|
177
|
+
process.stderr.write(chalk.dim("Usage: vertaa report --clients [--client <names>] [--format human|json|csv]\n"));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const { base, apiKey } = resolveConnection();
|
|
182
|
+
// Build API URL with optional client filter
|
|
183
|
+
let apiPath = "/reports/consolidated";
|
|
184
|
+
if (options.client) {
|
|
185
|
+
const encoded = encodeURIComponent(options.client);
|
|
186
|
+
apiPath += `?clients=${encoded}`;
|
|
187
|
+
}
|
|
188
|
+
const report = await apiRequest(base, apiPath, { method: "GET" }, apiKey);
|
|
189
|
+
// Format output
|
|
190
|
+
let output;
|
|
191
|
+
if (options.format === "json") {
|
|
192
|
+
output = JSON.stringify(report, null, 2) + "\n";
|
|
193
|
+
}
|
|
194
|
+
else if (options.format === "csv") {
|
|
195
|
+
output = formatCsvReport(report);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
output = formatHumanReport(report);
|
|
199
|
+
}
|
|
200
|
+
// Write to file or stdout
|
|
201
|
+
if (options.output) {
|
|
202
|
+
fs.writeFileSync(options.output, output, "utf-8");
|
|
203
|
+
process.stderr.write(chalk.green(`Report written to ${options.output}\n`));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
process.stdout.write(output);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/output/formats.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAM7D,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMzD,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAclF;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,EAAE,OAAO,GACnB,MAAM,
|
|
1
|
+
{"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/output/formats.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAM7D,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMzD,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAclF;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,EAAE,OAAO,GACnB,MAAM,CASR"}
|
package/dist/output/formats.js
CHANGED
|
@@ -33,9 +33,11 @@ export function validateFormat(command, format) {
|
|
|
33
33
|
return format;
|
|
34
34
|
}
|
|
35
35
|
export function resolveCommandFormat(command, explicitFormat, machineMode) {
|
|
36
|
+
// Treat "auto" as unset — let per-command defaults or machine mode decide
|
|
37
|
+
const format = explicitFormat === "auto" ? undefined : explicitFormat;
|
|
36
38
|
// Machine mode defaults to JSON unless explicitly overridden
|
|
37
|
-
if (machineMode && !
|
|
39
|
+
if (machineMode && !format) {
|
|
38
40
|
return "json";
|
|
39
41
|
}
|
|
40
|
-
return validateFormat(command,
|
|
42
|
+
return validateFormat(command, format);
|
|
41
43
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy push/pull sync module for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides bidirectional sync between local policy files and the server API.
|
|
5
|
+
* Uses the established CLI API patterns (resolveApiBase, getApiKey, apiRequest).
|
|
6
|
+
*
|
|
7
|
+
* Implements POL-10: Policy push/pull sync.
|
|
8
|
+
*/
|
|
9
|
+
import type { PolicyFile } from "@vertaaux/quality-control";
|
|
10
|
+
import type { VertaauxConfig } from "../config/schema.js";
|
|
11
|
+
export interface SyncConfig {
|
|
12
|
+
apiBase: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build sync config from CLI config and environment.
|
|
17
|
+
* Uses the established CLI pattern: resolveApiBase + getApiKey.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSyncConfig(cliConfig?: VertaauxConfig): SyncConfig;
|
|
20
|
+
export interface PushResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
policyId: string;
|
|
23
|
+
version: number;
|
|
24
|
+
created: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PullResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
policy?: PolicyFile;
|
|
30
|
+
policyId?: string;
|
|
31
|
+
version?: number;
|
|
32
|
+
name?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Push a local policy file to the server.
|
|
37
|
+
*
|
|
38
|
+
* If a policy with the given name already exists, updates it (with optional
|
|
39
|
+
* optimistic locking via expectedVersion). Otherwise creates a new policy.
|
|
40
|
+
*
|
|
41
|
+
* @param policy - The policy content to push
|
|
42
|
+
* @param config - API connection config
|
|
43
|
+
* @param options - Push options (name, description, force, etc.)
|
|
44
|
+
* @returns Push result with success status, policy ID, and version
|
|
45
|
+
*/
|
|
46
|
+
export declare function pushPolicy(policy: PolicyFile, config: SyncConfig, options: {
|
|
47
|
+
name: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
changeMessage?: string;
|
|
50
|
+
expectedVersion?: number;
|
|
51
|
+
force?: boolean;
|
|
52
|
+
}): Promise<PushResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Pull a policy from the server.
|
|
55
|
+
*
|
|
56
|
+
* Finds the policy by ID, name, or defaults to the default/first policy.
|
|
57
|
+
* Returns the full policy content for writing to a local file.
|
|
58
|
+
*
|
|
59
|
+
* @param config - API connection config
|
|
60
|
+
* @param options - Pull options (policyId or policyName)
|
|
61
|
+
* @returns Pull result with policy content and metadata
|
|
62
|
+
*/
|
|
63
|
+
export declare function pullPolicy(config: SyncConfig, options: {
|
|
64
|
+
policyId?: string;
|
|
65
|
+
policyName?: string;
|
|
66
|
+
}): Promise<PullResult>;
|
|
67
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/policy/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,cAAc,GAAG,UAAU,CAKtE;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4BD;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GACA,OAAO,CAAC,UAAU,CAAC,CA+ErB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,UAAU,CAAC,CAuDrB"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy push/pull sync module for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides bidirectional sync between local policy files and the server API.
|
|
5
|
+
* Uses the established CLI API patterns (resolveApiBase, getApiKey, apiRequest).
|
|
6
|
+
*
|
|
7
|
+
* Implements POL-10: Policy push/pull sync.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build sync config from CLI config and environment.
|
|
12
|
+
* Uses the established CLI pattern: resolveApiBase + getApiKey.
|
|
13
|
+
*/
|
|
14
|
+
export function buildSyncConfig(cliConfig) {
|
|
15
|
+
return {
|
|
16
|
+
apiBase: resolveApiBase(undefined, undefined),
|
|
17
|
+
apiKey: getApiKey(cliConfig?.apiKey),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Push a local policy file to the server.
|
|
22
|
+
*
|
|
23
|
+
* If a policy with the given name already exists, updates it (with optional
|
|
24
|
+
* optimistic locking via expectedVersion). Otherwise creates a new policy.
|
|
25
|
+
*
|
|
26
|
+
* @param policy - The policy content to push
|
|
27
|
+
* @param config - API connection config
|
|
28
|
+
* @param options - Push options (name, description, force, etc.)
|
|
29
|
+
* @returns Push result with success status, policy ID, and version
|
|
30
|
+
*/
|
|
31
|
+
export async function pushPolicy(policy, config, options) {
|
|
32
|
+
try {
|
|
33
|
+
// First, try to find existing policy by name
|
|
34
|
+
const listData = await apiRequest(config.apiBase, "/policies", { method: "GET" }, config.apiKey);
|
|
35
|
+
const existing = listData.policies?.find((p) => p.name === options.name);
|
|
36
|
+
if (existing) {
|
|
37
|
+
// Update existing policy
|
|
38
|
+
const body = {
|
|
39
|
+
content: policy,
|
|
40
|
+
changeMessage: options.changeMessage || "Pushed from CLI",
|
|
41
|
+
};
|
|
42
|
+
if (options.expectedVersion && !options.force) {
|
|
43
|
+
body.expectedVersion = options.expectedVersion;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const updated = await apiRequest(config.apiBase, `/policies/${existing.id}`, { method: "PUT", body }, config.apiKey);
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
policyId: existing.id,
|
|
50
|
+
version: updated.latestVersion || 0,
|
|
51
|
+
created: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
if (message.includes("409")) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
policyId: existing.id,
|
|
60
|
+
version: existing.latestVersion || 0,
|
|
61
|
+
created: false,
|
|
62
|
+
error: "Policy has been modified on server. Pull latest first, or use --force.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Create new policy
|
|
70
|
+
const created = await apiRequest(config.apiBase, "/policies", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: {
|
|
73
|
+
name: options.name,
|
|
74
|
+
description: options.description,
|
|
75
|
+
content: policy,
|
|
76
|
+
},
|
|
77
|
+
}, config.apiKey);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
policyId: created.id || "",
|
|
81
|
+
version: 1,
|
|
82
|
+
created: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
policyId: "",
|
|
90
|
+
version: 0,
|
|
91
|
+
created: false,
|
|
92
|
+
error: err instanceof Error ? err.message : String(err),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Pull a policy from the server.
|
|
98
|
+
*
|
|
99
|
+
* Finds the policy by ID, name, or defaults to the default/first policy.
|
|
100
|
+
* Returns the full policy content for writing to a local file.
|
|
101
|
+
*
|
|
102
|
+
* @param config - API connection config
|
|
103
|
+
* @param options - Pull options (policyId or policyName)
|
|
104
|
+
* @returns Pull result with policy content and metadata
|
|
105
|
+
*/
|
|
106
|
+
export async function pullPolicy(config, options) {
|
|
107
|
+
try {
|
|
108
|
+
let policyId = options.policyId;
|
|
109
|
+
// If no ID, find by name or get default
|
|
110
|
+
if (!policyId) {
|
|
111
|
+
const listData = await apiRequest(config.apiBase, "/policies", { method: "GET" }, config.apiKey);
|
|
112
|
+
if (options.policyName) {
|
|
113
|
+
const found = listData.policies?.find((p) => p.name === options.policyName);
|
|
114
|
+
if (!found) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: `Policy "${options.policyName}" not found on server`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
policyId = found.id;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const defaultPolicy = listData.policies?.find((p) => p.isDefault);
|
|
124
|
+
const target = defaultPolicy || listData.policies?.[0];
|
|
125
|
+
if (!target) {
|
|
126
|
+
return { success: false, error: "No policies found on server" };
|
|
127
|
+
}
|
|
128
|
+
policyId = target.id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Fetch full policy with content
|
|
132
|
+
const data = await apiRequest(config.apiBase, `/policies/${policyId}`, { method: "GET" }, config.apiKey);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
policy: data.content,
|
|
136
|
+
policyId: data.id || policyId,
|
|
137
|
+
version: data.latestVersion,
|
|
138
|
+
name: data.name,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: err instanceof Error ? err.message : String(err),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure validation functions for CLI option parsing.
|
|
3
3
|
*
|
|
4
|
-
* All validators throw Commander's
|
|
4
|
+
* All validators throw Commander's InvalidOptionArgumentError on failure,
|
|
5
5
|
* which integrates with Commander's built-in error handling pipeline.
|
|
6
6
|
* No process.exit calls -- callers decide error handling.
|
|
7
7
|
*
|
|
@@ -19,14 +19,14 @@ export interface NumericConstraint {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Validate and parse a numeric string value.
|
|
22
|
-
* Throws
|
|
22
|
+
* Throws InvalidOptionArgumentError with descriptive message on failure.
|
|
23
23
|
*
|
|
24
24
|
* Uses Number() (not parseInt) to reject partial parses like "12abc".
|
|
25
25
|
*/
|
|
26
26
|
export declare function validateNumeric(value: string, name: string, constraints?: NumericConstraint): number;
|
|
27
27
|
/**
|
|
28
28
|
* Validate a string value against an allowed set.
|
|
29
|
-
* Throws
|
|
29
|
+
* Throws InvalidOptionArgumentError with "Did you mean?" suggestion on typos.
|
|
30
30
|
*/
|
|
31
31
|
export declare function validateEnum(value: string, name: string, allowed: readonly string[]): string;
|
|
32
32
|
export declare function parseTimeout(value: string): number;
|
package/dist/utils/validators.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure validation functions for CLI option parsing.
|
|
3
3
|
*
|
|
4
|
-
* All validators throw Commander's
|
|
4
|
+
* All validators throw Commander's InvalidOptionArgumentError on failure,
|
|
5
5
|
* which integrates with Commander's built-in error handling pipeline.
|
|
6
6
|
* No process.exit calls -- callers decide error handling.
|
|
7
7
|
*
|
|
8
8
|
* Includes Levenshtein distance for "Did you mean?" suggestions on enum typos.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { InvalidOptionArgumentError } from "commander";
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Levenshtein distance (private)
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -52,26 +52,26 @@ export function closestMatch(input, candidates, maxDistance = 3) {
|
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Validate and parse a numeric string value.
|
|
55
|
-
* Throws
|
|
55
|
+
* Throws InvalidOptionArgumentError with descriptive message on failure.
|
|
56
56
|
*
|
|
57
57
|
* Uses Number() (not parseInt) to reject partial parses like "12abc".
|
|
58
58
|
*/
|
|
59
59
|
export function validateNumeric(value, name, constraints = {}) {
|
|
60
60
|
if (value.trim() === "") {
|
|
61
|
-
throw new
|
|
61
|
+
throw new InvalidOptionArgumentError(`Invalid value for --${name}: "${value}" is not a number`);
|
|
62
62
|
}
|
|
63
63
|
const parsed = Number(value);
|
|
64
64
|
if (Number.isNaN(parsed)) {
|
|
65
|
-
throw new
|
|
65
|
+
throw new InvalidOptionArgumentError(`Invalid value for --${name}: "${value}" is not a number`);
|
|
66
66
|
}
|
|
67
67
|
if (constraints.integer && !Number.isInteger(parsed)) {
|
|
68
|
-
throw new
|
|
68
|
+
throw new InvalidOptionArgumentError(`Invalid value for --${name}: "${value}" must be an integer`);
|
|
69
69
|
}
|
|
70
70
|
if (constraints.min !== undefined && parsed < constraints.min) {
|
|
71
|
-
throw new
|
|
71
|
+
throw new InvalidOptionArgumentError(`Invalid value for --${name}: ${parsed} is below minimum ${constraints.min}`);
|
|
72
72
|
}
|
|
73
73
|
if (constraints.max !== undefined && parsed > constraints.max) {
|
|
74
|
-
throw new
|
|
74
|
+
throw new InvalidOptionArgumentError(`Invalid value for --${name}: ${parsed} exceeds maximum ${constraints.max}`);
|
|
75
75
|
}
|
|
76
76
|
return parsed;
|
|
77
77
|
}
|
|
@@ -80,20 +80,20 @@ export function validateNumeric(value, name, constraints = {}) {
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
81
81
|
/**
|
|
82
82
|
* Validate a string value against an allowed set.
|
|
83
|
-
* Throws
|
|
83
|
+
* Throws InvalidOptionArgumentError with "Did you mean?" suggestion on typos.
|
|
84
84
|
*/
|
|
85
85
|
export function validateEnum(value, name, allowed) {
|
|
86
86
|
if (allowed.includes(value))
|
|
87
87
|
return value;
|
|
88
88
|
const suggestion = closestMatch(value, allowed);
|
|
89
89
|
const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
|
|
90
|
-
throw new
|
|
90
|
+
throw new InvalidOptionArgumentError(`Unknown value "${value}" for --${name}.${hint} Valid values: ${allowed.join(", ")}`);
|
|
91
91
|
}
|
|
92
92
|
// ---------------------------------------------------------------------------
|
|
93
93
|
// Convenience Commander parser factories
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
95
|
// These are used as the 3rd argument to Commander's .option() method.
|
|
96
|
-
// Commander catches
|
|
96
|
+
// Commander catches InvalidOptionArgumentError and routes it through configureOutput.
|
|
97
97
|
export function parseTimeout(value) {
|
|
98
98
|
return validateNumeric(value, "timeout", { min: 1, max: 300000, integer: true });
|
|
99
99
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertaaux/cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"url": "https://github.com/PetriLahdelma/vertaa",
|
|
28
28
|
"directory": "cli"
|
|
29
29
|
},
|
|
30
|
-
"homepage": "https://
|
|
30
|
+
"homepage": "https://vertaaux.ai",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|