Skip to content

Commit e951500

Browse files
committed
Improve output format, fix oauth
1 parent d19f264 commit e951500

4 files changed

Lines changed: 180 additions & 90 deletions

File tree

src/commands/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export async function auth(options: AuthOptions): Promise<void> {
3131
authUrl.searchParams.set("code_challenge", codeChallenge);
3232
authUrl.searchParams.set("code_challenge_method", "S256");
3333
authUrl.searchParams.set("state", state);
34+
authUrl.searchParams.set("scope", "projects:read specs:read specs:write");
3435

3536
console.log("\nOpening browser for authentication...");
3637
console.log(`If browser doesn't open, visit:\n${authUrl.toString()}\n`);

src/commands/evaluate.ts

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getAccessToken } from "../config.js";
22
import { readAndParseSpec, uploadSpec, parseProject } from "../api.js";
3+
import { printViolations, printReportUrl } from "../format.js";
34

45
interface EvaluateOptions {
56
project: string;
@@ -75,53 +76,7 @@ export async function evaluate(file: string, options: EvaluateOptions): Promise<
7576

7677
const violationsResult = await violationsResponse.json();
7778
printViolations(violationsResult);
78-
}
79-
80-
function printViolations(result: {
81-
violations?: Array<{
82-
key: { path?: string; operation_id?: string; schema_path?: string };
83-
value: Array<{ message: string; severity: string; rule_id: number }>;
84-
}>;
85-
}): void {
86-
const violations = result.violations || [];
87-
88-
// Count total violations
89-
let total = 0;
90-
for (const group of violations) {
91-
total += group.value.length;
92-
}
93-
94-
if (total === 0) {
95-
console.log("No violations found!");
96-
return;
97-
}
98-
99-
console.log(`Found ${total} violation(s):\n`);
100-
101-
let errorCount = 0;
102-
let warningCount = 0;
103-
let infoCount = 0;
104-
105-
for (const group of violations) {
106-
const location = group.key.path || group.key.schema_path || "(global)";
107-
108-
for (const v of group.value) {
109-
const severityIcon =
110-
v.severity === "error" ? "\x1b[31m\u2717\x1b[0m" :
111-
v.severity === "warning" ? "\x1b[33m\u26a0\x1b[0m" :
112-
"\x1b[34m\u2139\x1b[0m";
113-
114-
if (v.severity === "error") errorCount++;
115-
else if (v.severity === "warning") warningCount++;
116-
else infoCount++;
117-
118-
console.log(`${severityIcon} [Rule ${v.rule_id}] ${location}`);
119-
console.log(` ${v.message}\n`);
120-
}
121-
}
122-
123-
console.log("---");
124-
console.log(`Summary: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
79+
printReportUrl(server, orgSlug, projectName, specId);
12580
}
12681

12782
async function waitForEvaluation(

src/commands/violations.ts

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getAccessToken } from "../config.js";
2+
import { printViolations } from "../format.js";
23

34
interface ViolationsOptions {
45
project: string;
@@ -71,46 +72,3 @@ export async function violations(options: ViolationsOptions): Promise<void> {
7172
const result = await violationsResponse.json();
7273
printViolations(result);
7374
}
74-
75-
function printViolations(result: {
76-
violations?: Array<{
77-
key: { path?: string; operation_id?: string; schema_path?: string };
78-
value: Array<{ message: string; severity: string; rule_id: number }>;
79-
}>;
80-
totalViolations?: number;
81-
}): void {
82-
const violations = result.violations || [];
83-
const total = result.totalViolations || 0;
84-
85-
if (total === 0) {
86-
console.log("No violations found!");
87-
return;
88-
}
89-
90-
console.log(`Found ${total} violation(s):\n`);
91-
92-
let errorCount = 0;
93-
let warningCount = 0;
94-
let infoCount = 0;
95-
96-
for (const group of violations) {
97-
const location = group.key.path || group.key.schema_path || "(global)";
98-
99-
for (const v of group.value) {
100-
const severityIcon =
101-
v.severity === "error" ? "\x1b[31m\u2717\x1b[0m" :
102-
v.severity === "warning" ? "\x1b[33m\u26a0\x1b[0m" :
103-
"\x1b[34m\u2139\x1b[0m";
104-
105-
if (v.severity === "error") errorCount++;
106-
else if (v.severity === "warning") warningCount++;
107-
else infoCount++;
108-
109-
console.log(`${severityIcon} [Rule ${v.rule_id}] ${location}`);
110-
console.log(` ${v.message}\n`);
111-
}
112-
}
113-
114-
console.log("---");
115-
console.log(`Summary: ${errorCount} errors, ${warningCount} warnings, ${infoCount} info`);
116-
}

src/format.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* Terminal output formatting for REST Lens CLI.
3+
*
4+
* Produces clean, colored output like:
5+
*
6+
* error Rule 05: POST/GET tunneling detected /users/delete
7+
* warn Rule 01: Singular collection name /order
8+
* ──────────────────────────────────────────────────
9+
* 2 errors 3 warnings 1 info
10+
*/
11+
12+
// ── ANSI helpers ───────────────────────────────────────
13+
const esc = (code: string) => `\x1b[${code}m`;
14+
const reset = esc("0");
15+
16+
const c = {
17+
red: (s: string) => `${esc("31")}${s}${reset}`,
18+
yellow: (s: string) => `${esc("33")}${s}${reset}`,
19+
blue: (s: string) => `${esc("34")}${s}${reset}`,
20+
green: (s: string) => `${esc("32")}${s}${reset}`,
21+
dim: (s: string) => `${esc("2")}${s}${reset}`,
22+
bold: (s: string) => `${esc("1")}${s}${reset}`,
23+
cyan: (s: string) => `${esc("36")}${s}${reset}`,
24+
};
25+
26+
// ── Types ──────────────────────────────────────────────
27+
interface Violation {
28+
message: string;
29+
severity: string;
30+
rule_id: number;
31+
rule_slug?: string;
32+
}
33+
34+
// Array format (old): [{ key: {...}, value: Violation[] }]
35+
interface ViolationGroupArray {
36+
key: { path?: string; operation_id?: string; schema_path?: string };
37+
value: Violation[];
38+
}
39+
40+
// Object format (violations-service): { path: { "/foo": { key, violations } }, ... }
41+
interface ViolationGroupObject {
42+
key: { violation_key_type?: string; path?: string; operation_id?: string; schema_path?: string };
43+
violations: Violation[];
44+
}
45+
46+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47+
interface ViolationsResult {
48+
violations?: ViolationGroupArray[] | Record<string, Record<string, ViolationGroupObject>>;
49+
totalViolations?: number;
50+
}
51+
52+
interface FlatViolation {
53+
severity: string;
54+
ruleLabel: string;
55+
message: string;
56+
location: string;
57+
}
58+
59+
// ── Formatting ─────────────────────────────────────────
60+
61+
function severityLabel(severity: string): string {
62+
const labelMap: Record<string, string> = {
63+
error: c.red("error"),
64+
warning: c.yellow("warn "),
65+
info: c.blue("info "),
66+
};
67+
return labelMap[severity] ?? c.dim(severity.padEnd(5));
68+
}
69+
70+
function ruleLabel(ruleId: number, ruleSlug?: string): string {
71+
if (ruleSlug) return c.bold(ruleSlug);
72+
return c.bold(`Rule ${String(ruleId).padStart(2, "0")}`);
73+
}
74+
75+
function pad(str: string, len: number): string {
76+
const visible = str.replace(/\x1b\[[0-9;]*m/g, "");
77+
const diff = len - visible.length;
78+
return diff > 0 ? str + " ".repeat(diff) : str;
79+
}
80+
81+
function flatten(result: ViolationsResult): FlatViolation[] {
82+
const flat: FlatViolation[] = [];
83+
const violations = result.violations;
84+
if (!violations) return flat;
85+
86+
if (Array.isArray(violations)) {
87+
// Array format: [{ key, value: Violation[] }]
88+
for (const group of violations) {
89+
const location = group.key.path || group.key.schema_path || group.key.operation_id || "";
90+
for (const v of group.value) {
91+
flat.push({
92+
severity: v.severity,
93+
ruleLabel: ruleLabel(v.rule_id, v.rule_slug),
94+
message: v.message,
95+
location,
96+
});
97+
}
98+
}
99+
} else {
100+
// Object format: { path: { "/foo": { key, violations } }, operation_id: {...}, ... }
101+
for (const typeGroups of Object.values(violations)) {
102+
for (const group of Object.values(typeGroups)) {
103+
const location = group.key.path || group.key.schema_path || group.key.operation_id || "";
104+
for (const v of group.violations) {
105+
flat.push({
106+
severity: v.severity,
107+
ruleLabel: ruleLabel(v.rule_id, v.rule_slug),
108+
message: v.message,
109+
location,
110+
});
111+
}
112+
}
113+
}
114+
}
115+
116+
const order: Record<string, number> = { error: 0, warning: 1, info: 2 };
117+
flat.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));
118+
return flat;
119+
}
120+
121+
// ── Public API ─────────────────────────────────────────
122+
123+
export function printViolations(result: ViolationsResult): void {
124+
const flat = flatten(result);
125+
126+
if (flat.length === 0) {
127+
console.log(`\n ${c.green("\u2714")} No violations found.\n`);
128+
return;
129+
}
130+
131+
// Compute column widths from actual data
132+
const ruleColWidth = Math.max(...flat.map((v) => v.ruleLabel.replace(/\x1b\[[0-9;]*m/g, "").length)) + 1;
133+
const msgColWidth = Math.min(
134+
Math.max(...flat.map((v) => v.message.length)),
135+
52,
136+
);
137+
138+
console.log("");
139+
for (const v of flat) {
140+
const sev = severityLabel(v.severity);
141+
const rule = pad(v.ruleLabel + ":", ruleColWidth + 1);
142+
const msg = pad(v.message.length > msgColWidth ? v.message.slice(0, msgColWidth - 1) + "\u2026" : v.message, msgColWidth);
143+
const loc = v.location ? c.dim(v.location) : "";
144+
console.log(` ${sev} ${rule} ${msg} ${loc}`);
145+
}
146+
147+
// Divider
148+
const divWidth = Math.min(process.stdout.columns || 80, 72);
149+
console.log(c.dim(` ${"─".repeat(divWidth)}`));
150+
151+
// Summary counts
152+
let errorCount = 0, warnCount = 0, infoCount = 0;
153+
for (const v of flat) {
154+
if (v.severity === "error") errorCount++;
155+
else if (v.severity === "warning") warnCount++;
156+
else infoCount++;
157+
}
158+
159+
const parts: string[] = [];
160+
if (errorCount > 0) parts.push(c.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`));
161+
if (warnCount > 0) parts.push(c.yellow(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`));
162+
if (infoCount > 0) parts.push(c.blue(`${infoCount} info`));
163+
164+
console.log(` ${parts.join(" ")}`);
165+
console.log("");
166+
}
167+
168+
export function printReportUrl(server: string, orgSlug: string, projectName: string, specId: string): void {
169+
const url = `${server}/projects/${orgSlug}/${projectName}`;
170+
console.log(` ${c.dim("View report:")} ${c.cyan(url)}`);
171+
console.log("");
172+
}
173+
174+
export function printNoViolations(): void {
175+
console.log(`\n ${c.green("\u2714")} No violations found.\n`);
176+
}

0 commit comments

Comments
 (0)