UNPKG

chromium-helper

Version:

CLI tool for searching and exploring Chromium source code via Google's official APIs

921 lines • 38.4 kB
#!/usr/bin/env node import { Command } from 'commander'; import { ChromiumAPI } from './api.js'; import { formatOutput } from './formatter.js'; import { loadConfig } from './config.js'; import { getAIUsageGuide } from './ai-guide.js'; import { AuthManager, getAuthCookies } from './auth.js'; import { showCookieHelp } from './cookie-helper.js'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; const __filename = fileURLToPath(import.meta.url); const packageRoot = path.resolve(path.dirname(__filename), '..'); const packageJsonPath = path.join(packageRoot, 'package.json'); let packageInfo; try { packageInfo = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); } catch (error) { packageInfo = { version: "1.0.0", name: "chromium-helper" }; } const program = new Command(); /** * Filter comments to only include unresolved threads. * A thread is resolved if its last comment has unresolved=false. * This correctly handles Gerrit's threading model where the root comment * may have unresolved=true but a reply has resolved it. */ function filterUnresolvedThreads(results) { const filteredResults = {}; for (const [file, comments] of Object.entries(results)) { const commentArray = comments; // Build a map of comment IDs for quick lookup const commentMap = new Map(); commentArray.forEach(c => commentMap.set(c.id, c)); // Helper function to find the root of a thread const getThreadRoot = (comment) => { let current = comment; while (current.in_reply_to) { const parent = commentMap.get(current.in_reply_to); if (!parent) return null; current = parent; } return current; }; // Find root comments (those without in_reply_to) const rootComments = commentArray.filter(c => !c.in_reply_to); // For each root, find all its replies and check if thread is unresolved const unresolvedThreadComments = []; for (const root of rootComments) { // Get all comments in this thread (root + all replies) const threadComments = commentArray.filter(c => c.id === root.id || getThreadRoot(c)?.id === root.id); // Sort thread comments by time const sortedThread = [...threadComments].sort((a, b) => new Date(a.updated).getTime() - new Date(b.updated).getTime()); // A thread is resolved if the last comment that set resolution has unresolved=false // Track resolution status through the thread let threadResolved = false; for (const c of sortedThread) { if (c.unresolved === false) { threadResolved = true; } else if (c.unresolved === true) { threadResolved = false; } } // If thread is unresolved, include all its comments if (!threadResolved) { unresolvedThreadComments.push(...threadComments); } } if (unresolvedThreadComments.length > 0) { // Remove duplicates and maintain order const seen = new Set(); filteredResults[file] = unresolvedThreadComments.filter(c => { if (seen.has(c.id)) return false; seen.add(c.id); return true; }); } } return filteredResults; } async function main() { const config = await loadConfig(); const api = new ChromiumAPI(config.apiKey); program .name('chromium-helper') .alias('ch') .description('CLI tool for searching and exploring Chromium source code') .version(packageInfo.version) .option('-f, --format <type>', 'output format (json|table|plain)', 'plain') .option('--no-color', 'disable colored output') .option('--debug', 'enable debug logging') .option('--ai', 'show comprehensive usage guide for AI systems'); // Handle --ai flag if (process.argv.includes('--ai')) { console.log(getAIUsageGuide()); process.exit(0); } // Auth commands const auth = program .command('auth') .description('Authentication management for Gerrit'); auth .command('login') .description('Authenticate with Gerrit using browser') .option('--headless', 'Run browser in headless mode') .action(async (options) => { try { const authManager = new AuthManager(); await authManager.authenticate({ headless: options.headless }); process.exit(0); } catch (error) { console.error('Authentication failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); auth .command('status') .description('Check authentication status') .action(async () => { try { const authManager = new AuthManager(); const isValid = await authManager.checkAuth(); if (isValid) { console.log('āœ… Authentication is valid'); } else { console.log('āŒ No valid authentication found'); console.log('Run: ch auth login'); } process.exit(0); } catch (error) { console.error('Status check failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); auth .command('logout') .description('Clear saved authentication') .action(async () => { try { const authManager = new AuthManager(); await authManager.clearCookies(); process.exit(0); } catch (error) { console.error('Logout failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); auth .command('help') .description('Show detailed help for getting authentication cookies') .action(async () => { await showCookieHelp(); process.exit(0); }); auth .command('manual') .description('Manually save authentication cookies') .action(async () => { console.log(chalk.cyan('šŸŖ Manual Cookie Setup\n')); console.log('Please follow these steps:'); console.log(chalk.yellow('\n1. Open Chrome in INCOGNITO mode')); console.log(chalk.yellow('2. Sign in to:')); console.log(chalk.blue(' https://chromium-review.googlesource.com')); console.log(chalk.yellow('\n3. Open Developer Tools (F12)')); console.log(chalk.yellow('4. Go to Application > Cookies > chromium-review.googlesource.com')); console.log(chalk.yellow('5. Find these cookies (from .googlesource.com domain):')); console.log(chalk.green(' - SID')); console.log(chalk.green(' - __Secure-1PSID')); console.log(chalk.green(' - __Secure-3PSID')); console.log(chalk.gray('\n Note: All three cookies are required for full functionality')); console.log(chalk.yellow('\n6. Enter the cookie values below:\n')); // Use readline to get cookie values const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const question = (prompt) => { return new Promise((resolve) => { rl.question(prompt, resolve); }); }; try { const sid = await question('SID value: '); if (!sid) { console.log(chalk.red('\nāŒ SID cookie value is required')); rl.close(); process.exit(1); } const psid1 = await question('__Secure-1PSID value: '); if (!psid1) { console.log(chalk.red('\nāŒ __Secure-1PSID cookie value is required')); rl.close(); process.exit(1); } const psid3 = await question('__Secure-3PSID value: '); if (!psid3) { console.log(chalk.red('\nāŒ __Secure-3PSID cookie value is required')); rl.close(); process.exit(1); } // Combine all three cookies const cookieString = `SID=${sid}; __Secure-1PSID=${psid1}; __Secure-3PSID=${psid3}`; const authManager = new AuthManager(); await authManager.saveCookies(cookieString); console.log(chalk.green('\nāœ… Cookies saved successfully!')); console.log(chalk.gray('You can now use gerrit list commands with owner:self queries')); rl.close(); process.exit(0); } catch (error) { console.error('\nāŒ Error saving cookies:', error); rl.close(); process.exit(1); } }); // Search commands program .command('search') .alias('s') .description('Search Chromium source code') .argument('<query>', 'search query') .option('-c, --case-sensitive', 'case sensitive search') .option('-l, --language <lang>', 'filter by programming language') .option('-p, --file-pattern <pattern>', 'file pattern filter') .option('-t, --type <type>', 'search type (content|function|class|symbol|comment)') .option('--exclude-comments', 'exclude comments from search') .option('--limit <number>', 'maximum number of results', '20') .action(async (query, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.searchCode({ query, caseSensitive: options.caseSensitive, language: options.language, filePattern: options.filePattern, searchType: options.type, excludeComments: options.excludeComments, limit: parseInt(options.limit) }); const format = program.opts().format; console.log(formatOutput(results, format, 'search')); } catch (error) { console.error('Search failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Symbol lookup command program .command('symbol') .alias('sym') .description('Find symbol definitions and usage') .argument('<symbol>', 'symbol to find') .option('-f, --file <path>', 'file path context for symbol resolution') .action(async (symbol, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.findSymbol(symbol, options.file); const format = program.opts().format; console.log(formatOutput(results, format, 'symbol')); } catch (error) { console.error('Symbol lookup failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // File content command program .command('file') .alias('f') .description('Get file content from Chromium source') .argument('<path>', 'file path in Chromium source') .option('-s, --start <line>', 'starting line number') .option('-e, --end <line>', 'ending line number') .action(async (filePath, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getFile({ filePath, lineStart: options.start ? parseInt(options.start) : undefined, lineEnd: options.end ? parseInt(options.end) : undefined }); const format = program.opts().format; console.log(formatOutput(results, format, 'file')); } catch (error) { console.error('File fetch failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Gerrit CL commands const gerrit = program .command('gerrit') .alias('gr') .description('Gerrit code review operations'); gerrit .command('status') .description('Get CL status and test results') .argument('<cl>', 'CL number or URL') .action(async (cl) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getGerritCLStatus(cl); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-status')); } catch (error) { console.error('Gerrit status failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('comments') .description('Get CL review comments') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('--no-resolved', 'exclude resolved comments') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); let results = await api.getGerritCLComments({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, includeResolved: options.resolved !== false }); // Filter out resolved threads if --no-resolved flag is set if (options.resolved === false) { results = filterUnresolvedThreads(results); } const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-comments')); } catch (error) { console.error('Gerrit comments failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('diff') .description('Get CL diff/changes') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('-f, --file <path>', 'specific file path to get diff for') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getGerritCLDiff({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, filePath: options.file }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-diff')); } catch (error) { console.error('Gerrit diff failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('file') .description('Get file content from CL patchset') .argument('<cl>', 'CL number or URL') .argument('<path>', 'file path to get content for') .option('-p, --patchset <number>', 'specific patchset number') .action(async (cl, filePath, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getGerritPatchsetFile({ clNumber: cl, filePath, patchset: options.patchset ? parseInt(options.patchset) : undefined }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-file')); } catch (error) { console.error('Gerrit file failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('bots') .description('Get try-bot status for CL') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('--failed-only', 'show only failed bots') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getGerritCLTrybotStatus({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, failedOnly: options.failedOnly }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-bots')); } catch (error) { console.error('Gerrit bots failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('bots-errors') .alias('berr') .description('Get detailed errors from failed try-bots for CL') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('--all-bots', 'show errors for all bots (not just failed)') .option('-b, --bot <name>', 'filter by bot name (supports partial matching, e.g., "linux" or "linux-rel")') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getGerritCLBotErrors({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, failedOnly: !options.allBots, botFilter: options.bot }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-bot-errors')); } catch (error) { console.error('Gerrit bot errors failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('list') .description('List Gerrit CLs (requires authentication)') .option('-q, --query <query>', 'Gerrit search query (default: status:open owner:self)') .option('-a, --auth-cookie <cookie>', 'authentication cookie (optional if already logged in)') .option('-l, --limit <number>', 'maximum number of CLs to return', '25') .action(async (options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); // Get auth cookies with fallback to saved auth let authCookie; try { authCookie = await getAuthCookies(options.authCookie); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } const results = await api.listGerritCLs({ query: options.query, authCookie: authCookie, limit: parseInt(options.limit) }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-list')); } catch (error) { console.error('Gerrit list failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); gerrit .command('suggest-reviewers') .alias('sr') .description('Suggest optimal reviewers for CL based on OWNERS and recent activity') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('-l, --limit <number>', 'max reviewers to suggest', '5') .option('-m, --months <number>', 'activity lookback period in months', '6') .option('--fast', 'skip activity analysis for faster results (OWNERS-only)') .option('--show-all', 'show all candidates, not just optimal set') .option('--exclude <emails...>', 'exclude specific reviewer emails') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); console.log('šŸ” Analyzing CL for optimal reviewers...\n'); const results = await api.suggestReviewersForCL({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, maxReviewers: parseInt(options.limit), activityMonths: parseInt(options.months), excludeReviewers: options.exclude || [], fast: options.fast || false, }); const format = program.opts().format; console.log(formatOutput(results, format, 'suggest-reviewers', { showAll: options.showAll })); } catch (error) { console.error('Suggest reviewers failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // CI Build commands const ci = program .command('ci') .description('CI build operations'); ci .command('errors') .alias('err') .description('Get build errors from CI build URL') .argument('<buildUrl>', 'CI build URL (e.g., https://ci.chromium.org/ui/p/chromium/builders/try/linux-rel/2396535)') .option('--failed-only', 'show only failed tests') .action(async (buildUrl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getCIBuildErrors(buildUrl); const format = program.opts().format; console.log(formatOutput(results, format, 'ci-errors')); } catch (error) { console.error('CI errors failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Owners command program .command('owners') .alias('own') .description('Find OWNERS files for a file path') .argument('<path>', 'file path to find owners for') .action(async (filePath) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.findOwners(filePath); const format = program.opts().format; console.log(formatOutput(results, format, 'owners')); } catch (error) { console.error('Owners lookup failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Commits search command program .command('commits') .alias('cm') .description('Search commit history') .argument('<query>', 'search query for commits') .option('-a, --author <author>', 'filter by author') .option('--since <date>', 'commits after date (YYYY-MM-DD)') .option('--until <date>', 'commits before date (YYYY-MM-DD)') .option('--limit <number>', 'maximum number of results', '20') .action(async (query, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.searchCommits({ query, author: options.author, since: options.since, until: options.until, limit: parseInt(options.limit) }); const format = program.opts().format; console.log(formatOutput(results, format, 'commits')); } catch (error) { console.error('Commit search failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Issue commands const issues = program .command('issues') .alias('bugs') .description('Chromium issue operations'); issues .command('get') .alias('show') .description('Get Chromium issue details') .argument('<id>', 'issue ID or URL') .action(async (issueId) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getIssue(issueId); const format = program.opts().format; console.log(formatOutput(results, format, 'issue')); } catch (error) { console.error('Issue lookup failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); issues .command('search') .alias('find') .description('Search Chromium issues') .argument('<query>', 'search query') .option('--limit <number>', 'maximum number of results', '50') .option('--start <number>', 'starting index for pagination', '0') .action(async (query, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.searchIssues(query, { limit: parseInt(options.limit), startIndex: parseInt(options.start) }); const format = program.opts().format; console.log(formatOutput(results, format, 'issue-search')); } catch (error) { console.error('Issue search failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Legacy issue command for backward compatibility program .command('issue') .alias('bug') .description('Get Chromium issue details') .argument('<id>', 'issue ID or URL') .action(async (issueId) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getIssue(issueId); const format = program.opts().format; console.log(formatOutput(results, format, 'issue')); } catch (error) { console.error('Issue lookup failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Direct issue search command for convenience program .command('issue-search') .alias('isearch') .description('Search Chromium issues (shortcut for issues search)') .argument('<query>', 'search query') .option('--limit <number>', 'maximum number of results', '50') .option('--start <number>', 'starting index for pagination', '0') .action(async (query, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.searchIssues(query, { limit: parseInt(options.limit), startIndex: parseInt(options.start) }); const format = program.opts().format; console.log(formatOutput(results, format, 'issue-search')); } catch (error) { console.error('Issue search failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // PDFium Gerrit commands const pdfium = program .command('pdfium') .alias('pdf') .description('PDFium Gerrit operations'); pdfium .command('status') .description('Get PDFium CL status and test results') .argument('<cl>', 'CL number or URL') .action(async (cl) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getPdfiumGerritCLStatus(cl); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-status')); } catch (error) { console.error('PDFium Gerrit status failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); pdfium .command('comments') .description('Get PDFium CL review comments') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('--no-resolved', 'exclude resolved comments') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); let results = await api.getPdfiumGerritCLComments({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, includeResolved: options.resolved !== false }); // Filter out resolved threads if --no-resolved flag is set if (options.resolved === false) { results = filterUnresolvedThreads(results); } const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-comments')); } catch (error) { console.error('PDFium Gerrit comments failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); pdfium .command('diff') .description('Get PDFium CL diff/changes') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('-f, --file <path>', 'specific file path to get diff for') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getPdfiumGerritCLDiff({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, filePath: options.file }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-diff')); } catch (error) { console.error('PDFium Gerrit diff failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); pdfium .command('file') .description('Get file content from PDFium CL patchset') .argument('<cl>', 'CL number or URL') .argument('<path>', 'file path to get content for') .option('-p, --patchset <number>', 'specific patchset number') .action(async (cl, filePath, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getPdfiumGerritPatchsetFile({ clNumber: cl, filePath, patchset: options.patchset ? parseInt(options.patchset) : undefined }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-file')); } catch (error) { console.error('PDFium Gerrit file failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); pdfium .command('bots') .description('Get try-bot status for PDFium CL') .argument('<cl>', 'CL number or URL') .option('-p, --patchset <number>', 'specific patchset number') .option('--failed-only', 'show only failed bots') .action(async (cl, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getPdfiumGerritCLTrybotStatus({ clNumber: cl, patchset: options.patchset ? parseInt(options.patchset) : undefined, failedOnly: options.failedOnly }); const format = program.opts().format; console.log(formatOutput(results, format, 'gerrit-bots')); } catch (error) { console.error('PDFium Gerrit bots failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); pdfium .command('list') .description('List PDFium Gerrit CLs (requires authentication)') .option('-q, --query <query>', 'PDFium Gerrit search query (default: status:open owner:self)') .option('-a, --auth-cookie <cookie>', 'authentication cookie (optional if already logged in)') .option('-l, --limit <number>', 'maximum number of CLs to return', '25') .action(async (options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); // Get auth cookies with fallback to saved auth let authCookie; try { authCookie = await getAuthCookies(options.authCookie); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } const results = await api.listPdfiumGerritCLs({ query: options.query, authCookie: authCookie, limit: parseInt(options.limit) }); const format = program.opts().format; console.log(formatOutput(results, format, 'pdfium-gerrit-list')); } catch (error) { console.error('PDFium Gerrit list failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // List folder command program .command('list-folder') .alias('ls') .description('List files and folders in a Chromium source directory') .argument('<path>', 'folder path in Chromium source') .action(async (folderPath) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.listFolder(folderPath); const format = program.opts().format; console.log(formatOutput(results, format, 'list-folder')); } catch (error) { console.error('Folder listing failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Blame command program .command('blame') .description('Show git blame for a file') .argument('<file>', 'file path in Chromium source') .option('-l, --line <number>', 'show blame for specific line number') .action(async (file, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getFileBlame(file, options.line ? parseInt(options.line) : undefined); const format = program.opts().format; console.log(formatOutput(results, format, 'blame')); } catch (error) { console.error('Blame failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // History command program .command('history') .alias('log') .description('Show commit history for a file') .argument('<file>', 'file path in Chromium source') .option('-l, --limit <number>', 'maximum number of commits to show', '20') .action(async (file, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getFileHistory(file, parseInt(options.limit)); const format = program.opts().format; console.log(formatOutput(results, format, 'history')); } catch (error) { console.error('History failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Contributors command program .command('contributors') .description('Show top contributors for a file or directory') .argument('<path>', 'file or directory path in Chromium source') .option('-l, --limit <number>', 'maximum number of commits to analyze', '50') .action(async (path, options) => { try { const globalOptions = program.opts(); api.setDebugMode(globalOptions.debug); const results = await api.getPathContributors(path, parseInt(options.limit)); const format = program.opts().format; console.log(formatOutput(results, format, 'contributors')); } catch (error) { console.error('Contributors failed:', error instanceof Error ? error.message : String(error)); process.exit(1); } }); // Config command program .command('config') .description('Configuration management') .option('--set-api-key <key>', 'set API key') .option('--show', 'show current configuration') .action(async (options) => { if (options.setApiKey) { // TODO: Implement config setting console.log('API key configuration not yet implemented'); } else if (options.show) { console.log('Current configuration:'); console.log(`API Key: ${config.apiKey ? '***set***' : 'not set'}`); } }); await program.parseAsync(process.argv); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); }); //# sourceMappingURL=index.js.map