chromium-helper
Version:
CLI tool for searching and exploring Chromium source code via Google's official APIs
1,242 lines (1,241 loc) • 72.3 kB
JavaScript
import chalk from 'chalk';
import { table } from 'table';
export function formatOutput(data, format, context, options) {
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, context, options) {
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, context, options) {
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) {
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) {
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) {
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) => {
tableData.push([
result.file,
result.line.toString(),
result.content.replace(/\n/g, ' ').substring(0, 60) + '...'
]);
});
output += table(tableData) + '\n';
}
});
return output;
}
function formatSymbolResultsPlain(data) {
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, index) => {
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) {
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) {
return formatFileTable(data); // Same formatting for plain and table for files
}
function formatGerritStatusTable(data) {
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) {
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) {
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, index) => {
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) {
return formatOwnersTable(data); // Same formatting
}
function formatCommitsTable(data) {
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) => {
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) {
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, index) => {
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) {
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) => {
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) => {
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) {
return formatGerritDiffTable(data); // Same formatting for now
}
function formatGerritFileTable(data) {
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, index) => {
const lineNum = (index + 1).toString().padStart(4, ' ');
output += chalk.gray(lineNum + ': ') + line + '\n';
});
output += '─'.repeat(80) + '\n';
return output;
}
function formatGerritFilePlain(data) {
return formatGerritFileTable(data); // Same formatting for now
}
function formatDiffContent(diffData) {
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) => {
result += ` ${line}\n`;
});
}
if (section.a) {
// Removed lines
section.a.forEach((line) => {
result += chalk.red(`-${line}\n`);
});
}
if (section.b) {
// Added lines
section.b.forEach((line) => {
result += chalk.green(`+${line}\n`);
});
}
}
result += '```\n\n';
return result;
}
function getFileStatusText(status) {
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) {
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) => {
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) {
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, index) => {
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) => {
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) {
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) {
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) {
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) => {
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) {
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, index) => {
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) {
if (data.message) {
return data.message;
}
const rows = data.bots.map((bot) => [
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) {
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, index) => {
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) {
switch (status.toUpperCase()) {
case 'PASSED': return '✅';
case 'FAILED': return '❌';
case 'RUNNING': return '🔄';
case 'CANCELED': return '⏹️';
case 'UNKNOWN': return '❓';
default: return '⚪';
}
}
function formatListFolderTable(data) {
if (!data || !data.items || data.items.length === 0) {
return chalk.yellow('No items found in folder');
}
const tableData = [
['Type', 'Name']
];
data.items.forEach((item) => {
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) {
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) => item.type === 'folder');
const files = data.items.filter((item) => item.type === 'file');
if (folders.length > 0) {
output += chalk.bold('📁 Folders:\n');
folders.forEach((folder) => {
output += ` ${folder.name}/\n`;
});
if (files.length > 0)
output += '\n';
}
if (files.length > 0) {
output += chalk.bold('📄 Files:\n');
files.forEach((file) => {
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) {
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 = [];
if (cl.reviewers && cl.reviewers.REVIEWER) {
cl.reviewers.REVIEWER.forEach((r) => {
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) {
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) => {
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) => v.value || 0));
const minValue = Math.min(...values.map((v) => 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) {
switch (status.toUpperCase()) {
case 'NEW':
case 'OPEN':
return '🔵';
case 'MERGED':
return '✅';
case 'ABANDONED':
return '❌';
default:
return '⚪';
}
}
function getLabelValue(labels, labelName) {
if (!labels || !labels[labelName])
return '';
const label = labels[labelName];
const values = label.all || [];
const maxValue = Math.max(...values.filter((v) => v.value > 0).map((v) => v.value || 0), 0);
const minValue = Math.min(...values.filter((v) => v.value < 0).map((v) => v.value || 0), 0);
if (minValue < 0) {
return chalk.red(`${minValue}`);
}
else if (maxValue > 0) {
return chalk.green(`+${maxValue}`);
}
return '';
}
function formatPdfiumGerritListTable(cls) {
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 = [];
if (cl.reviewers && cl.reviewers.REVIEWER) {
cl.reviewers.REVIEWER.forEach((r) => {
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) {
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) => v.value || 0));
const minValue = Math.min(...values.map((v) => 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) {
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, index) => {
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) => {
// 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) => {
const lines = msg.split('\n');
lines.forEach((line) => {
output += chalk.red(` ${line}\n`);
});
});
}
output += '\n';
});
}
else {
output += chalk.green('✅ No test failures found\n');
}
return output;
}
function formatCIErrorsPlain(data) {
return formatCIErrorsTable(data); // Same formatting for plain
}
function formatGerritBotErrorsTable(data) {
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, index) => {
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, testIndex) => {
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) => {
if (line.includes('FAILED') || line.includes('FAIL')) {
output += chalk.red(` ${line}\n`);
}
else if (line.includes('Expected:') || line.includes('Actual:')) {