diff --git a/packages/cli/README.md b/packages/cli/README.md index 1d87b66..6a48d73 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -52,30 +52,22 @@ MyCoder includes a GitHub mode that enables the agent to work with GitHub issues - Create PRs when work is complete - Create additional GitHub issues for follow-up tasks or ideas -To enable GitHub mode: +GitHub mode is **enabled by default** but requires the Git and GitHub CLI tools to be installed and configured: -1. Via CLI option (overrides config file): - -```bash -mycoder --githubMode true -``` - -2. Via configuration file: +- Git CLI (`git`) must be installed +- GitHub CLI (`gh`) must be installed and authenticated -```js -// mycoder.config.js -export default { - githubMode: true, - // other configuration options... -}; -``` +MyCoder will automatically check for these requirements when GitHub mode is enabled and will: +- Warn you if any requirements are missing +- Automatically disable GitHub mode if the required tools are not available or not authenticated -To disable GitHub mode: +To manually enable/disable GitHub mode: -1. Via CLI option: +1. Via CLI option (overrides config file): ```bash -mycoder --githubMode false +mycoder --githubMode true # Enable GitHub mode +mycoder --githubMode false # Disable GitHub mode ``` 2. Via configuration file: @@ -83,16 +75,19 @@ mycoder --githubMode false ```js // mycoder.config.js export default { - githubMode: false, + githubMode: true, // Enable GitHub mode (default) // other configuration options... }; ``` Requirements for GitHub mode: +- Git CLI (`git`) needs to be installed - GitHub CLI (`gh`) needs to be installed and authenticated - User needs to have appropriate GitHub permissions for the target repository +If GitHub mode is enabled but the requirements are not met, MyCoder will provide instructions on how to install and configure the missing tools. + ## Configuration 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. diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 1d72962..cc57b93 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -20,6 +20,7 @@ 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 { checkGitCli } from '../utils/gitCliCheck.js'; import { nameToLogIndex } from '../utils/nameToLogIndex.js'; import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js'; @@ -58,6 +59,47 @@ 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', + ); + // Disable GitHub mode if git or gh CLI is not available + logger.info('Disabling GitHub mode due to missing CLI tools.'); + config.githubMode = false; + } else if (!gitCliCheck.ghAuthenticated) { + logger.warn( + 'GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.', + ); + // Disable GitHub mode if gh CLI is not authenticated + logger.info( + 'Disabling GitHub mode due to unauthenticated GitHub CLI.', + ); + config.githubMode = false; + } + } 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/options.ts b/packages/cli/src/options.ts index 99620dc..94d2994 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -88,7 +88,9 @@ export const sharedOptions = { } as const, githubMode: { type: 'boolean', - description: 'Enable GitHub mode for working with issues and PRs', + description: + 'Enable GitHub mode for working with issues and PRs (requires git and gh CLI tools)', + default: true, } as const, upgradeCheck: { type: 'boolean', diff --git a/packages/cli/src/utils/gitCliCheck.test.ts b/packages/cli/src/utils/gitCliCheck.test.ts new file mode 100644 index 0000000..7ef16a4 --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.test.ts @@ -0,0 +1,138 @@ +import { exec } from 'child_process'; + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { checkGitCli } 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); + } + }); + }); + }; + }), +})); + +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: (error: Error | null, result: { stdout: string }) => void, + ) => { + 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: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(new Error('Command not found'), { stdout: '' }); + } 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: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(new Error('Command not found'), { stdout: '' }); + } + }, + ); + + 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: (error: Error | null, result: { stdout: string }) => void, + ) => { + 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'), { + stdout: '', + }); + } + }, + ); + + 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".', + ); + }); +}); diff --git a/packages/cli/src/utils/gitCliCheck.ts b/packages/cli/src/utils/gitCliCheck.ts new file mode 100644 index 0000000..530b732 --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.ts @@ -0,0 +1,92 @@ +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 { + return false; + } +} + +/** + * Checks if gh command is available + */ +async function checkGhAvailable(): Promise { + try { + await execAsync('gh --version'); + return true; + } catch { + 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 { + 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; +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 204fe19..5954c75 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -44,5 +44,6 @@ "allowJs": false, "checkJs": false }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] }