Skip to content

Commit f5e19ff

Browse files
authored
Merge pull request #230 from drivecore/feature/issue-217-github-mode-check
feat(cli): Add checking for git and gh CLI tools in GitHub mode
2 parents 0654722 + 52aa70b commit f5e19ff

File tree

6 files changed

+291
-21
lines changed

6 files changed

+291
-21
lines changed

packages/cli/README.md

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,47 +52,42 @@ MyCoder includes a GitHub mode that enables the agent to work with GitHub issues
5252
- Create PRs when work is complete
5353
- Create additional GitHub issues for follow-up tasks or ideas
5454

55-
To enable GitHub mode:
55+
GitHub mode is **enabled by default** but requires the Git and GitHub CLI tools to be installed and configured:
5656

57-
1. Via CLI option (overrides config file):
58-
59-
```bash
60-
mycoder --githubMode true
61-
```
62-
63-
2. Via configuration file:
57+
- Git CLI (`git`) must be installed
58+
- GitHub CLI (`gh`) must be installed and authenticated
6459

65-
```js
66-
// mycoder.config.js
67-
export default {
68-
githubMode: true,
69-
// other configuration options...
70-
};
71-
```
60+
MyCoder will automatically check for these requirements when GitHub mode is enabled and will:
61+
- Warn you if any requirements are missing
62+
- Automatically disable GitHub mode if the required tools are not available or not authenticated
7263

73-
To disable GitHub mode:
64+
To manually enable/disable GitHub mode:
7465

75-
1. Via CLI option:
66+
1. Via CLI option (overrides config file):
7667

7768
```bash
78-
mycoder --githubMode false
69+
mycoder --githubMode true # Enable GitHub mode
70+
mycoder --githubMode false # Disable GitHub mode
7971
```
8072

8173
2. Via configuration file:
8274

8375
```js
8476
// mycoder.config.js
8577
export default {
86-
githubMode: false,
78+
githubMode: true, // Enable GitHub mode (default)
8779
// other configuration options...
8880
};
8981
```
9082

9183
Requirements for GitHub mode:
9284

85+
- Git CLI (`git`) needs to be installed
9386
- GitHub CLI (`gh`) needs to be installed and authenticated
9487
- User needs to have appropriate GitHub permissions for the target repository
9588

89+
If GitHub mode is enabled but the requirements are not met, MyCoder will provide instructions on how to install and configure the missing tools.
90+
9691
## Configuration
9792

9893
MyCoder is configured using a `mycoder.config.js` file in your project root, similar to ESLint and other modern JavaScript tools. This file exports a configuration object with your preferred settings.

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ 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 { checkGitCli } from '../utils/gitCliCheck.js';
2324
import { nameToLogIndex } from '../utils/nameToLogIndex.js';
2425
import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js';
2526

@@ -58,6 +59,47 @@ export const command: CommandModule<SharedOptions, DefaultArgs> = {
5859
if (config.upgradeCheck !== false) {
5960
await checkForUpdates(logger);
6061
}
62+
63+
// Check for git and gh CLI tools if GitHub mode is enabled
64+
if (config.githubMode) {
65+
logger.debug(
66+
'GitHub mode is enabled, checking for git and gh CLI tools...',
67+
);
68+
const gitCliCheck = await checkGitCli(logger);
69+
70+
if (gitCliCheck.errors.length > 0) {
71+
logger.warn(
72+
'GitHub mode is enabled but there are issues with git/gh CLI tools:',
73+
);
74+
gitCliCheck.errors.forEach((error) => logger.warn(`- ${error}`));
75+
76+
if (!gitCliCheck.gitAvailable || !gitCliCheck.ghAvailable) {
77+
logger.warn(
78+
'GitHub mode requires git and gh CLI tools to be installed.',
79+
);
80+
logger.warn(
81+
'Please install the missing tools or disable GitHub mode with --githubMode false',
82+
);
83+
// Disable GitHub mode if git or gh CLI is not available
84+
logger.info('Disabling GitHub mode due to missing CLI tools.');
85+
config.githubMode = false;
86+
} else if (!gitCliCheck.ghAuthenticated) {
87+
logger.warn(
88+
'GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.',
89+
);
90+
// Disable GitHub mode if gh CLI is not authenticated
91+
logger.info(
92+
'Disabling GitHub mode due to unauthenticated GitHub CLI.',
93+
);
94+
config.githubMode = false;
95+
}
96+
} else {
97+
logger.info(
98+
'GitHub mode is enabled and all required CLI tools are available.',
99+
);
100+
}
101+
}
102+
61103
const tokenTracker = new TokenTracker(
62104
'Root',
63105
undefined,

packages/cli/src/options.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ export const sharedOptions = {
8888
} as const,
8989
githubMode: {
9090
type: 'boolean',
91-
description: 'Enable GitHub mode for working with issues and PRs',
91+
description:
92+
'Enable GitHub mode for working with issues and PRs (requires git and gh CLI tools)',
93+
default: true,
9294
} as const,
9395
upgradeCheck: {
9496
type: 'boolean',
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { exec } from 'child_process';
2+
3+
import { describe, it, expect, vi, beforeEach } from 'vitest';
4+
5+
import { checkGitCli } from './gitCliCheck';
6+
7+
// Mock the child_process module
8+
vi.mock('child_process', () => ({
9+
exec: vi.fn(),
10+
}));
11+
12+
// Mock the util module
13+
vi.mock('util', () => ({
14+
promisify: vi.fn((fn) => {
15+
return (cmd: string) => {
16+
return new Promise((resolve, reject) => {
17+
fn(cmd, (error: Error | null, result: { stdout: string }) => {
18+
if (error) {
19+
reject(error);
20+
} else {
21+
resolve(result);
22+
}
23+
});
24+
});
25+
};
26+
}),
27+
}));
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(
39+
(
40+
cmd: string,
41+
callback: (error: Error | null, result: { stdout: string }) => void,
42+
) => {
43+
if (cmd === 'git --version') {
44+
callback(null, { stdout: 'git version 2.30.1' });
45+
} else if (cmd === 'gh --version') {
46+
callback(null, { stdout: 'gh version 2.0.0' });
47+
} else if (cmd === 'gh auth status') {
48+
callback(null, { stdout: 'Logged in to github.com as username' });
49+
}
50+
},
51+
);
52+
53+
const result = await checkGitCli();
54+
55+
expect(result.gitAvailable).toBe(true);
56+
expect(result.ghAvailable).toBe(true);
57+
expect(result.ghAuthenticated).toBe(true);
58+
expect(result.errors).toHaveLength(0);
59+
});
60+
61+
it('should detect when git is not available', async () => {
62+
mockExec.mockImplementation(
63+
(
64+
cmd: string,
65+
callback: (error: Error | null, result: { stdout: string }) => void,
66+
) => {
67+
if (cmd === 'git --version') {
68+
callback(new Error('Command not found'), { stdout: '' });
69+
} else if (cmd === 'gh --version') {
70+
callback(null, { stdout: 'gh version 2.0.0' });
71+
} else if (cmd === 'gh auth status') {
72+
callback(null, { stdout: 'Logged in to github.com as username' });
73+
}
74+
},
75+
);
76+
77+
const result = await checkGitCli();
78+
79+
expect(result.gitAvailable).toBe(false);
80+
expect(result.ghAvailable).toBe(true);
81+
expect(result.ghAuthenticated).toBe(true);
82+
expect(result.errors).toContain(
83+
'Git CLI is not available. Please install git.',
84+
);
85+
});
86+
87+
it('should detect when gh is not available', async () => {
88+
mockExec.mockImplementation(
89+
(
90+
cmd: string,
91+
callback: (error: Error | null, result: { stdout: string }) => void,
92+
) => {
93+
if (cmd === 'git --version') {
94+
callback(null, { stdout: 'git version 2.30.1' });
95+
} else if (cmd === 'gh --version') {
96+
callback(new Error('Command not found'), { stdout: '' });
97+
}
98+
},
99+
);
100+
101+
const result = await checkGitCli();
102+
103+
expect(result.gitAvailable).toBe(true);
104+
expect(result.ghAvailable).toBe(false);
105+
expect(result.ghAuthenticated).toBe(false);
106+
expect(result.errors).toContain(
107+
'GitHub CLI is not available. Please install gh CLI.',
108+
);
109+
});
110+
111+
it('should detect when gh is not authenticated', async () => {
112+
mockExec.mockImplementation(
113+
(
114+
cmd: string,
115+
callback: (error: Error | null, result: { stdout: string }) => void,
116+
) => {
117+
if (cmd === 'git --version') {
118+
callback(null, { stdout: 'git version 2.30.1' });
119+
} else if (cmd === 'gh --version') {
120+
callback(null, { stdout: 'gh version 2.0.0' });
121+
} else if (cmd === 'gh auth status') {
122+
callback(new Error('You are not logged into any GitHub hosts'), {
123+
stdout: '',
124+
});
125+
}
126+
},
127+
);
128+
129+
const result = await checkGitCli();
130+
131+
expect(result.gitAvailable).toBe(true);
132+
expect(result.ghAvailable).toBe(true);
133+
expect(result.ghAuthenticated).toBe(false);
134+
expect(result.errors).toContain(
135+
'GitHub CLI is not authenticated. Please run "gh auth login".',
136+
);
137+
});
138+
});

packages/cli/src/utils/gitCliCheck.ts

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

packages/cli/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,6 @@
4444
"allowJs": false,
4545
"checkJs": false
4646
},
47-
"include": ["src/**/*"]
47+
"include": ["src/**/*"],
48+
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
4849
}

0 commit comments

Comments
 (0)