UNPKG

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
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:')) {