Files
Codeman/src/utils/dependency-report.ts
T
Codeman maintainer 4e5d0dcbd6 fix: measure the doctor table columns and let the CLI paint them
"Antigravity CLI" is 15 characters and the hardcoded padEnd(14) pushed
that whole row one column right. Widths now come from the widest cell.

The header always said the CLI layer may colorize, but there was no way
to: renderTable now takes an optional ReportStyle whose hooks are
identity by default, so the module still decides nothing about color and
its output stays byte-stable. Padding is applied outside the paint, so a
row with no path detail ends at its status text instead of trailing
spaces inside a color run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 14:13:57 +02:00

119 lines
4.5 KiB
TypeScript

/**
* @fileoverview Renders ToolResult[] from the dependency checker into a
* human-readable grouped table or JSON, and computes the process exit code.
* Plain text by default (no color) so output is stable and snapshot-friendly;
* the CLI layer passes a `ReportStyle` to paint it (see `cli.ts`, `doctor`).
*
* Column widths are measured, not hardcoded: "Antigravity CLI" is 15 characters
* and the old `padEnd(14)` pushed its whole row one column right.
*
* @module utils/dependency-report
*/
import { columnWidths, padStyled } from '../cli-style.js';
import type { ProbeEnvironment, ToolCategory } from '../config/dependency-registry.js';
import type { ToolResult, ToolStatus } from './dependency-checker.js';
const CATEGORY_ORDER: ToolCategory[] = ['core', 'office', 'other'];
/**
* Paint hooks for the CLI layer. Every hook is identity by default, so this
* module never decides anything about color and its output stays byte-stable
* for tests.
*/
export interface ReportStyle {
title(text: string): string;
heading(text: string): string;
glyph(result: ToolResult, glyph: string): string;
label(text: string): string;
status(result: ToolResult, text: string): string;
path(text: string): string;
meta(text: string): string;
summary(text: string): string;
}
const identity = (text: string): string => text;
const PLAIN_STYLE: ReportStyle = {
title: identity,
heading: identity,
glyph: (_result, glyph) => glyph,
label: identity,
status: (_result, text) => text,
path: identity,
meta: identity,
summary: identity,
};
function glyph(r: ToolResult): string {
if (r.status === 'ok') return '✓';
if (r.status === 'skipped') return '○';
return r.required ? '✗' : '○';
}
function statusText(r: ToolResult): string {
if (r.status === 'ok') return r.version ?? 'installed';
if (r.status === 'outdated') return `${r.version ?? '?'} (below minimum)`;
if (r.status === 'skipped') return 'n/a';
if (r.status === 'error') return 'version error';
return 'not found';
}
export function computeExitCode(results: ToolResult[]): number {
const failed = results.some(
(r) => r.required && (r.status === 'missing' || r.status === 'outdated' || r.status === 'error')
);
return failed ? 1 : 0;
}
export function renderTable(
results: ToolResult[],
environment: ProbeEnvironment,
style: ReportStyle = PLAIN_STYLE
): string {
// Widths are taken across ALL categories so the groups line up with each other.
const [labelWidth, statusWidth] = columnWidths(results.map((r) => [r.label, statusText(r)]));
const lines: string[] = [style.title(`Codeman dependency check — ${environment}`), ''];
for (const category of CATEGORY_ORDER) {
const rows = results.filter((r) => r.category === category);
if (rows.length === 0) continue;
lines.push(style.heading(category.toUpperCase()));
for (const r of rows) {
const label = padStyled(r.label, labelWidth ?? 0, style.label);
const status = padStyled(statusText(r), statusWidth ?? 0, (text) => style.status(r, text));
const detail = r.path ? ` ${style.path(r.path)}` : '';
lines.push(` ${style.glyph(r, glyph(r))} ${label} ${status}${detail}`.trimEnd());
if (r.usedBy.length) lines.push(style.meta(` used by: ${r.usedBy.join(', ')}`));
if (r.installHint) lines.push(style.meta(` install: ${r.installHint}`));
}
lines.push('');
}
const ok = results.filter((r) => r.status === 'ok').length;
const requiredMissing = results.filter((r) => r.required && r.status !== 'ok' && r.status !== 'skipped').length;
const optionalMissing = results.filter((r) => !r.required && r.status === 'missing').length;
lines.push(
style.summary(`Summary: ${ok} ok · ${requiredMissing} required missing · ${optionalMissing} optional missing`)
);
return lines.join('\n');
}
export interface DependencyReportJson {
platform: { environment: ProbeEnvironment };
summary: { ok: number; requiredMissing: number; optionalMissing: number; exitCode: number };
tools: ToolResult[];
}
export function renderJson(results: ToolResult[], environment: ProbeEnvironment): DependencyReportJson {
const byStatus = (s: ToolStatus) => results.filter((r) => r.status === s).length;
return {
platform: { environment },
summary: {
ok: byStatus('ok'),
requiredMissing: results.filter((r) => r.required && r.status !== 'ok' && r.status !== 'skipped').length,
optionalMissing: results.filter((r) => !r.required && r.status === 'missing').length,
exitCode: computeExitCode(results),
},
tools: results,
};
}