import pc from 'picocolors'; import { renderJson } from './json-output.js'; type Diagnostic = Record; export function renderDoctorDiagnostics(diagnostics: Diagnostic[], format: string | undefined): string { if (format === 'json') return renderJson(diagnostics); if (format === 'table') return renderDoctorTable(diagnostics); if (format) throw new Error(`Unsupported doctor format: ${format}. Expected json or table.`); return renderLegacyDoctorOutput(diagnostics); } function renderLegacyDoctorOutput(diagnostics: Diagnostic[]): string { if (diagnostics.length === 0) return cleanDoctorMessage(); return renderJson(diagnostics); } export function renderDoctorTable(diagnostics: Diagnostic[]): string { if (diagnostics.length === 0) return cleanDoctorMessage(); const rows = diagnostics.map((diagnostic) => ({ severity: String(diagnostic.severity ?? 'info'), code: String(diagnostic.code ?? 'diagnostic'), location: diagnosticLocation(diagnostic), message: compactMessage(diagnostic), hints: suggestedHintLines(diagnostic), })); const widths = { severity: columnWidth('Severity', rows.map((row) => row.severity)), code: columnWidth('Code', rows.map((row) => row.code)), location: columnWidth('Location', rows.map((row) => row.location)), }; const lines = [ `${'Severity'.padEnd(widths.severity)} ${'Code'.padEnd(widths.code)} ${'Location'.padEnd(widths.location)} Message`, `${'-'.repeat(widths.severity)} ${'-'.repeat(widths.code)} ${'-'.repeat(widths.location)} ${'-'.repeat(7)}`, ]; for (const row of rows) { lines.push(`${row.severity.padEnd(widths.severity)} ${ row.code.padEnd(widths.code)} ${ row.location.padEnd(widths.location)} ${row.message}`); lines.push(...row.hints.map((hint) => ` try ${hint}`)); } return `${lines.join('\n')}\n`; } function diagnosticLocation(diagnostic: Diagnostic): string { const repository = diagnostic.repositoryName ?? diagnostic.repository; const file = diagnostic.sourceFile ?? diagnostic.file; const line = diagnostic.sourceLine ?? diagnostic.line; if (file || line) return `${ repository ? `${String(repository)}:` : '' }${String(file ?? '')}:${String(line ?? '')}`; if (repository) return String(repository); return '-'; } function compactMessage(diagnostic: Diagnostic): string { const message = String(diagnostic.message ?? ''); const count = typeof diagnostic.count === 'number' ? ` count=${diagnostic.count}` : ''; const total = typeof diagnostic.total === 'number' ? ` total=${diagnostic.total}` : ''; const multiplicity = typeof diagnostic.multiplicity === 'number' ? ` multiplicity=${diagnostic.multiplicity}` : ''; return `${message}${count}${total}${multiplicity}`.trim(); } function suggestedHintLines(diagnostic: Diagnostic): string[] { const direct = cliHints(diagnostic.suggestedHints); const omitted = numericValue(diagnostic.omittedImplementationHintSuggestionCount); if (direct.length > 0) return cappedHints(direct, omitted); return cappedHints( cliHintsFromSuggestions(diagnostic.implementationHintSuggestions), omitted, ); } function cliHints(value: unknown): string[] { return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []; } function cliHintsFromSuggestions(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.flatMap((item) => { if (!isRecord(item)) return []; return typeof item.cli === 'string' ? [item.cli] : []; }); } function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === 'object' && !Array.isArray(value)); } function cappedHints(hints: string[], omitted: number): string[] { const unique = [...new Set(hints)]; const shown = unique.slice(0, 3); const remaining = Math.max(0, unique.length - shown.length) + omitted; if (remaining > 0) shown.push(`... ${remaining} additional hint(s) omitted; use a scoped --implementation-hint`); return shown; } function numericValue(value: unknown): number { return typeof value === 'number' && Number.isFinite(value) ? value : 0; } function cleanDoctorMessage(): string { return `${pc.green('No diagnostics recorded')}\n`; } function columnWidth(header: string, values: string[]): number { return Math.max(header.length, ...values.map((value) => value.length)); }