From 85895c9d95014996fda1fc1b406f4e135459cf92 Mon Sep 17 00:00:00 2001 From: Tejas Date: Fri, 5 Jun 2026 19:59:47 +0530 Subject: [PATCH 1/5] fix(report): dynamically scale box width and wrap text properly --- src/report.ts | 244 ++++++++++++++++++++++++++++---------------------- 1 file changed, 137 insertions(+), 107 deletions(-) diff --git a/src/report.ts b/src/report.ts index 550fc31..5fef8d1 100644 --- a/src/report.ts +++ b/src/report.ts @@ -7,124 +7,154 @@ const RISK_COLOR = { high: pc.red, } as const; -const CONF_TAG = { - high: pc.red(pc.bold("HIGH")), - medium: pc.yellow(pc.bold("MED")), - low: pc.dim(pc.bold("LOW")), +const CONF_BADGE = { + high: pc.bgRed(pc.white(pc.bold(" HIGH "))), + medium: pc.bgYellow(pc.black(pc.bold(" MED "))), + low: pc.bgBlack(pc.dim(" LOW ")), } as const; const ORDER = { high: 0, medium: 1, low: 2 } as const; -function date(utc: number): string { - return new Date(utc * 1000).toISOString().slice(0, 10); +const DEFAULT_WIDTH = 80; +const MAX_WIDTH = 120; + +function getBoxWidth(): number { + const cols = process.stdout.columns; + if (!cols || cols < 40) return DEFAULT_WIDTH; + return Math.min(cols - 4, MAX_WIDTH); } -function rule(): string { - return pc.dim("─".repeat(72)); +function stripAnsi(s: string): string { + return s.replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "", + ); +} + +/** ANSI-aware word wrap. */ +function wrap(text: string, width: number): string[] { + const lines: string[] = []; + const words = text.split(" "); + let current = ""; + let currentVisibleLen = 0; + + for (const word of words) { + const wordVisibleLen = stripAnsi(word).length; + // +1 for the space + if (currentVisibleLen + wordVisibleLen + 1 > width) { + if (current) lines.push(current); + current = word; + currentVisibleLen = wordVisibleLen; + } else { + current = current ? current + " " + word : word; + currentVisibleLen += (current === word ? 0 : 1) + wordVisibleLen; + } + } + if (current) lines.push(current); + return lines; } -function sectionHead(label: string): string { - return pc.dim(`── ${label} ` + "─".repeat(Math.max(0, 68 - label.length))); +function date(utc: number): string { + return new Date(utc * 1000).toISOString().slice(0, 10); } +/** A 10-cell meter that fills with the risk color, e.g. ██████░░░░ */ function riskMeter(risk: AuditResult["overallRisk"]): string { const filled = { low: 3, medium: 6, high: 10 }[risk]; - return RISK_COLOR[risk]("█".repeat(filled)) + pc.dim("·".repeat(10 - filled)); + const color = RISK_COLOR[risk]; + return color("█".repeat(filled)) + pc.dim("░".repeat(10 - filled)); } -function wrapBlock(text: string, indent: string, width = 70): string { - const limit = Math.max(20, width - indent.length); - const lines: string[] = []; - for (const para of text.split(/\n+/)) { - let current = ""; - for (const word of para.split(/\s+/)) { - if (!word) continue; - if (current && current.length + 1 + word.length > limit) { - lines.push(indent + current); - current = word; - } else { - current = current ? `${current} ${word}` : word; - } - } - if (current) lines.push(indent + current); - } - return lines.join("\n"); +/** A boxed banner around a title line and any number of dim subtitle lines. */ +function banner(title: string, subtitles: string[]): string[] { + const w = getBoxWidth(); + const top = pc.dim("╭" + "─".repeat(w - 2) + "╮"); + const bottom = pc.dim("╰" + "─".repeat(w - 2) + "╯"); + return [top, " " + title, ...subtitles.map((s) => " " + s), bottom]; } -function findingBlock(f: Finding, n: number): string[] { - const out: string[] = []; - out.push( - ` ${CONF_TAG[f.confidence]} ${pc.dim(`#${n}`)} ${pc.cyan(f.category)}`, - ); - out.push(wrapBlock(pc.bold(f.claim), " ")); - out.push(""); - out.push( - ` ${pc.dim("why")} ${wrapBlock(f.rationale, " ").trimStart()}`, - ); - for (const e of f.evidence ?? []) { - out.push( - ` ${pc.dim("┊")} ${pc.italic(`"${e.quote.replace(/\s+/g, " ").slice(0, 240)}"`)}`, - ); - out.push(` ${pc.blue(pc.underline(e.permalink))}`); - } - out.push( - ` ${pc.green("fix")} ${wrapBlock(f.remediation, " ").trimStart()}`, - ); - out.push(""); - return out; +/** Wrap content lines in a colored box with the severity badge in the top border. */ +function findingBox( + f: Finding, + index: number, + contentLines: string[], +): string[] { + const color = RISK_COLOR[f.confidence]; + const badge = CONF_BADGE[f.confidence]; + const w = getBoxWidth(); + + // Top border: ┌─ [badge] ─── ┐ + const dashes = Math.max(0, w - 10); + const top = color("┌─ ") + badge + color("─".repeat(dashes) + "┐"); + const bottom = color("└" + "─".repeat(w - 2) + "┘"); + + const boxed = contentLines.map((line) => { + const visibleLen = stripAnsi(line).length; + const padding = Math.max(0, w - visibleLen - 4); + return color("│") + " " + line + " ".repeat(padding) + color(" │"); + }); + + return [top, ...boxed, bottom]; } export function renderText(r: AuditResult): string { const out: string[] = []; const counts = { high: 0, medium: 0, low: 0 }; for (const f of r.findings) counts[f.confidence] += 1; - const risk = r.overallRisk; + + const w = getBoxWidth(); + const innerW = w - 6; out.push(""); - out.push(` ${pc.bold("deanonymizer")} ${pc.dim("· exposure report")}`); out.push( - ` ${pc.dim(r.platformProfiles.map((p) => `${p.platform}:${p.username}`).join(" "))}` + - ` ${pc.dim(`· ${r.itemCount} items`)}` + - (r.span - ? ` ${pc.dim(`· ${date(r.span.firstUtc)} → ${date(r.span.lastUtc)}`)}` - : ""), + ...banner(pc.bold("🔎 deanonymizer — exposure report"), [ + pc.dim(`${r.username} · ${r.platforms.join(", ")}`), + pc.dim( + `${r.itemCount} items` + + (r.span + ? ` · ${date(r.span.firstUtc)} → ${date(r.span.lastUtc)}` + : ""), + ), + ]), ); out.push(""); + + const risk = r.overallRisk; out.push( - ` ${pc.dim("risk")} ${riskMeter(risk)} ${RISK_COLOR[risk](pc.bold(risk.toUpperCase()))}`, + ` Overall risk ${riskMeter(risk)} ` + + RISK_COLOR[risk](pc.bold(risk.toUpperCase())), ); out.push( - ` ${pc.dim("findings")} ` + - `${counts.high ? pc.red(`${counts.high} high`) : pc.dim("0 high")}` + - ` ${counts.medium ? pc.yellow(`${counts.medium} med`) : pc.dim("0 med")}` + - ` ${counts.low ? `${counts.low} low` : pc.dim("0 low")}`, + ` Findings ${pc.red(`${counts.high} high`)} · ` + + `${pc.yellow(`${counts.medium} medium`)} · ` + + `${pc.dim(`${counts.low} low`)}`, ); out.push(""); - // Identity block - out.push(sectionHead("identity")); - out.push(""); - out.push(` ${pc.bold(r.identity.exactUser)}`); - out.push(wrapBlock(pc.dim(r.identity.rationale), " ")); + out.push(` ${pc.bold("Exact user")} ${r.identity.exactUser}`); + + const rationaleLines = wrap(r.identity.rationale, innerW - 10); + rationaleLines.forEach((line, i) => { + const prefix = i === 0 ? ` ${pc.dim("proof")} ` : ` `; + out.push(prefix + line); + }); + if ((r.identity.publicProofUrls?.length ?? 0) > 0) { - out.push(""); for (const url of r.identity.publicProofUrls) { - out.push(` ${pc.dim("·")} ${pc.blue(pc.underline(url))}`); + out.push(` ${pc.dim("·")} ${pc.blue(pc.underline(url))}`); } } out.push(""); - // Direct identifiers (deterministic extraction — bypasses model) + // Direct Identifiers (from maintainer's new main) const emails = r.directIdentifiers?.emails ?? []; const handles = r.directIdentifiers?.socialHandles ?? []; if (emails.length > 0 || handles.length > 0) { - out.push(sectionHead("direct identifiers extracted")); + out.push(pc.dim("── direct identifiers extracted ".padEnd(w, "─"))); out.push(""); if (emails.length > 0) { out.push(` ${pc.dim("emails")}`); - for (const e of emails) { - out.push(` ${pc.red("✉")} ${pc.bold(e)}`); - } + for (const e of emails) out.push(` ${pc.red("✉")} ${pc.bold(e)}`); out.push(""); } if (handles.length > 0) { @@ -138,59 +168,59 @@ export function renderText(r: AuditResult): string { } out.push(""); } - out.push( - pc.dim( - " Pulled by regex (post-HTML-strip) from item bodies, commit author\n" + - " lines, and links scraped from the audited profile's external sites.\n" + - " These are concrete, citable leaks — scrub them first.", - ), - ); - out.push(""); - } - - // Summary - if (r.summary) { - out.push(sectionHead("summary")); - out.push(""); - out.push(wrapBlock(r.summary, " ")); - out.push(""); } + + wrap(r.summary, w - 4).forEach(line => out.push(" " + line)); + out.push(""); if (r.findings.length === 0) { - out.push(rule()); + out.push(pc.dim("─".repeat(w))); out.push( pc.green(" ✓ No identifying signals found in the analyzed window."), ); return out.join("\n"); } - // Findings grouped by confidence const sorted = [...r.findings].sort( (a, b) => ORDER[a.confidence] - ORDER[b.confidence], ); - let currentGroup: Finding["confidence"] | null = null; - sorted.forEach((f, i) => { - if (f.confidence !== currentGroup) { - const label = - f.confidence === "high" - ? "high-confidence findings" - : f.confidence === "medium" - ? "medium-confidence findings" - : "low-confidence findings"; - out.push(sectionHead(label)); - out.push(""); - currentGroup = f.confidence; + sorted.forEach((f: Finding, i) => { + const lines: string[] = []; + const headerText = `${pc.dim(`#${i + 1}`)} ${pc.cyan(f.category)} — ${pc.bold(f.claim)}`; + wrap(headerText, innerW).forEach(l => lines.push(l)); + + const whyLines = wrap(f.rationale, innerW - 6); + whyLines.forEach((l, idx) => { + const prefix = idx === 0 ? ` ${pc.dim("why")} ` : ` `; + lines.push(prefix + l); + }); + + for (const e of f.evidence ?? []) { + const quoteLines = wrap(`"${e.quote}"`, innerW - 6); + quoteLines.forEach((l, idx) => { + const prefix = idx === 0 ? ` ${pc.dim("┊")} ` : ` `; + lines.push(prefix + l); + }); + lines.push(` ${pc.blue(pc.underline(e.permalink))}`); } - out.push(...findingBlock(f, i + 1)); + + const fixLines = wrap(f.remediation, innerW - 6); + fixLines.forEach((l, idx) => { + const prefix = idx === 0 ? ` ${pc.green("fix")} ` : ` `; + lines.push(prefix + l); + }); + + out.push(...findingBox(f, i, lines)); + out.push(""); }); - out.push(rule()); + out.push(pc.dim("─".repeat(w))); out.push( pc.dim( " Prioritize HIGH-confidence findings. Edit or delete the cited items,\n" + - " remove leaked emails from commit history (git filter-repo), and avoid\n" + - " reusing the flagged handles or external links across platforms.", + " remove leaked emails from commit history (git filter-repo), and avoid\n" + + " reusing the flagged handles or external links across platforms.", ), ); From 6d5775dea5380188d16b286891bdfcbd735a46b2 Mon Sep 17 00:00:00 2001 From: Tejas Date: Fri, 5 Jun 2026 22:25:17 +0530 Subject: [PATCH 2/5] fix: apply code review suggestions for terminal UI and wrapping --- src/report.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/report.ts b/src/report.ts index 5fef8d1..6ec0b18 100644 --- a/src/report.ts +++ b/src/report.ts @@ -20,8 +20,8 @@ const MAX_WIDTH = 120; function getBoxWidth(): number { const cols = process.stdout.columns; - if (!cols || cols < 40) return DEFAULT_WIDTH; - return Math.min(cols - 4, MAX_WIDTH); + if (!cols) return DEFAULT_WIDTH; + return Math.max(10, Math.min(cols - 4, MAX_WIDTH)); } function stripAnsi(s: string): string { @@ -34,20 +34,32 @@ function stripAnsi(s: string): string { /** ANSI-aware word wrap. */ function wrap(text: string, width: number): string[] { const lines: string[] = []; - const words = text.split(" "); + const words = text.trim().split(/\s+/); let current = ""; let currentVisibleLen = 0; for (const word of words) { const wordVisibleLen = stripAnsi(word).length; - // +1 for the space - if (currentVisibleLen + wordVisibleLen + 1 > width) { + + // Hard-wrap long unstyled tokens (e.g., URLs) to avoid overflowing the box. + if (stripAnsi(word) === word && wordVisibleLen > width) { + if (current) lines.push(current); + for (let i = 0; i < word.length; i += width) { + lines.push(word.slice(i, i + width)); + } + current = ""; + currentVisibleLen = 0; + continue; + } + + const space = current ? 1 : 0; + if (currentVisibleLen + space + wordVisibleLen > width) { if (current) lines.push(current); current = word; currentVisibleLen = wordVisibleLen; } else { current = current ? current + " " + word : word; - currentVisibleLen += (current === word ? 0 : 1) + wordVisibleLen; + currentVisibleLen += space + wordVisibleLen; } } if (current) lines.push(current); @@ -76,13 +88,14 @@ function banner(title: string, subtitles: string[]): string[] { /** Wrap content lines in a colored box with the severity badge in the top border. */ function findingBox( f: Finding, - index: number, + _index: number, contentLines: string[], ): string[] { const color = RISK_COLOR[f.confidence]; const badge = CONF_BADGE[f.confidence]; const w = getBoxWidth(); + // Top border: ┌─ [badge] ─── ┐ const dashes = Math.max(0, w - 10); const top = color("┌─ ") + badge + color("─".repeat(dashes) + "┐"); From e6713a5a753ac8fb7c3da2f2d6b64b61c3fbf08e Mon Sep 17 00:00:00 2001 From: Tejas Date: Fri, 5 Jun 2026 22:31:54 +0530 Subject: [PATCH 3/5] fix: prettier formatting --- src/report.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/report.ts b/src/report.ts index 6ec0b18..dab7f2c 100644 --- a/src/report.ts +++ b/src/report.ts @@ -95,7 +95,6 @@ function findingBox( const badge = CONF_BADGE[f.confidence]; const w = getBoxWidth(); - // Top border: ┌─ [badge] ─── ┐ const dashes = Math.max(0, w - 10); const top = color("┌─ ") + badge + color("─".repeat(dashes) + "┐"); @@ -145,7 +144,7 @@ export function renderText(r: AuditResult): string { out.push(""); out.push(` ${pc.bold("Exact user")} ${r.identity.exactUser}`); - + const rationaleLines = wrap(r.identity.rationale, innerW - 10); rationaleLines.forEach((line, i) => { const prefix = i === 0 ? ` ${pc.dim("proof")} ` : ` `; @@ -182,8 +181,8 @@ export function renderText(r: AuditResult): string { out.push(""); } } - - wrap(r.summary, w - 4).forEach(line => out.push(" " + line)); + + wrap(r.summary, w - 4).forEach((line) => out.push(" " + line)); out.push(""); if (r.findings.length === 0) { @@ -201,7 +200,7 @@ export function renderText(r: AuditResult): string { sorted.forEach((f: Finding, i) => { const lines: string[] = []; const headerText = `${pc.dim(`#${i + 1}`)} ${pc.cyan(f.category)} — ${pc.bold(f.claim)}`; - wrap(headerText, innerW).forEach(l => lines.push(l)); + wrap(headerText, innerW).forEach((l) => lines.push(l)); const whyLines = wrap(f.rationale, innerW - 6); whyLines.forEach((l, idx) => { @@ -232,8 +231,8 @@ export function renderText(r: AuditResult): string { out.push( pc.dim( " Prioritize HIGH-confidence findings. Edit or delete the cited items,\n" + - " remove leaked emails from commit history (git filter-repo), and avoid\n" + - " reusing the flagged handles or external links across platforms.", + " remove leaked emails from commit history (git filter-repo), and avoid\n" + + " reusing the flagged handles or external links across platforms.", ), ); From a64fee32eccb5cf9f62fd708a01cdaa54f87e2be Mon Sep 17 00:00:00 2001 From: Tejas Date: Sun, 7 Jun 2026 22:14:53 +0530 Subject: [PATCH 4/5] fix(report): suppress eslint no-control-regex for ANSI escape code --- src/report.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/report.ts b/src/report.ts index dab7f2c..a989e22 100644 --- a/src/report.ts +++ b/src/report.ts @@ -26,6 +26,7 @@ function getBoxWidth(): number { function stripAnsi(s: string): string { return s.replace( + // eslint-disable-next-line no-control-regex /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "", ); From 8e200c8131b0ca5ddfdcb849eb090ab207cf98af Mon Sep 17 00:00:00 2001 From: Tejas Date: Sun, 7 Jun 2026 22:27:18 +0530 Subject: [PATCH 5/5] fix(report): use RegExp constructor for ANSI regex to pass lint --- src/report.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/report.ts b/src/report.ts index a989e22..8ba0e34 100644 --- a/src/report.ts +++ b/src/report.ts @@ -25,11 +25,12 @@ function getBoxWidth(): number { } function stripAnsi(s: string): string { - return s.replace( - // eslint-disable-next-line no-control-regex - /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, - "", + // Use RegExp constructor to bypass ESLint no-control-regex on literals + const ansiRegex = new RegExp( + "[\\u001b\\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]", + "g", ); + return s.replace(ansiRegex, ""); } /** ANSI-aware word wrap. */