import chalk from 'chalk';
import { table } from 'table';
import { SearchResult } from './api.js';
export type OutputFormat = 'json' | 'table' | 'plain';
export function formatOutput(data: any, format: OutputFormat, context: string, options?: any): string {
switch (format) {
case 'json':
return JSON.stringify(data, null, 2);
case 'table':
return formatAsTable(data, context, options);
case 'plain':
default:
return formatAsPlain(data, context, options);
}
}
function formatAsTable(data: any, context: string, options?: any): string {
switch (context) {
case 'search':
return formatSearchResultsTable(data);
case 'symbol':
return formatSymbolResultsTable(data);
case 'file':
return formatFileTable(data);
case 'gerrit-status':
return formatGerritStatusTable(data);
case 'gerrit-diff':
return formatGerritDiffTable(data);
case 'gerrit-file':
return formatGerritFileTable(data);
case 'gerrit-bots':
return formatGerritBotsTable(data);
case 'gerrit-list':
return formatGerritListTable(data);
case 'pdfium-gerrit-list':
return formatPdfiumGerritListTable(data);
case 'owners':
return formatOwnersTable(data);
case 'commits':
return formatCommitsTable(data);
case 'issue':
return formatIssueTable(data);
case 'issue-search':
return formatIssueSearchTable(data);
case 'list-folder':
return formatListFolderTable(data);
case 'ci-errors':
return formatCIErrorsTable(data);
case 'gerrit-comments':
return formatGerritCommentsTable(data);
case 'blame':
return formatBlameTable(data);
case 'history':
return formatHistoryTable(data);
case 'contributors':
return formatContributorsTable(data);
case 'suggest-reviewers':
return formatSuggestReviewersPlain(data, options);
default:
return JSON.stringify(data, null, 2);
}
}
function formatAsPlain(data: any, context: string, options?: any): string {
switch (context) {
case 'search':
return formatSearchResultsPlain(data);
case 'symbol':
return formatSymbolResultsPlain(data);
case 'file':
return formatFilePlain(data);
case 'gerrit-status':
return formatGerritStatusPlain(data);
case 'gerrit-diff':
return formatGerritDiffPlain(data);
case 'gerrit-file':
return formatGerritFilePlain(data);
case 'gerrit-bots':
return formatGerritBotsPlain(data);
case 'gerrit-list':
return formatGerritListPlain(data);
case 'pdfium-gerrit-list':
return formatPdfiumGerritListPlain(data);
case 'owners':
return formatOwnersPlain(data);
case 'commits':
return formatCommitsPlain(data);
case 'issue':
return formatIssuePlain(data);
case 'issue-search':
return formatIssueSearchPlain(data);
case 'list-folder':
return formatListFolderPlain(data);
case 'ci-errors':
return formatCIErrorsPlain(data);
case 'gerrit-bot-errors':
return formatGerritBotErrorsPlain(data);
case 'gerrit-comments':
return formatGerritCommentsPlain(data);
case 'blame':
return formatBlamePlain(data);
case 'history':
return formatHistoryPlain(data);
case 'contributors':
return formatContributorsPlain(data);
case 'suggest-reviewers':
return formatSuggestReviewersPlain(data, options);
default:
return JSON.stringify(data, null, 2);
}
}
function formatSearchResultsTable(results: SearchResult[]): string {
if (!results || results.length === 0) {
return chalk.yellow('No results found');
}
const tableData = [
['File', 'Line', 'Content', 'URL']
];
results.forEach(result => {
tableData.push([
result.file,
result.line.toString(),
result.content.replace(/\n/g, ' ').substring(0, 80) + '...',
result.url
]);
});
return table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
}
function formatSearchResultsPlain(results: SearchResult[]): string {
if (!results || results.length === 0) {
return chalk.yellow('No results found');
}
let output = chalk.cyan(`Found ${results.length} results:\n\n`);
results.forEach((result, index) => {
output += chalk.bold.green(`${index + 1}. ${result.file}:${result.line}\n`);
output += chalk.gray('───────────────────────────────\n');
output += `${result.content}\n`;
output += chalk.blue(`🔗 ${result.url}\n\n`);
});
return output;
}
function formatSymbolResultsTable(data: any): string {
const { symbol, symbolResults, classResults, functionResults, usageResults } = data;
let output = chalk.bold.cyan(`Symbol: ${symbol}\n\n`);
const sections = [
{ title: 'Symbol Definitions', results: symbolResults },
{ title: 'Class Definitions', results: classResults },
{ title: 'Function Definitions', results: functionResults },
{ title: 'Usage Examples', results: usageResults }
];
sections.forEach(section => {
if (section.results && section.results.length > 0) {
output += chalk.bold.yellow(`${section.title}:\n`);
const tableData = [['File', 'Line', 'Content']];
section.results.forEach((result: SearchResult) => {
tableData.push([
result.file,
result.line.toString(),
result.content.replace(/\n/g, ' ').substring(0, 60) + '...'
]);
});
output += table(tableData) + '\n';
}
});
return output;
}
function formatSymbolResultsPlain(data: any): string {
const { symbol, symbolResults, classResults, functionResults, usageResults, estimatedUsageCount } = data;
let output = chalk.bold.cyan(`Symbol: ${symbol}\n\n`);
const sections = [
{ title: '🎯 Symbol Definitions', results: symbolResults, icon: '🎯' },
{ title: '🏗️ Class Definitions', results: classResults, icon: '🏗️' },
{ title: '⚙️ Function Definitions', results: functionResults, icon: '⚙️' },
{ title: '📚 Usage Examples', results: usageResults, icon: '📚' }
];
sections.forEach(section => {
if (section.results && section.results.length > 0) {
output += chalk.bold.yellow(`${section.title}:\n`);
if (section.title.includes('Usage') && estimatedUsageCount) {
output += chalk.gray(`Found ${estimatedUsageCount} total usage matches across the codebase\n\n`);
}
section.results.forEach((result: SearchResult, index: number) => {
output += chalk.green(`${index + 1}. ${result.file}:${result.line}\n`);
output += `${result.content}\n`;
output += chalk.blue(`🔗 ${result.url}\n\n`);
});
}
});
return output;
}
function formatFileTable(data: any): string {
const { filePath, totalLines, displayedLines, lineStart, lineEnd, browserUrl, source, githubUrl, webrtcUrl } = data;
let output = chalk.bold.cyan(`File: ${filePath}\n`);
output += chalk.gray(`Total lines: ${totalLines} | Displayed: ${displayedLines}\n`);
if (lineStart) {
output += chalk.gray(`Lines: ${lineStart}${lineEnd ? `-${lineEnd}` : '+'}\n`);
}
if (source) {
output += chalk.yellow(`📌 Source: ${source}\n`);
}
output += chalk.blue(`🔗 ${browserUrl}\n`);
if (githubUrl) {
output += chalk.blue(`🔗 GitHub: ${githubUrl}\n`);
}
if (webrtcUrl) {
output += chalk.blue(`🔗 WebRTC: ${webrtcUrl}\n`);
}
output += '\n' + chalk.gray('Content:\n');
output += '─'.repeat(80) + '\n';
output += data.content + '\n';
output += '─'.repeat(80) + '\n';
return output;
}
function formatFilePlain(data: any): string {
return formatFileTable(data); // Same formatting for plain and table for files
}
function formatGerritStatusTable(data: any): string {
if (!data) return chalk.red('No CL data found');
let output = chalk.bold.cyan(`CL: ${data.subject || 'Unknown'}\n\n`);
const infoData = [
['Property', 'Value'],
['Status', data.status || 'Unknown'],
['Owner', data.owner?.name || 'Unknown'],
['Created', data.created ? new Date(data.created).toLocaleDateString() : 'Unknown'],
['Updated', data.updated ? new Date(data.updated).toLocaleDateString() : 'Unknown']
];
output += table(infoData) + '\n';
// Extract and display commit message from current revision
if (data.current_revision && data.revisions && data.revisions[data.current_revision]) {
const currentRevision = data.revisions[data.current_revision];
if (currentRevision.commit && currentRevision.commit.message) {
output += chalk.bold.yellow('📝 Commit Message:\n');
output += chalk.gray('─'.repeat(40)) + '\n';
output += formatCommitMessage(currentRevision.commit.message) + '\n';
}
}
return output;
}
function formatGerritStatusPlain(data: any): string {
if (!data) return chalk.red('No CL data found');
let output = chalk.bold.cyan(`CL: ${data.subject || 'Unknown'}\n\n`);
output += chalk.yellow('Status: ') + (data.status || 'Unknown') + '\n';
output += chalk.yellow('Owner: ') + (data.owner?.name || 'Unknown') + '\n';
output += chalk.yellow('Created: ') + (data.created ? new Date(data.created).toLocaleDateString() : 'Unknown') + '\n';
output += chalk.yellow('Updated: ') + (data.updated ? new Date(data.updated).toLocaleDateString() : 'Unknown') + '\n';
// Extract and display commit message from current revision
if (data.current_revision && data.revisions && data.revisions[data.current_revision]) {
const currentRevision = data.revisions[data.current_revision];
if (currentRevision.commit && currentRevision.commit.message) {
output += '\n' + chalk.bold.yellow('📝 Commit Message:\n');
output += chalk.gray('─'.repeat(40)) + '\n';
output += formatCommitMessage(currentRevision.commit.message) + '\n';
}
}
return output;
}
function formatOwnersTable(data: any): string {
const { filePath, ownerFiles } = data;
let output = chalk.bold.cyan(`OWNERS for: ${filePath}\n\n`);
if (!ownerFiles || ownerFiles.length === 0) {
return output + chalk.yellow('No OWNERS files found');
}
ownerFiles.forEach((owner: any, index: number) => {
output += chalk.bold.green(`${index + 1}. ${owner.path}\n`);
output += chalk.gray('───────────────────────────────\n');
output += owner.content.split('\n').slice(0, 10).join('\n') + '\n';
output += chalk.blue(`🔗 ${owner.browserUrl}\n\n`);
});
return output;
}
function formatOwnersPlain(data: any): string {
return formatOwnersTable(data); // Same formatting
}
function formatCommitsTable(data: any): string {
if (!data || !data.log || data.log.length === 0) {
return chalk.yellow('No commits found');
}
let output = chalk.bold.cyan(`Found ${data.log.length} commits:\n\n`);
const tableData = [
['Hash', 'Author', 'Date', 'Message']
];
data.log.forEach((commit: any) => {
tableData.push([
commit.commit.substring(0, 8),
commit.author.name,
new Date(commit.author.time * 1000).toLocaleDateString(),
commit.message.split('\n')[0].substring(0, 50) + '...'
]);
});
return output + table(tableData);
}
function formatCommitsPlain(data: any): string {
if (!data || !data.log || data.log.length === 0) {
return chalk.yellow('No commits found');
}
let output = chalk.bold.cyan(`Found ${data.log.length} commits:\n\n`);
data.log.forEach((commit: any, index: number) => {
output += chalk.bold.green(`${index + 1}. ${commit.commit.substring(0, 8)}\n`);
output += chalk.yellow('Author: ') + commit.author.name + '\n';
output += chalk.yellow('Date: ') + new Date(commit.author.time * 1000).toLocaleDateString() + '\n';
output += chalk.yellow('Message: ') + commit.message.split('\n')[0] + '\n';
output += chalk.blue(`🔗 https://chromium.googlesource.com/chromium/src/+/${commit.commit}\n\n`);
});
return output;
}
function formatGerritDiffTable(data: any): string {
if (!data) return chalk.red('No diff data found');
let output = chalk.bold.cyan(`CL ${data.clId}: ${data.subject}\n\n`);
output += chalk.yellow('Patchset: ') + data.patchset + '\n';
output += chalk.yellow('Author: ') + data.author + '\n\n';
if (data.error) {
output += chalk.red(data.error) + '\n\n';
if (data.changedFiles && data.changedFiles.length > 0) {
output += chalk.yellow('Changed files:\n');
data.changedFiles.forEach((file: string) => {
output += `- ${file}\n`;
});
}
return output;
}
if (data.diffData) {
// Format specific file diff
output += chalk.bold.green('Diff Content:\n');
output += formatDiffContent(data.diffData);
} else {
// Format file overview
output += chalk.bold.green(`Files changed: ${data.changedFiles.length}\n\n`);
const tableData = [
['File', 'Status', 'Lines']
];
data.changedFiles.slice(0, 10).forEach((fileName: string) => {
const fileInfo = data.filesData[fileName];
const status = getFileStatusText(fileInfo?.status || 'M');
const lines = `+${fileInfo?.lines_inserted || 0} -${fileInfo?.lines_deleted || 0}`;
tableData.push([fileName, status, lines]);
});
output += table(tableData);
if (data.changedFiles.length > 10) {
output += chalk.gray(`\nShowing first 10 files. Total: ${data.changedFiles.length} files changed.\n`);
}
}
return output;
}
function formatGerritDiffPlain(data: any): string {
return formatGerritDiffTable(data); // Same formatting for now
}
function formatGerritFileTable(data: any): string {
if (!data) return chalk.red('No file data found');
let output = chalk.bold.cyan(`File: ${data.filePath}\n`);
output += chalk.yellow('CL: ') + `${data.clId} - ${data.subject}\n`;
output += chalk.yellow('Patchset: ') + data.patchset + '\n';
output += chalk.yellow('Author: ') + data.author + '\n';
output += chalk.yellow('Lines: ') + data.lines + '\n\n';
output += chalk.bold.green('Content:\n');
output += '─'.repeat(80) + '\n';
// Add line numbers to content
const lines = data.content.split('\n');
lines.forEach((line: string, index: number) => {
const lineNum = (index + 1).toString().padStart(4, ' ');
output += chalk.gray(lineNum + ': ') + line + '\n';
});
output += '─'.repeat(80) + '\n';
return output;
}
function formatGerritFilePlain(data: any): string {
return formatGerritFileTable(data); // Same formatting for now
}
function formatDiffContent(diffData: any): string {
let result = '';
if (!diffData.content) {
return chalk.gray('No diff content available.\n\n');
}
result += '```diff\n';
for (const section of diffData.content) {
if (section.ab) {
// Unchanged lines (context)
section.ab.forEach((line: string) => {
result += ` ${line}\n`;
});
}
if (section.a) {
// Removed lines
section.a.forEach((line: string) => {
result += chalk.red(`-${line}\n`);
});
}
if (section.b) {
// Added lines
section.b.forEach((line: string) => {
result += chalk.green(`+${line}\n`);
});
}
}
result += '```\n\n';
return result;
}
function getFileStatusText(status: string): string {
switch (status) {
case 'A': return 'Added';
case 'D': return 'Deleted';
case 'M': return 'Modified';
case 'R': return 'Renamed';
case 'C': return 'Copied';
default: return 'Modified';
}
}
function formatIssueTable(data: any): string {
if (!data) return chalk.red('No issue data found');
if (data.error) {
let output = chalk.red(`Error: ${data.error}\n`);
output += chalk.blue(`🔗 View issue: ${data.browserUrl}\n`);
return output;
}
let output = chalk.bold.cyan(`Issue ${data.issueId}: ${data.title || 'Unknown Title'}\n\n`);
const infoData = [
['Property', 'Value'],
['Status', data.status || 'Unknown'],
['Priority', data.priority || 'Unknown'],
['Type', data.type || 'Unknown'],
['Severity', data.severity || 'Unknown'],
['Reporter', data.reporter || 'Unknown'],
['Assignee', data.assignee || 'Unassigned'],
['Created', data.created ? new Date(data.created).toLocaleDateString() : 'Unknown'],
['Modified', data.modified ? new Date(data.modified).toLocaleDateString() : 'Unknown']
];
output += table(infoData) + '\n';
if (data.description && data.description.length > 10) {
output += chalk.bold.yellow('Description:\n');
output += data.description + '\n\n';
}
if (data.relatedCLs && data.relatedCLs.length > 0) {
output += chalk.bold.yellow('Related CLs:\n');
data.relatedCLs.forEach((cl: string) => {
output += `- CL ${cl}: https://chromium-review.googlesource.com/c/chromium/src/+/${cl}\n`;
});
output += '\n';
}
output += chalk.blue(`🔗 View issue: ${data.browserUrl}\n`);
return output;
}
function formatIssuePlain(data: any): string {
if (!data) return chalk.red('No issue data found');
if (data.error) {
let output = chalk.red(`Error: ${data.error}\n`);
output += chalk.blue(`🔗 View issue: ${data.browserUrl}\n`);
return output;
}
let output = chalk.bold.cyan(`Issue ${data.issueId}: ${data.title || 'Unknown Title'}\n`);
output += chalk.gray('═'.repeat(80)) + '\n\n';
// Issue metadata
output += chalk.yellow('Status: ') + (data.status || 'Unknown') + '\n';
output += chalk.yellow('Priority: ') + (data.priority || 'Unknown') + '\n';
output += chalk.yellow('Type: ') + (data.type || 'Unknown') + '\n';
output += chalk.yellow('Severity: ') + (data.severity || 'Unknown') + '\n';
output += chalk.yellow('Reporter: ') + (data.reporter || 'Unknown') + '\n';
output += chalk.yellow('Assignee: ') + (data.assignee || 'Unassigned') + '\n';
output += chalk.yellow('Created: ') + (data.created ? new Date(data.created).toLocaleDateString() : 'Unknown') + '\n';
output += chalk.yellow('Modified: ') + (data.modified ? new Date(data.modified).toLocaleDateString() : 'Unknown') + '\n';
if (data.extractionMethod) {
output += chalk.gray(`Data source: ${data.extractionMethod}`) + '\n';
}
output += '\n';
// Issue description (first comment)
if (data.description && data.description.length > 10) {
output += chalk.bold.yellow('📝 Description:\n');
output += chalk.gray('─'.repeat(40)) + '\n';
output += formatCommentContent(data.description) + '\n\n';
}
// Comments
if (data.comments && data.comments.length > 0) {
output += chalk.bold.yellow(`💬 Comments (${data.comments.length}):\n`);
output += chalk.gray('─'.repeat(40)) + '\n';
data.comments.forEach((comment: any, index: number) => {
output += chalk.bold.green(`Comment #${index + 1}\n`);
output += chalk.blue(`👤 ${comment.author || 'Unknown'}`);
if (comment.timestamp) {
const date = new Date(comment.timestamp);
output += chalk.gray(` • ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`);
}
output += '\n';
output += formatCommentContent(comment.content) + '\n';
if (index < data.comments.length - 1) {
output += chalk.gray('┈'.repeat(30)) + '\n';
}
});
output += '\n';
}
// Related CLs
if (data.relatedCLs && data.relatedCLs.length > 0) {
output += chalk.bold.yellow('🔗 Related CLs:\n');
data.relatedCLs.forEach((cl: string) => {
output += ` • CL ${cl}: https://chromium-review.googlesource.com/c/chromium/src/+/${cl}\n`;
});
output += '\n';
}
output += chalk.gray('═'.repeat(80)) + '\n';
output += chalk.blue(`🌐 View issue: ${data.browserUrl}\n`);
return output;
}
function formatCommentContent(content: string): string {
if (!content) return chalk.gray('(no content)');
// Split into paragraphs and format nicely
const paragraphs = content.split('\n\n').filter(p => p.trim().length > 0);
return paragraphs.map(paragraph => {
// Wrap long lines
const words = paragraph.trim().split(/\s+/);
const lines = [];
let currentLine = '';
for (const word of words) {
if (currentLine.length + word.length + 1 > 78) {
if (currentLine) {
lines.push(currentLine.trim());
currentLine = word;
} else {
lines.push(word); // Word too long, keep as is
}
} else {
currentLine += (currentLine ? ' ' : '') + word;
}
}
if (currentLine) {
lines.push(currentLine.trim());
}
return lines.map(line => ` ${line}`).join('\n');
}).join('\n\n');
}
function formatCommitMessage(message: string): string {
if (!message) return chalk.gray('(no commit message)');
// Split message into lines and format nicely
const lines = message.split('\n').filter(line => line.trim().length > 0);
return lines.map((line, index) => {
// First line (subject) should be bold
if (index === 0) {
return ` ${chalk.bold(line.trim())}`;
}
// Subsequent lines with proper indentation
const trimmedLine = line.trim();
// Special formatting for common patterns
if (trimmedLine.startsWith('Bug:')) {
return ` ${chalk.yellow(trimmedLine)}`;
} else if (trimmedLine.startsWith('Change-Id:')) {
return ` ${chalk.blue(trimmedLine)}`;
} else if (trimmedLine.startsWith('- https://crrev.com/')) {
return ` ${chalk.cyan(trimmedLine)}`;
} else if (trimmedLine.match(/^https?:\/\//)) {
return ` ${chalk.cyan(trimmedLine)}`;
} else {
return ` ${trimmedLine}`;
}
}).join('\n');
}
function formatIssueSearchTable(data: any): string {
if (!data || !data.issues || data.issues.length === 0) {
return chalk.yellow('No issues found');
}
let output = chalk.bold.cyan(`Found ${data.total} issues for query: "${data.query}"\n\n`);
const tableData = [
['ID', 'Title', 'Type', 'Assignee', 'Status', '7D Views', 'Modified', 'Has CL']
];
data.issues.forEach((issue: any) => {
tableData.push([
issue.issueId,
(issue.title || 'No title').substring(0, 35) + '...',
issue.type || 'Unknown',
issue.assignee ? issue.assignee.split('@')[0] : 'None',
issue.status || 'Unknown',
(issue.views7Days || 0).toString(),
issue.modified ? new Date(issue.modified).toLocaleDateString() : 'Unknown',
issue.hasCL ? '✓' : '-'
]);
});
output += table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
if (data.searchUrl) {
output += '\n' + chalk.blue(`🔗 Web search: ${data.searchUrl}\n`);
}
return output;
}
function formatIssueSearchPlain(data: any): string {
if (!data || !data.issues || data.issues.length === 0) {
return chalk.yellow('No issues found');
}
let output = chalk.bold.cyan(`Found ${data.total} issues for query: "${data.query}"\n\n`);
data.issues.forEach((issue: any, index: number) => {
output += chalk.bold.green(`${index + 1}. Issue ${issue.issueId}\n`);
output += chalk.yellow('Title: ') + (issue.title || 'No title') + '\n';
output += chalk.yellow('Type: ') + (issue.type || 'Unknown') + '\n';
output += chalk.yellow('Status: ') + (issue.status || 'Unknown') + '\n';
output += chalk.yellow('Assignee: ') + (issue.assignee || 'None') + '\n';
output += chalk.yellow('7-Day Views: ') + (issue.views7Days || 0) + '\n';
output += chalk.yellow('Modified: ') + (issue.modified ? new Date(issue.modified).toLocaleDateString() : 'Unknown') + '\n';
output += chalk.yellow('Has CL: ') + (issue.hasCL ? 'Yes' : 'No') + '\n';
if (issue.hasCL && issue.clInfo) {
output += chalk.gray(' CL Info: ') + issue.clInfo + '\n';
}
output += chalk.blue(`🔗 ${issue.browserUrl}\n`);
if (index < data.issues.length - 1) {
output += chalk.gray('─'.repeat(60)) + '\n';
}
});
output += '\n';
if (data.searchUrl) {
output += chalk.blue(`🌐 Web search: ${data.searchUrl}\n`);
}
return output;
}
function formatGerritBotsTable(data: any): string {
if (data.message) {
return data.message;
}
const rows = data.bots.map((bot: any) => [
bot.name,
getStatusIcon(bot.status) + ' ' + bot.status,
bot.summary || '',
bot.buildUrl || bot.luciUrl || '',
]);
return table([
['Bot Name', 'Status', 'Summary', 'URL'],
...rows
], {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
},
header: {
alignment: 'center',
content: `Try-Bot Status for CL ${data.clId} (Patchset ${data.patchset})\n` +
`📊 Total: ${data.totalBots} | ✅ Passed: ${data.passedBots} | ❌ Failed: ${data.failedBots} | 🔄 Running: ${data.runningBots}`
}
});
}
function formatGerritBotsPlain(data: any): string {
if (data.message) {
return data.message;
}
let output = chalk.bold(`Try-Bot Status for CL ${data.clId}\n`);
output += chalk.gray('─'.repeat(50)) + '\n';
output += chalk.cyan(`Patchset: ${data.patchset}\n`);
output += chalk.cyan(`LUCI Run: ${data.runId || 'N/A'}\n\n`);
output += chalk.bold('📊 Summary:\n');
output += ` Total: ${data.totalBots}\n`;
output += ` ✅ Passed: ${data.passedBots}\n`;
output += ` ❌ Failed: ${data.failedBots}\n`;
output += ` 🔄 Running: ${data.runningBots}\n`;
if (data.canceledBots > 0) {
output += ` ⏹️ Canceled: ${data.canceledBots}\n`;
}
output += '\n';
if (data.bots.length === 0) {
output += chalk.yellow('No bot results to display\n');
return output;
}
output += chalk.bold('🤖 Bots:\n');
data.bots.forEach((bot: any, index: number) => {
const statusIcon = getStatusIcon(bot.status);
output += `${statusIcon} ${chalk.bold(bot.name)} - ${bot.status}\n`;
if (bot.summary) {
output += chalk.gray(` ${bot.summary}\n`);
}
if (bot.failureStep) {
output += chalk.red(` Failed step: ${bot.failureStep}\n`);
}
if (bot.buildUrl) {
output += chalk.blue(` 🔗 Build: ${bot.buildUrl}\n`);
} else if (bot.luciUrl) {
output += chalk.blue(` 🔗 LUCI: ${bot.luciUrl}\n`);
}
if (index < data.bots.length - 1) {
output += '\n';
}
});
if (data.luciUrl) {
output += '\n' + chalk.blue(`🌐 Full LUCI report: ${data.luciUrl}\n`);
}
return output;
}
function getStatusIcon(status: string): string {
switch (status.toUpperCase()) {
case 'PASSED': return '✅';
case 'FAILED': return '❌';
case 'RUNNING': return '🔄';
case 'CANCELED': return '⏹️';
case 'UNKNOWN': return '❓';
default: return '⚪';
}
}
function formatListFolderTable(data: any): string {
if (!data || !data.items || data.items.length === 0) {
return chalk.yellow('No items found in folder');
}
const tableData = [
['Type', 'Name']
];
data.items.forEach((item: any) => {
const icon = item.type === 'folder' ? '📁' : '📄';
const name = item.type === 'folder' ? `${item.name}/` : item.name;
tableData.push([icon, name]);
});
return `${chalk.bold(`📁 ${data.path}`)}\n\n` +
`Folders: ${data.folders} | Files: ${data.files} | Total: ${data.totalItems}\n\n` +
table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
}) +
`\n${chalk.blue(`🔗 ${data.browserUrl}`)}`;
}
function formatListFolderPlain(data: any): string {
if (!data || !data.items || data.items.length === 0) {
return chalk.yellow('No items found in folder');
}
let output = chalk.bold(`📁 ${data.path}\n\n`);
output += `📊 Summary: ${data.folders} folders, ${data.files} files (${data.totalItems} total)\n`;
if (data.source) {
output += chalk.yellow(`📌 Source: ${data.source}\n`);
}
output += '\n';
// Separate folders and files
const folders = data.items.filter((item: any) => item.type === 'folder');
const files = data.items.filter((item: any) => item.type === 'file');
if (folders.length > 0) {
output += chalk.bold('📁 Folders:\n');
folders.forEach((folder: any) => {
output += ` ${folder.name}/\n`;
});
if (files.length > 0) output += '\n';
}
if (files.length > 0) {
output += chalk.bold('📄 Files:\n');
files.forEach((file: any) => {
output += ` ${file.name}\n`;
});
}
output += '\n' + chalk.blue(`🔗 ${data.browserUrl}\n`);
if (data.githubUrl) {
output += chalk.blue(`🔗 GitHub: ${data.githubUrl}\n`);
}
if (data.webrtcUrl) {
output += chalk.blue(`🔗 WebRTC: ${data.webrtcUrl}\n`);
}
return output;
}
function formatGerritListTable(cls: any[]): string {
if (!cls || cls.length === 0) {
return chalk.yellow('No CLs found');
}
const tableData = [
['', 'Subject', 'Status', 'Owner', 'Reviewers', 'Repo', 'Branch', 'Updated', 'Size', 'CR', 'V', 'Q']
];
cls.forEach(cl => {
const clNumber = cl._number || cl.id;
const status = cl.status || 'UNKNOWN';
const statusIcon = getGerritStatusIcon(status);
const subject = `${clNumber}: ${cl.subject || 'No subject'}`;
const truncatedSubject = subject.length > 60 ? subject.substring(0, 57) + '...' : subject;
// Get owner email (remove @chromium.org for display)
const ownerEmail = cl.owner?.email || cl.owner?.name || 'Unknown';
const owner = ownerEmail.replace('@chromium.org', '').replace('@google.com', '');
// Get reviewers
const reviewers: string[] = [];
if (cl.reviewers && cl.reviewers.REVIEWER) {
cl.reviewers.REVIEWER.forEach((r: any) => {
const email = r.email || r.name || '';
reviewers.push(email.replace('@chromium.org', '').replace('@google.com', ''));
});
}
const reviewerStr = reviewers.slice(0, 2).join(', ') + (reviewers.length > 2 ? '...' : '');
// Get project/repo
const project = cl.project || 'chromium/src';
const shortProject = project.replace('chromium/', '');
// Get branch
const branch = cl.branch || 'main';
// Format updated time
const updated = new Date(cl.updated);
const now = new Date();
const diffMs = now.getTime() - updated.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
let updatedStr;
if (diffHours < 24) {
updatedStr = `${diffHours}h ago`;
} else if (diffDays < 30) {
updatedStr = `${diffDays}d ago`;
} else {
updatedStr = updated.toLocaleDateString();
}
// Get size
const size = `+${cl.insertions || 0},-${cl.deletions || 0}`;
// Get labels
const cr = getLabelValue(cl.labels, 'Code-Review');
const v = getLabelValue(cl.labels, 'Verified');
const cq = getLabelValue(cl.labels, 'Commit-Queue');
tableData.push([
statusIcon,
truncatedSubject,
status,
owner,
reviewerStr,
shortProject,
branch,
updatedStr,
size,
cr,
v,
cq
]);
});
return chalk.cyan(`Found ${cls.length} CLs\n\n`) +
table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
}
function formatGerritListPlain(cls: any[]): string {
if (!cls || cls.length === 0) {
return chalk.yellow('No CLs found');
}
let output = chalk.cyan(`Found ${cls.length} CLs\n\n`);
cls.forEach((cl, index) => {
const clNumber = cl._number || cl.id;
const status = cl.status || 'UNKNOWN';
const statusEmoji = getGerritStatusIcon(status);
output += chalk.bold(`${index + 1}. ${statusEmoji} CL ${clNumber}: ${cl.subject}\n`);
output += chalk.gray('─'.repeat(80)) + '\n';
// Get owner email
const ownerEmail = cl.owner?.email || cl.owner?.name || 'Unknown';
const ownerDisplay = ownerEmail.replace('@chromium.org', '').replace('@google.com', '');
output += ` Owner: ${ownerDisplay} (${ownerEmail})\n`;
// Get reviewers
if (cl.reviewers && cl.reviewers.REVIEWER && cl.reviewers.REVIEWER.length > 0) {
const reviewers = cl.reviewers.REVIEWER.map((r: any) => {
const email = r.email || r.name || '';
return email.replace('@chromium.org', '').replace('@google.com', '');
});
output += ` Reviewers: ${reviewers.join(', ')}\n`;
}
output += ` Status: ${status}\n`;
output += ` Repo: ${cl.project || 'chromium/src'}\n`;
output += ` Branch: ${cl.branch || 'main'}\n`;
output += ` Created: ${new Date(cl.created).toLocaleDateString()}\n`;
output += ` Updated: ${new Date(cl.updated).toLocaleDateString()}\n`;
if (cl.current_revision_number) {
output += ` Patchset: ${cl.current_revision_number}\n`;
}
if (cl.insertions || cl.deletions) {
output += ` Changes: ${chalk.green(`+${cl.insertions || 0}`)} / ${chalk.red(`-${cl.deletions || 0}`)}\n`;
}
if (cl.total_comment_count > 0) {
output += ` Comments: ${cl.total_comment_count} (${cl.unresolved_comment_count} unresolved)\n`;
}
// Add labels if present
if (cl.labels) {
const importantLabels = ['Code-Review', 'Commit-Queue', 'Auto-Submit'];
const labelText = [];
for (const label of importantLabels) {
if (cl.labels[label]) {
const values = cl.labels[label].all || [];
const maxValue = Math.max(...values.map((v: any) => v.value || 0));
const minValue = Math.min(...values.map((v: any) => v.value || 0));
if (maxValue > 0) {
labelText.push(`${label}: ${chalk.green(`+${maxValue}`)}`);
} else if (minValue < 0) {
labelText.push(`${label}: ${chalk.red(`${minValue}`)}`);
}
}
}
if (labelText.length > 0) {
output += ` Labels: ${labelText.join(', ')}\n`;
}
}
output += chalk.blue(` 🔗 https://chromium-review.googlesource.com/c/chromium/src/+/${clNumber}\n`);
output += '\n';
});
return output;
}
function getGerritStatusIcon(status: string): string {
switch (status.toUpperCase()) {
case 'NEW':
case 'OPEN':
return '🔵';
case 'MERGED':
return '✅';
case 'ABANDONED':
return '❌';
default:
return '⚪';
}
}
function getLabelValue(labels: any, labelName: string): string {
if (!labels || !labels[labelName]) return '';
const label = labels[labelName];
const values = label.all || [];
const maxValue = Math.max(...values.filter((v: any) => v.value > 0).map((v: any) => v.value || 0), 0);
const minValue = Math.min(...values.filter((v: any) => v.value < 0).map((v: any) => v.value || 0), 0);
if (minValue < 0) {
return chalk.red(`${minValue}`);
} else if (maxValue > 0) {
return chalk.green(`+${maxValue}`);
}
return '';
}
function formatPdfiumGerritListTable(cls: any[]): string {
if (!cls || cls.length === 0) {
return chalk.yellow('No PDFium CLs found');
}
const tableData = [
['', 'Subject', 'Status', 'Owner', 'Reviewers', 'Repo', 'Branch', 'Updated', 'Size', 'CR', 'V', 'Q']
];
cls.forEach(cl => {
const clNumber = cl._number || cl.id;
const status = cl.status || 'UNKNOWN';
const statusIcon = getGerritStatusIcon(status);
const subject = `${clNumber}: ${cl.subject || 'No subject'}`;
const truncatedSubject = subject.length > 60 ? subject.substring(0, 57) + '...' : subject;
// Get owner email (remove @chromium.org for display)
const ownerEmail = cl.owner?.email || cl.owner?.name || 'Unknown';
const owner = ownerEmail.replace('@chromium.org', '').replace('@google.com', '');
// Get reviewers
const reviewers: string[] = [];
if (cl.reviewers && cl.reviewers.REVIEWER) {
cl.reviewers.REVIEWER.forEach((r: any) => {
const email = r.email || r.name || '';
reviewers.push(email.replace('@chromium.org', '').replace('@google.com', ''));
});
}
const reviewerStr = reviewers.slice(0, 2).join(', ') + (reviewers.length > 2 ? '...' : '');
// Get project/repo
const project = cl.project || 'pdfium';
const shortProject = project;
// Get branch
const branch = cl.branch || 'main';
// Format updated time
const updated = new Date(cl.updated);
const now = new Date();
const diffMs = now.getTime() - updated.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
let updatedStr;
if (diffHours < 24) {
updatedStr = `${diffHours}h ago`;
} else if (diffDays < 30) {
updatedStr = `${diffDays}d ago`;
} else {
updatedStr = updated.toLocaleDateString();
}
// Get size
const size = `+${cl.insertions || 0},-${cl.deletions || 0}`;
// Get labels
const cr = getLabelValue(cl.labels, 'Code-Review');
const v = getLabelValue(cl.labels, 'Verified');
const cq = getLabelValue(cl.labels, 'Commit-Queue');
tableData.push([
statusIcon,
truncatedSubject,
status,
owner,
reviewerStr,
shortProject,
branch,
updatedStr,
size,
cr,
v,
cq
]);
});
return table(tableData);
}
function formatPdfiumGerritListPlain(cls: any[]): string {
if (!cls || cls.length === 0) {
return 'No PDFium CLs found';
}
let output = chalk.bold.blue(`📋 Found ${cls.length} PDFium CL${cls.length !== 1 ? 's' : ''}\n\n`);
cls.forEach((cl, index) => {
const clNumber = cl._number;
const status = getGerritStatusIcon(cl.status);
const subject = cl.subject || 'No subject';
output += chalk.bold(`${index + 1}. ${status} CL ${clNumber}: ${subject}\n`);
output += chalk.gray('─'.repeat(60)) + '\n';
output += ` Author: ${cl.owner?.name || 'Unknown'} (${cl.owner?.email || 'no email'})\n`;
output += ` Status: ${status}\n`;
output += ` Created: ${new Date(cl.created).toLocaleDateString()}\n`;
output += ` Updated: ${new Date(cl.updated).toLocaleDateString()}\n`;
if (cl.current_revision_number) {
output += ` Patchset: ${cl.current_revision_number}\n`;
}
if (cl.insertions || cl.deletions) {
output += ` Changes: ${chalk.green(`+${cl.insertions || 0}`)} / ${chalk.red(`-${cl.deletions || 0}`)}\n`;
}
if (cl.total_comment_count > 0) {
output += ` Comments: ${cl.total_comment_count} (${cl.unresolved_comment_count} unresolved)\n`;
}
// Add labels if present
if (cl.labels) {
const importantLabels = ['Code-Review', 'Commit-Queue', 'Auto-Submit'];
const labelText = [];
for (const label of importantLabels) {
if (cl.labels[label]) {
const values = cl.labels[label].all || [];
const maxValue = Math.max(...values.map((v: any) => v.value || 0));
const minValue = Math.min(...values.map((v: any) => v.value || 0));
if (maxValue > 0) {
labelText.push(`${label}: ${chalk.green(`+${maxValue}`)}`);
} else if (minValue < 0) {
labelText.push(`${label}: ${chalk.red(`${minValue}`)}`);
}
}
}
if (labelText.length > 0) {
output += ` Labels: ${labelText.join(', ')}\n`;
}
}
output += chalk.blue(` 🔗 https://pdfium-review.googlesource.com/c/pdfium/+/${clNumber}\n`);
output += '\n';
});
return output;
}
function formatCIErrorsTable(data: any): string {
if (!data) return chalk.red('No CI build data found');
let output = chalk.bold.cyan(`CI Build: ${data.builder} #${data.buildNumber}\n\n`);
if (data.error) {
return output + chalk.red(data.error);
}
const infoData = [
['Property', 'Value'],
['Project', data.project || 'Unknown'],
['Bucket', data.bucket || 'Unknown'],
['Builder', data.builder || 'Unknown'],
['Build Number', data.buildNumber || 'Unknown'],
['Status', data.buildStatus || 'Unknown'],
['Total Tests', data.totalTests?.toString() || '0'],
['Failed Tests', data.failedTestCount?.toString() || '0']
];
output += table(infoData) + '\n';
output += chalk.blue(`🔗 ${data.buildUrl}\n\n`);
if (data.failedTests && data.failedTests.length > 0) {
output += chalk.bold.red(`❌ Failed Tests (${data.failedTests.length}):\n\n`);
data.failedTests.forEach((test: any, index: number) => {
output += chalk.bold.yellow(`${index + 1}. ${test.testName}\n`);
output += chalk.gray(' Test ID: ') + test.testId + '\n';
output += chalk.gray(' Status: ') + test.status + '\n';
if (test.location) {
output += chalk.blue(` 📁 ${test.location.fileName}\n`);
}
// Show detailed error output if available (includes stack traces)
if (test.detailedError) {
output += chalk.red('\n 🔍 Detailed Error Output:\n');
output += chalk.gray(' ' + '─'.repeat(78) + '\n');
// Split the detailed error into lines and display them
const errorLines = test.detailedError.split('\n');
errorLines.slice(0, 50).forEach((line: string) => {
// Highlight certain patterns
if (line.includes('FAILED') || line.includes('FAIL')) {
output += chalk.red(` ${line}\n`);
} else if (line.includes('Expected:') || line.includes('Actual:')) {
output += chalk.yellow(` ${line}\n`);
} else if (line.match(/^\s*#\d+\s+0x/)) {
// Stack trace lines
output += chalk.gray(` ${line}\n`);
} else if (line.includes('.cc:') || line.includes('.h:')) {
// File references
output += chalk.cyan(` ${line}\n`);
} else {
output += chalk.white(` ${line}\n`);
}
});
if (errorLines.length > 50) {
output += chalk.gray(` ... (${errorLines.length - 50} more lines)\n`);
}
output += chalk.gray(' ' + '─'.repeat(78) + '\n');
} else if (test.errorMessages && test.errorMessages.length > 0) {
// Fallback to basic error messages if detailed error not available
output += chalk.red(' Error:\n');
test.errorMessages.forEach((msg: string) => {
const lines = msg.split('\n');
lines.forEach((line: string) => {
output += chalk.red(` ${line}\n`);
});
});
}
output += '\n';
});
} else {
output += chalk.green('✅ No test failures found\n');
}
return output;
}
function formatCIErrorsPlain(data: any): string {
return formatCIErrorsTable(data); // Same formatting for plain
}
function formatGerritBotErrorsTable(data: any): string {
if (!data) return chalk.red('No bot error data found');
let output = chalk.bold.cyan(`Bot Errors for CL ${data.clId} (Patchset ${data.patchset})\n\n`);
if (data.message) {
return output + chalk.yellow(data.message);
}
const infoData = [
['Property', 'Value'],
['CL ID', data.clId || 'Unknown'],
['Patchset', data.patchset?.toString() || 'Unknown'],
['Total Bots', data.totalBots?.toString() || '0'],
['Failed Bots', data.failedBots?.toString() || '0'],
['Bots with Errors', data.botsWithErrors?.toString() || '0']
];
output += table(infoData) + '\n';
if (data.luciUrl) {
output += chalk.blue(`🔗 LUCI Run: ${data.luciUrl}\n\n`);
}
if (data.bots && data.bots.length > 0) {
output += chalk.bold.red(`❌ Bot Errors (${data.bots.length}):\n\n`);
data.bots.forEach((bot: any, index: number) => {
output += chalk.bold.yellow(`${index + 1}. ${bot.botName}\n`);
output += chalk.gray(' Status: ') + bot.status + '\n';
if (bot.buildUrl) {
output += chalk.blue(` 🔗 Build: ${bot.buildUrl}\n`);
}
if (bot.error) {
output += chalk.red(` Error: ${bot.error}\n`);
} else if (bot.errors) {
const errors = bot.errors;
output += chalk.gray(` Build Status: ${errors.buildStatus}\n`);
output += chalk.gray(` Failed Tests: ${errors.failedTestCount}/${errors.totalTests}\n`);
if (errors.failedTests && errors.failedTests.length > 0) {
output += chalk.red(` \n Test Failures (showing first 5):\n`);
errors.failedTests.slice(0, 5).forEach((test: any, testIndex: number) => {
output += chalk.yellow(` ${testIndex + 1}. ${test.testName}\n`);
// Show detailed error if available (includes stack traces)
if (test.detailedError) {
const errorLines = test.detailedError.split('\n');
errorLines.slice(0, 30).forEach((line: string) => {
if (line.includes('FAILED') || line.includes('FAIL')) {
output += chalk.red(` ${line}\n`);
} else if (line.includes('Expected:') || line.includes('Actual:')) {
output += chalk.yellow(` ${line}\n`);
} else if (line.match(/^\s*#\d+\s+0x/)) {
output += chalk.gray(` ${line}\n`);
} else if (line.includes('.cc:') || line.includes('.h:')) {
output += chalk.cyan(` ${line}\n`);
} else {
output += chalk.white(` ${line}\n`);
}
});
if (errorLines.length > 30) {
output += chalk.gray(` ... (${errorLines.length - 30} more lines)\n`);
}
} else if (test.errorMessages && test.errorMessages.length > 0) {
// Fallback to basic error messages
const firstError = test.errorMessages[0];
const lines = firstError.split('\n').slice(0, 3);
lines.forEach((line: string) => {
output += chalk.red(` ${line}\n`);
});
if (firstError.split('\n').length > 3) {
output += chalk.gray(' ...\n');
}
}
});
if (errors.failedTests.length > 5) {
output += chalk.gray(` ... and ${errors.failedTests.length - 5} more failed tests\n`);
}
}
}
output += '\n';
});
} else {
output += chalk.green('✅ No bot errors found\n');
}
return output;
}
function formatGerritBotErrorsPlain(data: any): string {
return formatGerritBotErrorsTable(data); // Same formatting for plain
}
// Helper interface for comment threading
interface CommentThread {
rootComment: any;
replies: any[];
}
// Build threaded comment structure
function buildCommentThreads(comments: any[]): CommentThread[] {
const threads: CommentThread[] = [];
const commentMap = new Map<string, any>();
// First pass: index all comments by ID
comments.forEach(comment => {
commentMap.set(comment.id, comment);
});
// Second pass: build threads
comments.forEach(comment => {
if (!comment.in_reply_to) {
// This is a root comment
threads.push({
rootComment: comment,
replies: []
});
}
});
// Third pass: attach replies to threads
comments.forEach(comment => {
if (comment.in_reply_to) {
// Find the thread this belongs to
const thread = findThreadForComment(comment, threads, commentMap);
if (thread) {
thread.replies.push(comment);
}
}
});
// Sort replies chronologically within each thread
threads.forEach(thread => {
thread.replies.sort((a, b) => {
const dateA = new Date(a.updated).getTime();
const dateB = new Date(b.updated).getTime();
return dateA - dateB;
});
});
return threads;
}
// Find which thread a reply belongs to
function findThreadForComment(comment: any, threads: CommentThread[], commentMap: Map<string, any>): CommentThread | null {
let current = comment;
// Walk up the reply chain to find the root
while (current.in_reply_to) {
const parent = commentMap.get(current.in_reply_to);
if (!parent) break;
if (!parent.in_reply_to) {
// Found root, now find its thread
return threads.find(t => t.rootComment.id === parent.id) || null;
}
current = parent;
}
return null;
}
// Format a single comment with proper indentation
function formatSingleComment(comment: any, indent: number = 0, isReply: boolean = false): string {
const indentStr = ' '.repeat(indent);
const replyMarker = isReply ? '↳ ' : '';
let output = '';
// Author and metadata
const author = comment.author?.display_name || comment.author?.name || 'Unknown';
const timestamp = new Date(comment.updated).toLocaleString();
const unresolvedTag = comment.unresolved ? chalk.yellow(' [UNRESOLVED]') : chalk.gray(' [resolved]');
output += chalk.bold.cyan(`${indentStr}${replyMarker}${author}`) +
chalk.gray(` • ${timestamp}`) +
unresolvedTag + '\n';
// Patchset info
if (comment.patch_set) {
output += chalk.gray(`${indentStr}Patchset ${comment.patch_set}`) + '\n';
}
// Line reference
if (comment.line) {
const lineInfo = comment.range ?
`Lines ${comment.range.start_line}-${comment.range.end_line}` :
`Line ${comment.line}`;
output += chalk.blue(`${indentStr}📍 ${lineInfo}`) + '\n';
}
// Comment message
if (comment.message) {
const messageLines = comment.message.split('\n');
messageLines.forEach((line: string) => {
output += chalk.white(`${indentStr} ${line}`) + '\n';
});
}
return output;
}
function formatGerritCommentsTable(data: any): string {
if (!data || Object.keys(data).length === 0) {
return chalk.yellow('No comments found');
}
let output = chalk.bold.cyan('📝 Gerrit Comments\n\n');
// Process each file
for (const [filePath, comments] of Object.entries(data)) {
const commentArray = comments as any[];
if (commentArray.length === 0) continue;
// File header
const displayPath = filePath === '/PATCHSET_LEVEL' ?
chalk.yellow('General Comments (Patchset Level)') :
chalk.green(filePath);
output += chalk.bold(`\n${displayPath}\n`);
output += chalk.gray('═'.repeat(80)) + '\n\n';
// Build threads for this file
const threads = buildCommentThreads(commentArray);
// Display each thread
threads.forEach((thread, threadIndex) => {
// Thread header
const threadNum = threadIndex + 1;
const replyCount = thread.replies.length;
const threadStatus = thread.rootComment.unresolved || thread.replies.some((r: any) => r.unresolved) ?
chalk.yellow('🔔 Active Discussion') :
chalk.gray('✓ Resolved');
output += chalk.bold.white(`Thread #${threadNum}`) +
chalk.gray(` (${replyCount} ${replyCount === 1 ? 'reply' : 'replies'})`) +
` ${threadStatus}\n`;
output += chalk.gray('─'.repeat(78)) + '\n';
// Root comment
output += formatSingleComment(thread.rootComment, 0, false);
// Replies with indentation
thread.replies.forEach((reply, replyIndex) => {
output += '\n';
output += formatSingleComment(reply, 1, true);
});
output += '\n';
});
}
// Summary
const totalComments = Object.values(data).reduce((sum: number, comments: any) => sum + comments.length, 0);
const totalUnresolved = Object.values(data).reduce((sum: number, comments: any) => {
return sum + comments.filter((c: any) => c.unresolved).length;
}, 0);
output += chalk.gray('═'.repeat(80)) + '\n';
output += chalk.bold(`📊 Summary: ${totalComments} comments (${totalUnresolved} unresolved)\n`);
return output;
}
function formatGerritCommentsPlain(data: any): string {
return formatGerritCommentsTable(data); // Same formatting
}
function formatBlameTable(data: any): string {
if (!data || !data.lines || data.lines.length === 0) {
return chalk.yellow('No blame data found');
}
let output = chalk.bold.cyan(`Git Blame: ${data.filePath}\n\n`);
output += chalk.gray(`Total lines: ${data.totalLines}\n`);
if (data.lineNumber) {
output += chalk.gray(`Showing line: ${data.lineNumber}\n`);
}
output += chalk.blue(`🔗 ${data.blameUrl}\n\n`);
const tableData = [
['Line', 'Commit', 'Author', 'Date', 'Content']
];
data.lines.forEach((line: any) => {
tableData.push([
line.lineNumber.toString(),
line.commit.substring(0, 8),
line.author.substring(0, 20),
line.date.substring(0, 10),
line.content.substring(0, 50) + (line.content.length > 50 ? '...' : '')
]);
});
return output + table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
}
function formatBlamePlain(data: any): string {
if (!data || !data.lines || data.lines.length === 0) {
return chalk.yellow('No blame data found');
}
let output = chalk.bold.cyan(`Git Blame: ${data.filePath}\n`);
output += chalk.gray('═'.repeat(80)) + '\n\n';
output += chalk.gray(`Total lines: ${data.totalLines}\n`);
if (data.lineNumber) {
output += chalk.gray(`Showing line: ${data.lineNumber}\n`);
}
output += chalk.blue(`🔗 ${data.blameUrl}\n\n`);
data.lines.forEach((line: any) => {
output += chalk.bold.green(`Line ${line.lineNumber}\n`);
output += chalk.yellow(' Commit: ') + `${line.commit.substring(0, 8)} (${line.date})\n`;
output += chalk.yellow(' Author: ') + line.author + '\n';
output += chalk.yellow(' Code: ') + chalk.white(line.content) + '\n';
output += chalk.blue(` 🔗 https://chromium.googlesource.com/chromium/src/+/${line.commit}\n\n`);
});
return output;
}
function formatHistoryTable(data: any): string {
if (!data || !data.commits || data.commits.length === 0) {
return chalk.yellow('No commit history found');
}
let output = chalk.bold.cyan(`Commit History: ${data.filePath}\n\n`);
output += chalk.gray(`Total commits: ${data.totalCommits}\n`);
output += chalk.blue(`🔗 ${data.historyUrl}\n\n`);
const tableData = [
['Commit', 'Author', 'Date', 'Message']
];
data.commits.forEach((commit: any) => {
tableData.push([
commit.commit.substring(0, 8),
commit.author.substring(0, 20),
commit.date.substring(0, 10),
commit.message.substring(0, 50) + (commit.message.length > 50 ? '...' : '')
]);
});
return output + table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
}
function formatHistoryPlain(data: any): string {
if (!data || !data.commits || data.commits.length === 0) {
return chalk.yellow('No commit history found');
}
let output = chalk.bold.cyan(`Commit History: ${data.filePath}\n`);
output += chalk.gray('═'.repeat(80)) + '\n\n';
output += chalk.gray(`Total commits: ${data.totalCommits}\n`);
output += chalk.blue(`🔗 ${data.historyUrl}\n\n`);
data.commits.forEach((commit: any, index: number) => {
output += chalk.bold.green(`${index + 1}. ${commit.commit.substring(0, 8)}\n`);
output += chalk.yellow(' Author: ') + commit.author + '\n';
output += chalk.yellow(' Date: ') + commit.date + '\n';
output += chalk.yellow(' Message: ') + commit.message + '\n';
output += chalk.blue(` 🔗 https://chromium.googlesource.com/chromium/src/+/${commit.commit}\n\n`);
});
return output;
}
function formatContributorsTable(data: any): string {
if (!data || !data.contributors || data.contributors.length === 0) {
return chalk.yellow('No contributors found');
}
let output = chalk.bold.cyan(`Top Contributors: ${data.path}\n\n`);
output += chalk.gray(`Total contributors: ${data.totalContributors}\n`);
output += chalk.gray(`Analyzed commits: ${data.analyzedCommits}\n\n`);
const tableData = [
['Rank', 'Author', 'Commits', 'Last Commit', 'Last Date']
];
data.contributors.forEach((contributor: any, index: number) => {
tableData.push([
(index + 1).toString(),
contributor.author.substring(0, 30),
contributor.commits.toString(),
contributor.lastCommit.substring(0, 8),
contributor.lastCommitDate.substring(0, 10)
]);
});
return output + table(tableData, {
border: {
topBody: '─',
topJoin: '┬',
topLeft: '┌',
topRight: '┐',
bottomBody: '─',
bottomJoin: '┴',
bottomLeft: '└',
bottomRight: '┘',
bodyLeft: '│',
bodyRight: '│',
bodyJoin: '│',
joinBody: '─',
joinLeft: '├',
joinRight: '┤',
joinJoin: '┼'
}
});
}
function formatContributorsPlain(data: any): string {
if (!data || !data.contributors || data.contributors.length === 0) {
return chalk.yellow('No contributors found');
}
let output = chalk.bold.cyan(`Top Contributors: ${data.path}\n`);
output += chalk.gray('═'.repeat(80)) + '\n\n';
output += chalk.gray(`Total contributors: ${data.totalContributors}\n`);
output += chalk.gray(`Analyzed commits: ${data.analyzedCommits}\n\n`);
data.contributors.forEach((contributor: any, index: number) => {
output += chalk.bold.green(`${index + 1}. ${contributor.author}\n`);
output += chalk.yellow(' Commits: ') + contributor.commits + '\n';
output += chalk.yellow(' Last commit: ') + `${contributor.lastCommit.substring(0, 8)} (${contributor.lastCommitDate})\n`;
output += chalk.blue(` 🔗 https://chromium.googlesource.com/chromium/src/+/${contributor.lastCommit}\n\n`);
});
return output;
}
function formatSuggestReviewersPlain(data: any, options?: any): string {
if (!data) {
return chalk.yellow('No reviewer suggestions available');
}
let output = '';
// Header
output += chalk.bold.cyan('╔═══════════════════════════════════════════════════════════════════════╗\n');
output += chalk.bold.cyan('║ ') + chalk.bold.white(`Suggested Reviewers for CL ${data.clNumber}`) + ' '.repeat(Math.max(0, 43 - data.clNumber.length)) + chalk.bold.cyan('║\n');
output += chalk.bold.cyan('╚═══════════════════════════════════════════════════════════════════════╝\n\n');
// Subject
if (data.subject) {
output += chalk.gray(`📝 ${data.subject}\n\n`);
}
// Summary
const optimalCount = data.optimalSet?.length || 0;
const fileCount = data.changedFiles?.length || 0;
output += chalk.bold(`📊 Summary: ${fileCount} files changed → `);
output += chalk.bold.green(`${optimalCount} reviewer${optimalCount !== 1 ? 's' : ''} recommended\n\n`);
// Analysis details
if (data.analysisDetails) {
output += chalk.gray(` 📁 ${data.analysisDetails.ownersFilesFetched} OWNERS files analyzed\n`);
output += chalk.gray(` 📅 ${data.analysisDetails.activityMonths} months of commit history\n\n`);
}
// Optimal Reviewer Set
if (data.optimalSet && data.optimalSet.length > 0) {
output += chalk.bold.green('┌─────────────────────────────────────────────────────────────────────────┐\n');
output += chalk.bold.green('│ 🎯 OPTIMAL REVIEWER SET │\n');
output += chalk.bold.green('├─────────────────────────────────────────────────────────────────────────┤\n');
data.optimalSet.forEach((reviewer: any, index: number) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : ' ';
const coveragePct = Math.round(reviewer.coverageScore * 100);
const filesCovered = reviewer.canReviewFiles?.length || 0;
const totalFiles = data.changedFiles?.length || 1;
output += chalk.green('│ ') + `${medal} ${chalk.bold.yellow(reviewer.email)}\n`;
output += chalk.green('│ ') + chalk.white(`Coverage: ${filesCovered}/${totalFiles} files (${coveragePct}%)`);
output += chalk.gray(` │ Activity: ${reviewer.recentCommits} commits\n`);
// Show files this reviewer covers
if (reviewer.canReviewFiles && reviewer.canReviewFiles.length > 0) {
const filesToShow = reviewer.canReviewFiles.slice(0, 5);
filesToShow.forEach((file: string) => {
const shortFile = file.length > 60 ? '...' + file.slice(-57) : file;
output += chalk.green('│ ') + chalk.gray(`└── ${shortFile}\n`);
});
if (reviewer.canReviewFiles.length > 5) {
output += chalk.green('│ ') + chalk.gray(` ... and ${reviewer.canReviewFiles.length - 5} more files\n`);
}
}
output += chalk.green('│\n');
});
output += chalk.bold.green('└─────────────────────────────────────────────────────────────────────────┘\n\n');
} else {
output += chalk.yellow('⚠️ No suitable reviewers found.\n\n');
}
// Uncovered files
if (data.uncoveredFiles && data.uncoveredFiles.length > 0) {
output += chalk.yellow('⚠️ Uncovered files (no specific owners found):\n');
data.uncoveredFiles.forEach((file: string) => {
output += chalk.gray(` - ${file}\n`);
});
output += '\n';
}
// File coverage summary
if (data.changedFiles && data.changedFiles.length > 0 && data.optimalSet && data.optimalSet.length > 0) {
output += chalk.bold.blue('┌─────────────────────────────────────────────────────────────────────────┐\n');
output += chalk.bold.blue('│ 📋 FILE COVERAGE │\n');
output += chalk.bold.blue('├─────────────────────────────────────────────────────────────────────────┤\n');
const fileToReviewer = new Map<string, string>();
data.optimalSet.forEach((reviewer: any) => {
reviewer.canReviewFiles?.forEach((file: string) => {
if (!fileToReviewer.has(file)) {
fileToReviewer.set(file, reviewer.email);
}
});
});
data.changedFiles.forEach((file: string) => {
const reviewer = fileToReviewer.get(file);
const shortFile = file.length > 45 ? '...' + file.slice(-42) : file;
if (reviewer) {
const shortReviewer = reviewer.length > 25 ? reviewer.slice(0, 22) + '...' : reviewer;
output += chalk.blue('│ ') + chalk.green('✅ ') + chalk.white(shortFile.padEnd(45)) + chalk.gray(` → ${shortReviewer}\n`);
} else {
output += chalk.blue('│ ') + chalk.yellow('⚠️ ') + chalk.white(shortFile.padEnd(45)) + chalk.gray(` → (no owner)\n`);
}
});
output += chalk.bold.blue('└─────────────────────────────────────────────────────────────────────────┘\n\n');
}
// Show all candidates if requested
if (options?.showAll && data.suggestedReviewers && data.suggestedReviewers.length > 0) {
output += chalk.bold('📈 All candidates ranked by score:\n\n');
data.suggestedReviewers.forEach((reviewer: any, index: number) => {
const coveragePct = Math.round(reviewer.coverageScore * 100);
const score = reviewer.combinedScore.toFixed(2);
const inOptimal = data.optimalSet?.some((r: any) => r.email === reviewer.email) ? chalk.green(' ✓') : '';
output += chalk.gray(` #${(index + 1).toString().padStart(2)} `);
output += chalk.yellow(reviewer.email.padEnd(35));
output += chalk.white(`Score: ${score} `);
output += chalk.gray(`(Coverage: ${coveragePct}%, Activity: ${reviewer.recentCommits})`);
output += inOptimal + '\n';
});
output += '\n';
}
// Gerrit link
output += chalk.blue(`🔗 CL: https://chromium-review.googlesource.com/c/chromium/src/+/${data.clNumber}\n`);
return output;
}