@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.
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CA6B3B"}
@@ -50,6 +50,7 @@ export async function startDeviceFlow(clientId, authBase = DEFAULT_AUTH_BASE) {
50
50
  console.log(` Or open: ${deviceCodeResponse.verification_uri_complete}`);
51
51
  console.log("\n");
52
52
  }
53
+ console.log(" Press Ctrl+C to cancel.\n");
53
54
  // Step 3: Poll for token with countdown
54
55
  const tokens = await pollForToken(clientId, deviceCodeResponse.device_code, deviceCodeResponse.interval, deviceCodeResponse.expires_in, authBase);
55
56
  return tokens;
@@ -83,6 +84,12 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
83
84
  const startTime = Date.now();
84
85
  const timeoutMs = Math.min(expiresInSeconds, DEFAULT_TIMEOUT_SECONDS) * 1000;
85
86
  let interval = intervalSeconds * 1000; // Convert to milliseconds
87
+ let cancelled = false;
88
+ // Handle Ctrl+C gracefully
89
+ const onSigint = () => {
90
+ cancelled = true;
91
+ };
92
+ process.on("SIGINT", onSigint);
86
93
  // Start spinner with countdown
87
94
  const spinner = ora({
88
95
  text: `Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`,
@@ -90,6 +97,9 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
90
97
  }).start();
91
98
  try {
92
99
  while (true) {
100
+ if (cancelled) {
101
+ throw new Error("Login cancelled.");
102
+ }
93
103
  // Check timeout
94
104
  const elapsed = Date.now() - startTime;
95
105
  const remaining = Math.max(0, timeoutMs - elapsed);
@@ -98,8 +108,11 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
98
108
  }
99
109
  // Update spinner with countdown
100
110
  spinner.text = `Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`;
101
- // Wait for poll interval
102
- await sleep(interval);
111
+ // Wait for poll interval (cancellable)
112
+ await sleep(interval, () => cancelled);
113
+ if (cancelled) {
114
+ throw new Error("Login cancelled.");
115
+ }
103
116
  // Poll token endpoint
104
117
  const response = await fetch(url, {
105
118
  method: "POST",
@@ -142,15 +155,36 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
142
155
  }
143
156
  }
144
157
  finally {
145
- // Ensure spinner is stopped
158
+ process.removeListener("SIGINT", onSigint);
146
159
  if (spinner.isSpinning) {
147
160
  spinner.stop();
148
161
  }
149
162
  }
150
163
  }
151
164
  /**
152
- * Sleep for specified milliseconds.
165
+ * Sleep for specified milliseconds, with early cancellation support.
153
166
  */
154
- function sleep(ms) {
155
- return new Promise((resolve) => setTimeout(resolve, ms));
167
+ function sleep(ms, isCancelled) {
168
+ return new Promise((resolve) => {
169
+ if (isCancelled?.()) {
170
+ resolve();
171
+ return;
172
+ }
173
+ let check;
174
+ const timer = setTimeout(() => {
175
+ if (check)
176
+ clearInterval(check);
177
+ resolve();
178
+ }, ms);
179
+ // Check cancellation every 200ms to respond quickly to Ctrl+C
180
+ if (isCancelled) {
181
+ check = setInterval(() => {
182
+ if (isCancelled()) {
183
+ clearTimeout(timer);
184
+ clearInterval(check);
185
+ resolve();
186
+ }
187
+ }, 200);
188
+ }
189
+ });
156
190
  }
@@ -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;AA8EpC,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,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,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA4tBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiT3D"}
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;AA+EpC,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,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,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2tBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiT3D"}
@@ -14,6 +14,7 @@ import { resolveApiBase, getApiKey, apiRequest, waitForAudit, } from "../utils/c
14
14
  import { createOutput, formatSarif, formatAuditHtml } from "../output/factory.js";
15
15
  import { createEnvelope, writeJsonOutput, writeOutput as writeStdout } from "../output/envelope.js";
16
16
  import { resolveCommandFormat } from "../output/formats.js";
17
+ import { getVersion } from "../ui/banner.js";
17
18
  import { createSpinner, updateSpinner, succeedSpinner, failSpinner, } from "../ui/spinner.js";
18
19
  import { runFixWizard } from "../interactive/fix-wizard.js";
19
20
  import { isInteractive } from "../interactive/prompts.js";
@@ -29,7 +30,6 @@ import semver from "semver";
29
30
  // Artifact directory
30
31
  const ARTIFACTS_DIR = ".vertaaux/artifacts";
31
32
  // CLI version for policy version requirements (read from package.json)
32
- import { getVersion } from "../ui/banner.js";
33
33
  const CLI_VERSION = getVersion();
34
34
  /**
35
35
  * Detect current branch from CI environment or git.
@@ -403,7 +403,7 @@ async function executeAudit(targetUrl, options, config) {
403
403
  if (format === "json") {
404
404
  if (options.output) {
405
405
  const output = JSON.stringify(createEnvelope(created, "audit"), null, 2);
406
- const filePath = writeOutputToFile(output, options.output, undefined);
406
+ const filePath = writeOutputToFile(output, options.output);
407
407
  if (filePath && !quiet) {
408
408
  console.error(`Report written to: ${filePath}`);
409
409
  }
@@ -528,7 +528,7 @@ async function executeAudit(targetUrl, options, config) {
528
528
  if (format === "json") {
529
529
  if (options.output) {
530
530
  const jsonStr = JSON.stringify(createEnvelope(filteredResult, "audit"), null, 2);
531
- const filePath = writeOutputToFile(jsonStr, options.output, undefined);
531
+ const filePath = writeOutputToFile(jsonStr, options.output);
532
532
  if (filePath && !quiet) {
533
533
  console.error(`Report written to: ${filePath}`);
534
534
  }
@@ -615,7 +615,7 @@ export function registerAuditCommand(program) {
615
615
  .option("--routes <routes>", "Comma-separated list of routes to audit")
616
616
  .option("--auth-profile <profile>", "Authentication profile for protected pages")
617
617
  .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
618
- .option("--format <format>", "Output format: json|sarif|junit|html|human|auto")
618
+ .option("--format <format>", "Output format: json|sarif|junit|html|human (default: human in terminal, auto-detected in CI)")
619
619
  .option("-o, --output <path>", "Output file path")
620
620
  .option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
621
621
  .option("--wait", "Wait for audit completion (default)")
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Client management commands for VertaaUX CLI.
3
+ *
4
+ * Provides agency client CRUD operations and batch auditing
5
+ * across client URL portfolios with bounded concurrency.
6
+ *
7
+ * Implements 46-06: CLI client management and batch audit.
8
+ */
9
+ import { Command } from "commander";
10
+ /**
11
+ * Register the client command with the Commander program.
12
+ */
13
+ export declare function registerClientCommand(program: Command): void;
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/commands/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0HpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwX5D"}
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Client management commands for VertaaUX CLI.
3
+ *
4
+ * Provides agency client CRUD operations and batch auditing
5
+ * across client URL portfolios with bounded concurrency.
6
+ *
7
+ * Implements 46-06: CLI client management and batch audit.
8
+ */
9
+ import chalk from "chalk";
10
+ import pLimit from "p-limit";
11
+ import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
12
+ /**
13
+ * Auto-generate a URL-friendly slug from a name.
14
+ */
15
+ function generateSlug(name) {
16
+ return name
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, "-")
19
+ .replace(/^-|-$/g, "");
20
+ }
21
+ /**
22
+ * Determine if an identifier looks like a CUID (starts with 'c' followed by alphanumerics).
23
+ */
24
+ function looksLikeCuid(identifier) {
25
+ return /^c[a-z0-9]{20,}$/i.test(identifier);
26
+ }
27
+ /**
28
+ * Resolve API connection settings.
29
+ * Reads from environment variables (standard CLI pattern).
30
+ */
31
+ function resolveConnection() {
32
+ return {
33
+ base: resolveApiBase(),
34
+ apiKey: getApiKey(),
35
+ };
36
+ }
37
+ /**
38
+ * Format a date string for display.
39
+ */
40
+ function formatDate(dateStr) {
41
+ try {
42
+ const d = new Date(dateStr);
43
+ return d.toISOString().split("T")[0];
44
+ }
45
+ catch {
46
+ return dateStr;
47
+ }
48
+ }
49
+ /**
50
+ * Wait for an audit to complete by polling the status endpoint.
51
+ */
52
+ async function waitForAuditCompletion(base, apiKey, jobId, timeoutMs = 120000, intervalMs = 3000) {
53
+ const start = Date.now();
54
+ while (true) {
55
+ const status = await apiRequest(base, `/audit/${jobId}`, { method: "GET" }, apiKey);
56
+ if (status.status === "completed")
57
+ return status;
58
+ if (status.status === "failed") {
59
+ throw new Error(status.error || `Audit ${jobId} failed`);
60
+ }
61
+ if (Date.now() - start > timeoutMs) {
62
+ throw new Error(`Timed out waiting for audit ${jobId}`);
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
65
+ }
66
+ }
67
+ /**
68
+ * Compute overall score as mean of category scores.
69
+ */
70
+ function computeOverallScore(scores) {
71
+ const values = Object.values(scores);
72
+ if (values.length === 0)
73
+ return null;
74
+ const sum = values.reduce((acc, v) => acc + v, 0);
75
+ return Math.round((sum / values.length) * 100) / 100;
76
+ }
77
+ /**
78
+ * Register the client command with the Commander program.
79
+ */
80
+ export function registerClientCommand(program) {
81
+ const clientCmd = program
82
+ .command("client")
83
+ .description("Manage agency clients");
84
+ // vertaa client create <name>
85
+ clientCmd
86
+ .command("create <name>")
87
+ .description("Create a new client")
88
+ .option("--slug <slug>", "URL-friendly identifier (auto-generated from name if omitted)")
89
+ .option("--urls <urls...>", "URLs to associate with this client")
90
+ .option("--policy <id>", "Policy ID to assign")
91
+ .action(async (name, options) => {
92
+ try {
93
+ const { base, apiKey } = resolveConnection();
94
+ const slug = options.slug || generateSlug(name);
95
+ const body = { name, slug };
96
+ if (options.urls && options.urls.length > 0) {
97
+ body.urls = options.urls;
98
+ }
99
+ if (options.policy) {
100
+ body.policyId = options.policy;
101
+ }
102
+ const client = await apiRequest(base, "/clients", { method: "POST", body }, apiKey);
103
+ process.stderr.write(chalk.green(`Client created successfully\n\n`));
104
+ process.stderr.write(` Name: ${client.name}\n`);
105
+ process.stderr.write(` Slug: ${client.slug}\n`);
106
+ process.stderr.write(` ID: ${client.id}\n`);
107
+ if (client.urls.length > 0) {
108
+ process.stderr.write(` URLs: ${client.urls.join(", ")}\n`);
109
+ }
110
+ if (client.policy) {
111
+ process.stderr.write(` Policy: ${client.policy.name}\n`);
112
+ }
113
+ process.stderr.write("\n");
114
+ }
115
+ catch (error) {
116
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
117
+ process.exit(1);
118
+ }
119
+ });
120
+ // vertaa client list
121
+ clientCmd
122
+ .command("list")
123
+ .description("List all clients")
124
+ .option("--format <format>", "Output format: human|json", "human")
125
+ .action(async (options) => {
126
+ try {
127
+ const { base, apiKey } = resolveConnection();
128
+ const clients = await apiRequest(base, "/clients", { method: "GET" }, apiKey);
129
+ if (options.format === "json") {
130
+ process.stdout.write(JSON.stringify(clients, null, 2) + "\n");
131
+ return;
132
+ }
133
+ // Human format: table
134
+ if (clients.length === 0) {
135
+ process.stderr.write(chalk.dim("No clients found. Create one with: vertaa client create <name>\n"));
136
+ return;
137
+ }
138
+ // Table header
139
+ const nameWidth = 20;
140
+ const slugWidth = 20;
141
+ const urlsWidth = 8;
142
+ const policyWidth = 20;
143
+ const updatedWidth = 12;
144
+ const header = padRight("Name", nameWidth) +
145
+ padRight("Slug", slugWidth) +
146
+ padRight("URLs", urlsWidth) +
147
+ padRight("Policy", policyWidth) +
148
+ padRight("Updated", updatedWidth);
149
+ process.stderr.write("\n");
150
+ process.stderr.write(chalk.bold(header) + "\n");
151
+ process.stderr.write(chalk.dim("-".repeat(nameWidth + slugWidth + urlsWidth + policyWidth + updatedWidth)) + "\n");
152
+ for (const client of clients) {
153
+ const row = padRight(truncate(client.name, nameWidth - 2), nameWidth) +
154
+ padRight(truncate(client.slug, slugWidth - 2), slugWidth) +
155
+ padRight(String(client.urls.length), urlsWidth) +
156
+ padRight(truncate(client.policy?.name || "-", policyWidth - 2), policyWidth) +
157
+ padRight(formatDate(client.updatedAt), updatedWidth);
158
+ process.stderr.write(row + "\n");
159
+ }
160
+ process.stderr.write("\n");
161
+ process.stderr.write(chalk.dim(`${clients.length} client${clients.length !== 1 ? "s" : ""}\n`));
162
+ }
163
+ catch (error) {
164
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
165
+ process.exit(1);
166
+ }
167
+ });
168
+ // vertaa client remove <identifier>
169
+ clientCmd
170
+ .command("remove <identifier>")
171
+ .description("Remove a client")
172
+ .option("--force", "Skip confirmation")
173
+ .action(async (identifier, options) => {
174
+ try {
175
+ const { base, apiKey } = resolveConnection();
176
+ // Resolve the client: try ID first, then slug lookup
177
+ let clientId;
178
+ let clientName;
179
+ let urlCount;
180
+ if (looksLikeCuid(identifier)) {
181
+ // Looks like an ID, use directly but verify it exists
182
+ const client = await apiRequest(base, `/clients/${identifier}`, { method: "GET" }, apiKey);
183
+ clientId = client.id;
184
+ clientName = client.name;
185
+ urlCount = client.urls.length;
186
+ }
187
+ else {
188
+ // Treat as slug -- look up in the list
189
+ const clients = await apiRequest(base, "/clients", { method: "GET" }, apiKey);
190
+ const match = clients.find((c) => c.slug === identifier || c.name === identifier);
191
+ if (!match) {
192
+ process.stderr.write(chalk.red(`Error: Client "${identifier}" not found.\n`));
193
+ process.exit(1);
194
+ return;
195
+ }
196
+ clientId = match.id;
197
+ clientName = match.name;
198
+ urlCount = match.urls.length;
199
+ }
200
+ // Confirmation gate
201
+ if (!options.force) {
202
+ process.stderr.write(chalk.yellow(`About to delete client '${clientName}' with ${urlCount} URL${urlCount !== 1 ? "s" : ""}. This cannot be undone.\n`));
203
+ process.stderr.write(chalk.dim("Add --force to confirm deletion.\n"));
204
+ process.exit(1);
205
+ return;
206
+ }
207
+ await apiRequest(base, `/clients/${clientId}`, { method: "DELETE" }, apiKey);
208
+ process.stderr.write(chalk.green(`Client '${clientName}' removed.\n`));
209
+ }
210
+ catch (error) {
211
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
212
+ process.exit(1);
213
+ }
214
+ });
215
+ // vertaa client audit
216
+ clientCmd
217
+ .command("audit")
218
+ .description("Run batch audit across client URLs")
219
+ .option("--client <names>", "Comma-separated client names/slugs (default: all)")
220
+ .option("--concurrency <n>", "Concurrent audits", "3")
221
+ .option("--format <format>", "Output format: human|json", "human")
222
+ .action(async (options) => {
223
+ try {
224
+ const { base, apiKey } = resolveConnection();
225
+ // Fetch all clients
226
+ const allClients = await apiRequest(base, "/clients", { method: "GET" }, apiKey);
227
+ if (allClients.length === 0) {
228
+ process.stderr.write(chalk.dim("No clients found. Create one with: vertaa client create <name>\n"));
229
+ return;
230
+ }
231
+ // Filter by --client if provided
232
+ let selectedClients;
233
+ if (options.client) {
234
+ const names = options.client.split(",").map((n) => n.trim().toLowerCase());
235
+ selectedClients = allClients.filter((c) => names.includes(c.slug.toLowerCase()) ||
236
+ names.includes(c.name.toLowerCase()));
237
+ if (selectedClients.length === 0) {
238
+ process.stderr.write(chalk.red(`Error: No matching clients found for: ${options.client}\n`));
239
+ process.stderr.write(chalk.dim(`Available: ${allClients.map((c) => c.slug).join(", ")}\n`));
240
+ process.exit(1);
241
+ return;
242
+ }
243
+ }
244
+ else {
245
+ selectedClients = allClients;
246
+ }
247
+ // Flatten all URLs
248
+ const urlTasks = [];
249
+ for (const client of selectedClients) {
250
+ for (const url of client.urls) {
251
+ urlTasks.push({ client, url });
252
+ }
253
+ }
254
+ if (urlTasks.length === 0) {
255
+ process.stderr.write(chalk.dim("No URLs to audit across selected clients.\n"));
256
+ return;
257
+ }
258
+ const concurrency = parseInt(options.concurrency, 10) || 3;
259
+ const limit = pLimit(concurrency);
260
+ process.stderr.write(chalk.dim(`Auditing ${urlTasks.length} URL${urlTasks.length !== 1 ? "s" : ""} across ${selectedClients.length} client${selectedClients.length !== 1 ? "s" : ""} (concurrency: ${concurrency})\n\n`));
261
+ // Run batch audits with bounded concurrency
262
+ const results = [];
263
+ let completed = 0;
264
+ const promises = urlTasks.map(({ client, url }) => limit(async () => {
265
+ completed++;
266
+ process.stderr.write(`[${completed}/${urlTasks.length}] Auditing ${url}...`);
267
+ try {
268
+ // Start audit
269
+ const job = await apiRequest(base, "/audit", { method: "POST", body: { url, mode: "basic" } }, apiKey);
270
+ if (!job.job_id) {
271
+ throw new Error("No job_id in audit response");
272
+ }
273
+ // Wait for completion
274
+ const result = await waitForAuditCompletion(base, apiKey, job.job_id);
275
+ const scores = (result.scores || {});
276
+ const overall = computeOverallScore(scores);
277
+ process.stderr.write(` ${chalk.green("done")} (score: ${overall !== null ? overall : "n/a"})\n`);
278
+ results.push({
279
+ client: client.name,
280
+ url,
281
+ score: overall,
282
+ status: "success",
283
+ jobId: job.job_id,
284
+ });
285
+ }
286
+ catch (err) {
287
+ process.stderr.write(` ${chalk.red("failed")}\n`);
288
+ results.push({
289
+ client: client.name,
290
+ url,
291
+ score: null,
292
+ status: "error",
293
+ error: err instanceof Error ? err.message : String(err),
294
+ });
295
+ }
296
+ }));
297
+ await Promise.all(promises);
298
+ // Output results
299
+ process.stderr.write("\n");
300
+ if (options.format === "json") {
301
+ // Group by client for JSON output
302
+ const grouped = {};
303
+ for (const r of results) {
304
+ if (!grouped[r.client])
305
+ grouped[r.client] = [];
306
+ grouped[r.client].push(r);
307
+ }
308
+ process.stdout.write(JSON.stringify(grouped, null, 2) + "\n");
309
+ return;
310
+ }
311
+ // Human format: per-client summary
312
+ const grouped = {};
313
+ for (const r of results) {
314
+ if (!grouped[r.client])
315
+ grouped[r.client] = [];
316
+ grouped[r.client].push(r);
317
+ }
318
+ for (const [clientName, clientResults] of Object.entries(grouped)) {
319
+ process.stderr.write(chalk.bold(`${clientName}\n`));
320
+ for (const r of clientResults) {
321
+ const scoreStr = r.status === "success"
322
+ ? r.score !== null
323
+ ? String(r.score)
324
+ : "n/a"
325
+ : chalk.red("error");
326
+ process.stderr.write(` ${r.url} ${scoreStr}\n`);
327
+ }
328
+ // Client average
329
+ const successScores = clientResults
330
+ .filter((r) => r.status === "success" && r.score !== null)
331
+ .map((r) => r.score);
332
+ if (successScores.length > 0) {
333
+ const avg = Math.round((successScores.reduce((a, b) => a + b, 0) / successScores.length) * 100) / 100;
334
+ process.stderr.write(chalk.dim(` Average: ${avg}\n`));
335
+ }
336
+ process.stderr.write("\n");
337
+ }
338
+ // Overall summary
339
+ const totalSuccess = results.filter((r) => r.status === "success").length;
340
+ const totalFailed = results.filter((r) => r.status === "error").length;
341
+ process.stderr.write(chalk.dim(`Summary: ${totalSuccess} succeeded, ${totalFailed} failed out of ${results.length} audits\n`));
342
+ }
343
+ catch (error) {
344
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
345
+ process.exit(1);
346
+ }
347
+ });
348
+ }
349
+ /**
350
+ * Pad a string to the right to a given width.
351
+ */
352
+ function padRight(str, width) {
353
+ return str.length >= width ? str : str + " ".repeat(width - str.length);
354
+ }
355
+ /**
356
+ * Truncate a string to a maximum length with ellipsis.
357
+ */
358
+ function truncate(str, maxLen) {
359
+ if (str.length <= maxLen)
360
+ return str;
361
+ return str.slice(0, maxLen - 1) + "\u2026";
362
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Drift detection command for VertaaUX CLI.
3
+ *
4
+ * Compares current audit scores against a baseline and reports
5
+ * per-category regressions with delta magnitudes. Used in local
6
+ * workflows and CI pipelines to detect score regressions before merging.
7
+ *
8
+ * Implements DRIFT-01: CLI drift check command.
9
+ */
10
+ import { Command } from "commander";
11
+ /**
12
+ * Register the drift command with the Commander program.
13
+ */
14
+ export declare function registerDriftCommand(program: Command): void;
15
+ //# sourceMappingURL=drift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../src/commands/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyNpC;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsK3D"}