Skip to content

Commit 8996f36

Browse files
committed
feat: add git and gh CLI tools availability check
Implements a utility function to check if git and gh CLI tools are available and if gh is authenticated when GitHub mode is enabled. - Created gitCliCheck.ts utility with functions to check git/gh availability - Added warning messages when GitHub mode is enabled but tools are missing - Added unit tests for the new functionality Fixes #217
1 parent 68d34ab commit 8996f36

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed

packages/cli/src/commands/$default.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js';
2020
import { SharedOptions } from '../options.js';
2121
import { captureException } from '../sentry/index.js';
2222
import { getConfigFromArgv, loadConfig } from '../settings/config.js';
23+
import { checkGitHubTools, getGitHubModeWarning } from '../utils/githubTools.js';
2324
import { nameToLogIndex } from '../utils/nameToLogIndex.js';
2425
import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js';
26+
import { checkGitCli } from '../utils/gitCliCheck.js';
2527

2628
import type { CommandModule, Argv } from 'yargs';
2729

@@ -58,6 +60,27 @@ export const command: CommandModule<SharedOptions, DefaultArgs> = {
5860
if (config.upgradeCheck !== false) {
5961
await checkForUpdates(logger);
6062
}
63+
64+
// Check for git and gh CLI tools if GitHub mode is enabled
65+
if (config.githubMode) {
66+
logger.debug('GitHub mode is enabled, checking for git and gh CLI tools...');
67+
const gitCliCheck = await checkGitCli(logger);
68+
69+
if (gitCliCheck.errors.length > 0) {
70+
logger.warn('GitHub mode is enabled but there are issues with git/gh CLI tools:');
71+
gitCliCheck.errors.forEach(error => logger.warn(`- ${error}`));
72+
73+
if (!gitCliCheck.gitAvailable || !gitCliCheck.ghAvailable) {
74+
logger.warn('GitHub mode requires git and gh CLI tools to be installed.');
75+
logger.warn('Please install the missing tools or disable GitHub mode with --githubMode false');
76+
} else if (!gitCliCheck.ghAuthenticated) {
77+
logger.warn('GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.');
78+
}
79+
} else {
80+
logger.info('GitHub mode is enabled and all required CLI tools are available.');
81+
}
82+
}
83+
6184
const tokenTracker = new TokenTracker(
6285
'Root',
6386
undefined,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { checkGitCli, GitCliCheckResult } from './gitCliCheck';
3+
4+
// Mock the child_process module
5+
vi.mock('child_process', () => ({
6+
exec: vi.fn(),
7+
}));
8+
9+
// Mock the util module
10+
vi.mock('util', () => ({
11+
promisify: vi.fn((fn) => {
12+
return (cmd: string) => {
13+
return new Promise((resolve, reject) => {
14+
fn(cmd, (error: Error | null, result: { stdout: string }) => {
15+
if (error) {
16+
reject(error);
17+
} else {
18+
resolve(result);
19+
}
20+
});
21+
});
22+
};
23+
}),
24+
}));
25+
26+
// Import the mocked modules
27+
import { exec } from 'child_process';
28+
29+
describe('gitCliCheck', () => {
30+
const mockExec = exec as unknown as vi.Mock;
31+
32+
beforeEach(() => {
33+
mockExec.mockReset();
34+
});
35+
36+
it('should return all true when git and gh are available and authenticated', async () => {
37+
// Mock successful responses
38+
mockExec.mockImplementation((cmd: string, callback: Function) => {
39+
if (cmd === 'git --version') {
40+
callback(null, { stdout: 'git version 2.30.1' });
41+
} else if (cmd === 'gh --version') {
42+
callback(null, { stdout: 'gh version 2.0.0' });
43+
} else if (cmd === 'gh auth status') {
44+
callback(null, { stdout: 'Logged in to github.com as username' });
45+
}
46+
});
47+
48+
const result = await checkGitCli();
49+
50+
expect(result.gitAvailable).toBe(true);
51+
expect(result.ghAvailable).toBe(true);
52+
expect(result.ghAuthenticated).toBe(true);
53+
expect(result.errors).toHaveLength(0);
54+
});
55+
56+
it('should detect when git is not available', async () => {
57+
mockExec.mockImplementation((cmd: string, callback: Function) => {
58+
if (cmd === 'git --version') {
59+
callback(new Error('Command not found'));
60+
} else if (cmd === 'gh --version') {
61+
callback(null, { stdout: 'gh version 2.0.0' });
62+
} else if (cmd === 'gh auth status') {
63+
callback(null, { stdout: 'Logged in to github.com as username' });
64+
}
65+
});
66+
67+
const result = await checkGitCli();
68+
69+
expect(result.gitAvailable).toBe(false);
70+
expect(result.ghAvailable).toBe(true);
71+
expect(result.ghAuthenticated).toBe(true);
72+
expect(result.errors).toContain('Git CLI is not available. Please install git.');
73+
});
74+
75+
it('should detect when gh is not available', async () => {
76+
mockExec.mockImplementation((cmd: string, callback: Function) => {
77+
if (cmd === 'git --version') {
78+
callback(null, { stdout: 'git version 2.30.1' });
79+
} else if (cmd === 'gh --version') {
80+
callback(new Error('Command not found'));
81+
}
82+
});
83+
84+
const result = await checkGitCli();
85+
86+
expect(result.gitAvailable).toBe(true);
87+
expect(result.ghAvailable).toBe(false);
88+
expect(result.ghAuthenticated).toBe(false);
89+
expect(result.errors).toContain('GitHub CLI is not available. Please install gh CLI.');
90+
});
91+
92+
it('should detect when gh is not authenticated', async () => {
93+
mockExec.mockImplementation((cmd: string, callback: Function) => {
94+
if (cmd === 'git --version') {
95+
callback(null, { stdout: 'git version 2.30.1' });
96+
} else if (cmd === 'gh --version') {
97+
callback(null, { stdout: 'gh version 2.0.0' });
98+
} else if (cmd === 'gh auth status') {
99+
callback(new Error('You are not logged into any GitHub hosts'));
100+
}
101+
});
102+
103+
const result = await checkGitCli();
104+
105+
expect(result.gitAvailable).toBe(true);
106+
expect(result.ghAvailable).toBe(true);
107+
expect(result.ghAuthenticated).toBe(false);
108+
expect(result.errors).toContain('GitHub CLI is not authenticated. Please run "gh auth login".');
109+
});
110+
});

packages/cli/src/utils/gitCliCheck.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { exec } from 'child_process';
2+
import { promisify } from 'util';
3+
import { Logger } from 'mycoder-agent';
4+
5+
const execAsync = promisify(exec);
6+
7+
/**
8+
* Result of CLI tool checks
9+
*/
10+
export interface GitCliCheckResult {
11+
gitAvailable: boolean;
12+
ghAvailable: boolean;
13+
ghAuthenticated: boolean;
14+
errors: string[];
15+
}
16+
17+
/**
18+
* Checks if git command is available
19+
*/
20+
async function checkGitAvailable(): Promise<boolean> {
21+
try {
22+
await execAsync('git --version');
23+
return true;
24+
} catch (error) {
25+
return false;
26+
}
27+
}
28+
29+
/**
30+
* Checks if gh command is available
31+
*/
32+
async function checkGhAvailable(): Promise<boolean> {
33+
try {
34+
await execAsync('gh --version');
35+
return true;
36+
} catch (error) {
37+
return false;
38+
}
39+
}
40+
41+
/**
42+
* Checks if gh is authenticated
43+
*/
44+
async function checkGhAuthenticated(): Promise<boolean> {
45+
try {
46+
const { stdout } = await execAsync('gh auth status');
47+
return stdout.includes('Logged in to');
48+
} catch (error) {
49+
return false;
50+
}
51+
}
52+
53+
/**
54+
* Checks if git and gh CLI tools are available and if gh is authenticated
55+
* @param logger Optional logger for debug output
56+
* @returns Object with check results
57+
*/
58+
export async function checkGitCli(logger?: Logger): Promise<GitCliCheckResult> {
59+
const result: GitCliCheckResult = {
60+
gitAvailable: false,
61+
ghAvailable: false,
62+
ghAuthenticated: false,
63+
errors: [],
64+
};
65+
66+
logger?.debug('Checking for git CLI availability...');
67+
result.gitAvailable = await checkGitAvailable();
68+
69+
logger?.debug('Checking for gh CLI availability...');
70+
result.ghAvailable = await checkGhAvailable();
71+
72+
if (result.ghAvailable) {
73+
logger?.debug('Checking for gh CLI authentication...');
74+
result.ghAuthenticated = await checkGhAuthenticated();
75+
}
76+
77+
// Collect any errors
78+
if (!result.gitAvailable) {
79+
result.errors.push('Git CLI is not available. Please install git.');
80+
}
81+
82+
if (!result.ghAvailable) {
83+
result.errors.push('GitHub CLI is not available. Please install gh CLI.');
84+
} else if (!result.ghAuthenticated) {
85+
result.errors.push('GitHub CLI is not authenticated. Please run "gh auth login".');
86+
}
87+
88+
return result;
89+
}

0 commit comments

Comments
 (0)