mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
This commit introduces the CodeRabbit service and its associated routes, enabling users to trigger, manage, and check the status of code reviews through a new API. Key features include: - New routes for triggering code reviews, checking status, and stopping reviews. - Integration with the CodeRabbit CLI for authentication and status checks. - UI components for displaying code review results and settings management. - Unit tests for the new code review functionality to ensure reliability. This enhancement aims to streamline the code review process and leverage AI capabilities for improved code quality.
519 lines
12 KiB
TypeScript
519 lines
12 KiB
TypeScript
/**
|
|
* Unified CLI Detection Framework
|
|
*
|
|
* Provides consistent CLI detection and management across all providers
|
|
*/
|
|
|
|
import { spawn, execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { createLogger } from '@automaker/utils';
|
|
|
|
const logger = createLogger('CliDetection');
|
|
|
|
export interface CliInfo {
|
|
name: string;
|
|
command: string;
|
|
version?: string;
|
|
path?: string;
|
|
installed: boolean;
|
|
authenticated: boolean;
|
|
authMethod: 'cli' | 'api_key' | 'none';
|
|
platform?: string;
|
|
architectures?: string[];
|
|
}
|
|
|
|
export interface CliDetectionOptions {
|
|
timeout?: number;
|
|
includeWsl?: boolean;
|
|
wslDistribution?: string;
|
|
}
|
|
|
|
export interface CliDetectionResult {
|
|
cli: CliInfo;
|
|
detected: boolean;
|
|
issues: string[];
|
|
}
|
|
|
|
export interface UnifiedCliDetection {
|
|
claude?: CliDetectionResult;
|
|
codex?: CliDetectionResult;
|
|
cursor?: CliDetectionResult;
|
|
coderabbit?: CliDetectionResult;
|
|
}
|
|
|
|
/**
|
|
* CLI Configuration for different providers
|
|
*/
|
|
const CLI_CONFIGS = {
|
|
claude: {
|
|
name: 'Claude CLI',
|
|
commands: ['claude'],
|
|
versionArgs: ['--version'],
|
|
installCommands: {
|
|
darwin: 'brew install anthropics/claude/claude',
|
|
linux: 'curl -fsSL https://claude.ai/install.sh | sh',
|
|
win32: 'iwr https://claude.ai/install.ps1 -UseBasicParsing | iex',
|
|
},
|
|
},
|
|
codex: {
|
|
name: 'Codex CLI',
|
|
commands: ['codex', 'openai'],
|
|
versionArgs: ['--version'],
|
|
installCommands: {
|
|
darwin: 'npm install -g @openai/codex-cli',
|
|
linux: 'npm install -g @openai/codex-cli',
|
|
win32: 'npm install -g @openai/codex-cli',
|
|
},
|
|
},
|
|
cursor: {
|
|
name: 'Cursor CLI',
|
|
commands: ['cursor-agent', 'cursor'],
|
|
versionArgs: ['--version'],
|
|
installCommands: {
|
|
darwin: 'brew install cursor/cursor/cursor-agent',
|
|
linux: 'curl -fsSL https://cursor.sh/install.sh | sh',
|
|
win32: 'iwr https://cursor.sh/install.ps1 -UseBasicParsing | iex',
|
|
},
|
|
},
|
|
coderabbit: {
|
|
name: 'CodeRabbit CLI',
|
|
commands: ['coderabbit', 'cr'],
|
|
versionArgs: ['--version'],
|
|
installCommands: {
|
|
darwin: 'npm install -g coderabbit',
|
|
linux: 'npm install -g coderabbit',
|
|
win32: 'npm install -g coderabbit',
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
/**
|
|
* Detect if a CLI is installed and available
|
|
*/
|
|
export async function detectCli(
|
|
provider: keyof typeof CLI_CONFIGS,
|
|
options: CliDetectionOptions = {}
|
|
): Promise<CliDetectionResult> {
|
|
const config = CLI_CONFIGS[provider];
|
|
const { timeout = 5000, includeWsl = false, wslDistribution } = options;
|
|
const issues: string[] = [];
|
|
|
|
const cliInfo: CliInfo = {
|
|
name: config.name,
|
|
command: '',
|
|
installed: false,
|
|
authenticated: false,
|
|
authMethod: 'none',
|
|
};
|
|
|
|
try {
|
|
// Find the command in PATH
|
|
const command = await findCommand([...config.commands]);
|
|
if (command) {
|
|
cliInfo.command = command;
|
|
}
|
|
|
|
if (!cliInfo.command) {
|
|
issues.push(`${config.name} not found in PATH`);
|
|
return { cli: cliInfo, detected: false, issues };
|
|
}
|
|
|
|
cliInfo.path = cliInfo.command;
|
|
cliInfo.installed = true;
|
|
|
|
// Get version
|
|
try {
|
|
cliInfo.version = await getCliVersion(cliInfo.command, [...config.versionArgs], timeout);
|
|
} catch (error) {
|
|
issues.push(`Failed to get ${config.name} version: ${error}`);
|
|
}
|
|
|
|
// Check authentication
|
|
cliInfo.authMethod = await checkCliAuth(provider, cliInfo.command);
|
|
cliInfo.authenticated = cliInfo.authMethod !== 'none';
|
|
|
|
return { cli: cliInfo, detected: true, issues };
|
|
} catch (error) {
|
|
issues.push(`Error detecting ${config.name}: ${error}`);
|
|
return { cli: cliInfo, detected: false, issues };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect all CLIs in the system
|
|
*/
|
|
export async function detectAllCLis(
|
|
options: CliDetectionOptions = {}
|
|
): Promise<UnifiedCliDetection> {
|
|
const results: UnifiedCliDetection = {};
|
|
|
|
// Detect all providers in parallel
|
|
const providers = Object.keys(CLI_CONFIGS) as Array<keyof typeof CLI_CONFIGS>;
|
|
const detectionPromises = providers.map(async (provider) => {
|
|
const result = await detectCli(provider, options);
|
|
return { provider, result };
|
|
});
|
|
|
|
const detections = await Promise.all(detectionPromises);
|
|
|
|
for (const { provider, result } of detections) {
|
|
results[provider] = result;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Find the first available command from a list of alternatives
|
|
*/
|
|
export async function findCommand(commands: string[]): Promise<string | null> {
|
|
for (const command of commands) {
|
|
try {
|
|
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
const result = execSync(`${whichCommand} ${command}`, {
|
|
encoding: 'utf8',
|
|
timeout: 2000,
|
|
}).trim();
|
|
|
|
if (result) {
|
|
return result.split('\n')[0]; // Take first result on Windows
|
|
}
|
|
} catch {
|
|
// Command not found, try next
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get CLI version
|
|
*/
|
|
export async function getCliVersion(
|
|
command: string,
|
|
args: string[],
|
|
timeout: number = 5000
|
|
): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
stdio: 'pipe',
|
|
timeout,
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout?.on('data', (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr?.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
if (code === 0 && stdout) {
|
|
resolve(stdout.trim());
|
|
} else if (stderr) {
|
|
reject(stderr.trim());
|
|
} else {
|
|
reject(`Command exited with code ${code}`);
|
|
}
|
|
});
|
|
|
|
child.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check authentication status for a CLI
|
|
*/
|
|
export async function checkCliAuth(
|
|
provider: keyof typeof CLI_CONFIGS,
|
|
command: string
|
|
): Promise<'cli' | 'api_key' | 'none'> {
|
|
try {
|
|
switch (provider) {
|
|
case 'claude':
|
|
return await checkClaudeAuth(command);
|
|
case 'codex':
|
|
return await checkCodexAuth(command);
|
|
case 'cursor':
|
|
return await checkCursorAuth(command);
|
|
case 'coderabbit':
|
|
return await checkCodeRabbitAuth(command);
|
|
default:
|
|
return 'none';
|
|
}
|
|
} catch {
|
|
return 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check Claude CLI authentication
|
|
*/
|
|
async function checkClaudeAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
|
|
try {
|
|
// Check for environment variable
|
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
return 'api_key';
|
|
}
|
|
|
|
// Try running a simple command to check CLI auth
|
|
const result = await getCliVersion(command, ['--version'], 3000);
|
|
if (result) {
|
|
return 'cli'; // If version works, assume CLI is authenticated
|
|
}
|
|
} catch {
|
|
// Version command might work even without auth, so we need a better check
|
|
}
|
|
|
|
// Try a more specific auth check
|
|
return new Promise((resolve) => {
|
|
const child = spawn(command, ['whoami'], {
|
|
stdio: 'pipe',
|
|
timeout: 3000,
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout?.on('data', (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr?.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
if (code === 0 && stdout && !stderr.includes('not authenticated')) {
|
|
resolve('cli');
|
|
} else {
|
|
resolve('none');
|
|
}
|
|
});
|
|
|
|
child.on('error', () => {
|
|
resolve('none');
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check Codex CLI authentication
|
|
*/
|
|
async function checkCodexAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
|
|
// Check for environment variable
|
|
if (process.env.OPENAI_API_KEY) {
|
|
return 'api_key';
|
|
}
|
|
|
|
try {
|
|
// Try a simple auth check
|
|
const result = await getCliVersion(command, ['--version'], 3000);
|
|
if (result) {
|
|
return 'cli';
|
|
}
|
|
} catch {
|
|
// Version check failed
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
/**
|
|
* Check Cursor CLI authentication
|
|
*/
|
|
async function checkCursorAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
|
|
// Check for environment variable
|
|
if (process.env.CURSOR_API_KEY) {
|
|
return 'api_key';
|
|
}
|
|
|
|
// Check for credentials files
|
|
const credentialPaths = [
|
|
path.join(os.homedir(), '.cursor', 'credentials.json'),
|
|
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
|
|
path.join(os.homedir(), '.cursor', 'auth.json'),
|
|
path.join(os.homedir(), '.config', 'cursor', 'auth.json'),
|
|
];
|
|
|
|
for (const credPath of credentialPaths) {
|
|
try {
|
|
if (fs.existsSync(credPath)) {
|
|
const content = fs.readFileSync(credPath, 'utf8');
|
|
const creds = JSON.parse(content);
|
|
if (creds.accessToken || creds.token || creds.apiKey) {
|
|
return 'cli';
|
|
}
|
|
}
|
|
} catch {
|
|
// Invalid credentials file
|
|
}
|
|
}
|
|
|
|
// Try a simple command
|
|
try {
|
|
const result = await getCliVersion(command, ['--version'], 3000);
|
|
if (result) {
|
|
return 'cli';
|
|
}
|
|
} catch {
|
|
// Version check failed
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
/**
|
|
* Check CodeRabbit CLI authentication
|
|
*
|
|
* Expected output when authenticated:
|
|
* ```
|
|
* CodeRabbit CLI Status
|
|
* ✅ Authentication: Logged in
|
|
* User Information:
|
|
* 👤 Name: ...
|
|
* ```
|
|
*/
|
|
async function checkCodeRabbitAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
|
|
// Check for environment variable
|
|
if (process.env.CODERABBIT_API_KEY) {
|
|
return 'api_key';
|
|
}
|
|
|
|
// Try running auth status command
|
|
return new Promise((resolve) => {
|
|
const child = spawn(command, ['auth', 'status'], {
|
|
stdio: 'pipe',
|
|
timeout: 10000, // Increased timeout for slower systems
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout?.on('data', (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr?.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
const output = stdout + stderr;
|
|
|
|
// Check for positive authentication indicators in output
|
|
const isAuthenticated =
|
|
code === 0 &&
|
|
(output.includes('Logged in') || output.includes('logged in')) &&
|
|
!output.toLowerCase().includes('not logged in') &&
|
|
!output.toLowerCase().includes('not authenticated');
|
|
|
|
if (isAuthenticated) {
|
|
resolve('cli');
|
|
} else {
|
|
resolve('none');
|
|
}
|
|
});
|
|
|
|
child.on('error', () => {
|
|
resolve('none');
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get installation instructions for a provider
|
|
*/
|
|
export function getInstallInstructions(
|
|
provider: keyof typeof CLI_CONFIGS,
|
|
platform: NodeJS.Platform = process.platform
|
|
): string {
|
|
const config = CLI_CONFIGS[provider];
|
|
const command = config.installCommands[platform as keyof typeof config.installCommands];
|
|
|
|
if (!command) {
|
|
return `No installation instructions available for ${provider} on ${platform}`;
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
/**
|
|
* Get platform-specific CLI paths and versions
|
|
*/
|
|
export function getPlatformCliPaths(provider: keyof typeof CLI_CONFIGS): string[] {
|
|
const config = CLI_CONFIGS[provider];
|
|
const platform = process.platform;
|
|
|
|
switch (platform) {
|
|
case 'darwin':
|
|
return [
|
|
`/usr/local/bin/${config.commands[0]}`,
|
|
`/opt/homebrew/bin/${config.commands[0]}`,
|
|
path.join(os.homedir(), '.local', 'bin', config.commands[0]),
|
|
];
|
|
|
|
case 'linux':
|
|
return [
|
|
`/usr/bin/${config.commands[0]}`,
|
|
`/usr/local/bin/${config.commands[0]}`,
|
|
path.join(os.homedir(), '.local', 'bin', config.commands[0]),
|
|
path.join(os.homedir(), '.npm', 'global', 'bin', config.commands[0]),
|
|
];
|
|
|
|
case 'win32':
|
|
return [
|
|
path.join(
|
|
os.homedir(),
|
|
'AppData',
|
|
'Local',
|
|
'Programs',
|
|
config.commands[0],
|
|
`${config.commands[0]}.exe`
|
|
),
|
|
path.join(process.env.ProgramFiles || '', config.commands[0], `${config.commands[0]}.exe`),
|
|
path.join(
|
|
process.env.ProgramFiles || '',
|
|
config.commands[0],
|
|
'bin',
|
|
`${config.commands[0]}.exe`
|
|
),
|
|
];
|
|
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate CLI installation
|
|
*/
|
|
export function validateCliInstallation(cliInfo: CliInfo): {
|
|
valid: boolean;
|
|
issues: string[];
|
|
} {
|
|
const issues: string[] = [];
|
|
|
|
if (!cliInfo.installed) {
|
|
issues.push('CLI is not installed');
|
|
}
|
|
|
|
if (cliInfo.installed && !cliInfo.version) {
|
|
issues.push('Could not determine CLI version');
|
|
}
|
|
|
|
if (cliInfo.installed && cliInfo.authMethod === 'none') {
|
|
issues.push('CLI is not authenticated');
|
|
}
|
|
|
|
return {
|
|
valid: issues.length === 0,
|
|
issues,
|
|
};
|
|
}
|