diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 8287ad7..f2ca110 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -20,8 +20,10 @@ import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; import { SharedOptions } from '../options.js'; import { captureException } from '../sentry/index.js'; import { getConfigFromArgv, loadConfig } from '../settings/config.js'; +import { checkGitHubTools, getGitHubModeWarning } from '../utils/githubTools.js'; import { nameToLogIndex } from '../utils/nameToLogIndex.js'; import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js'; +import { checkGitCli } from '../utils/gitCliCheck.js'; import type { CommandModule, Argv } from 'yargs'; @@ -58,6 +60,27 @@ export const command: CommandModule = { if (config.upgradeCheck !== false) { await checkForUpdates(logger); } + + // Check for git and gh CLI tools if GitHub mode is enabled + if (config.githubMode) { + logger.debug('GitHub mode is enabled, checking for git and gh CLI tools...'); + const gitCliCheck = await checkGitCli(logger); + + if (gitCliCheck.errors.length > 0) { + logger.warn('GitHub mode is enabled but there are issues with git/gh CLI tools:'); + gitCliCheck.errors.forEach(error => logger.warn(`- ${error}`)); + + if (!gitCliCheck.gitAvailable || !gitCliCheck.ghAvailable) { + logger.warn('GitHub mode requires git and gh CLI tools to be installed.'); + logger.warn('Please install the missing tools or disable GitHub mode with --githubMode false'); + } else if (!gitCliCheck.ghAuthenticated) { + logger.warn('GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.'); + } + } else { + logger.info('GitHub mode is enabled and all required CLI tools are available.'); + } + } + const tokenTracker = new TokenTracker( 'Root', undefined, diff --git a/packages/cli/src/utils/gitCliCheck.test.ts b/packages/cli/src/utils/gitCliCheck.test.ts new file mode 100644 index 0000000..fb31fad --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { checkGitCli, GitCliCheckResult } from './gitCliCheck'; + +// Mock the child_process module +vi.mock('child_process', () => ({ + exec: vi.fn(), +})); + +// Mock the util module +vi.mock('util', () => ({ + promisify: vi.fn((fn) => { + return (cmd: string) => { + return new Promise((resolve, reject) => { + fn(cmd, (error: Error | null, result: { stdout: string }) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }; + }), +})); + +// Import the mocked modules +import { exec } from 'child_process'; + +describe('gitCliCheck', () => { + const mockExec = exec as unknown as vi.Mock; + + beforeEach(() => { + mockExec.mockReset(); + }); + + it('should return all true when git and gh are available and authenticated', async () => { + // Mock successful responses + mockExec.mockImplementation((cmd: string, callback: Function) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(null, { stdout: 'Logged in to github.com as username' }); + } + }); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should detect when git is not available', async () => { + mockExec.mockImplementation((cmd: string, callback: Function) => { + if (cmd === 'git --version') { + callback(new Error('Command not found')); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(null, { stdout: 'Logged in to github.com as username' }); + } + }); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(false); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(true); + expect(result.errors).toContain('Git CLI is not available. Please install git.'); + }); + + it('should detect when gh is not available', async () => { + mockExec.mockImplementation((cmd: string, callback: Function) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(new Error('Command not found')); + } + }); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(false); + expect(result.ghAuthenticated).toBe(false); + expect(result.errors).toContain('GitHub CLI is not available. Please install gh CLI.'); + }); + + it('should detect when gh is not authenticated', async () => { + mockExec.mockImplementation((cmd: string, callback: Function) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(new Error('You are not logged into any GitHub hosts')); + } + }); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(false); + expect(result.errors).toContain('GitHub CLI is not authenticated. Please run "gh auth login".'); + }); +}); \ No newline at end of file diff --git a/packages/cli/src/utils/gitCliCheck.ts b/packages/cli/src/utils/gitCliCheck.ts new file mode 100644 index 0000000..163ca6f --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.ts @@ -0,0 +1,89 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { Logger } from 'mycoder-agent'; + +const execAsync = promisify(exec); + +/** + * Result of CLI tool checks + */ +export interface GitCliCheckResult { + gitAvailable: boolean; + ghAvailable: boolean; + ghAuthenticated: boolean; + errors: string[]; +} + +/** + * Checks if git command is available + */ +async function checkGitAvailable(): Promise { + try { + await execAsync('git --version'); + return true; + } catch (error) { + return false; + } +} + +/** + * Checks if gh command is available + */ +async function checkGhAvailable(): Promise { + try { + await execAsync('gh --version'); + return true; + } catch (error) { + return false; + } +} + +/** + * Checks if gh is authenticated + */ +async function checkGhAuthenticated(): Promise { + try { + const { stdout } = await execAsync('gh auth status'); + return stdout.includes('Logged in to'); + } catch (error) { + return false; + } +} + +/** + * Checks if git and gh CLI tools are available and if gh is authenticated + * @param logger Optional logger for debug output + * @returns Object with check results + */ +export async function checkGitCli(logger?: Logger): Promise { + const result: GitCliCheckResult = { + gitAvailable: false, + ghAvailable: false, + ghAuthenticated: false, + errors: [], + }; + + logger?.debug('Checking for git CLI availability...'); + result.gitAvailable = await checkGitAvailable(); + + logger?.debug('Checking for gh CLI availability...'); + result.ghAvailable = await checkGhAvailable(); + + if (result.ghAvailable) { + logger?.debug('Checking for gh CLI authentication...'); + result.ghAuthenticated = await checkGhAuthenticated(); + } + + // Collect any errors + if (!result.gitAvailable) { + result.errors.push('Git CLI is not available. Please install git.'); + } + + if (!result.ghAvailable) { + result.errors.push('GitHub CLI is not available. Please install gh CLI.'); + } else if (!result.ghAuthenticated) { + result.errors.push('GitHub CLI is not authenticated. Please run "gh auth login".'); + } + + return result; +} \ No newline at end of file