#!/usr/bin/env node
import { Command } from 'commander';
import { ChromiumAPI } from './api.js';
import { formatOutput, OutputFormat } 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: { version: string; name: string };
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: Record<string, any[]>): Record<string, any[]> {
const filteredResults: Record<string, any[]> = {};
for (const [file, comments] of Object.entries(results)) {
const commentArray = comments as any[];
// Build a map of comment IDs for quick lookup
const commentMap = new Map<string, any>();
commentArray.forEach(c => commentMap.set(c.id, c));
// Helper function to find the root of a thread
const getThreadRoot = (comment: any): any | null => {
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: any[] = [];
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<string>();
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: string): Promise<string> => {
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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 as OutputFormat;
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);
});