Skip to content

Check for git and gh CLI tools when GitHub mode is enabled #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/cli/src/commands/$default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -58,6 +60,27 @@ export const command: CommandModule<SharedOptions, DefaultArgs> = {
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,
Expand Down
110 changes: 110 additions & 0 deletions packages/cli/src/utils/gitCliCheck.test.ts
Original file line number Diff line number Diff line change
@@ -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".');
});
});
89 changes: 89 additions & 0 deletions packages/cli/src/utils/gitCliCheck.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
try {
await execAsync('git --version');
return true;
} catch (error) {
return false;
}
}

/**
* Checks if gh command is available
*/
async function checkGhAvailable(): Promise<boolean> {
try {
await execAsync('gh --version');
return true;
} catch (error) {
return false;
}
}

/**
* Checks if gh is authenticated
*/
async function checkGhAuthenticated(): Promise<boolean> {
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<GitCliCheckResult> {
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;
}
Loading