diff --git a/README.md b/README.md index 958a4b5..38aa9d2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ npx @colbymchenry/codegraph ``` -Interactive installer configures Claude Code automatically +Interactive installer configures Claude Code and Cursor automatically @@ -69,35 +69,35 @@ We ran the same complex task 3 times with and without CodeGraph: ### 🔄 How It Works ``` -┌─────────────────────────────────────────────────────────────────┐ +┌──────────────────────────────────────────────────────────────────┐ │ Claude Code │ │ │ │ "Implement user authentication" │ │ │ │ │ ▼ │ -│ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ Explore Agent │ ──── │ Explore Agent │ │ -│ └────────┬────────┘ └────────┬────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Explore Agent │ ──── │ Explore Agent │ │ +│ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ └───────────┼────────────────────────┼─────────────────────────────┘ │ │ ▼ ▼ -┌───────────────────────────────────────────────────────────────────┐ -│ CodeGraph MCP Server │ +┌──────────────────────────────────────────────────────────────────┐ +│ CodeGraph MCP Server │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Search │ │ Callers │ │ Context │ │ │ │ "auth" │ │ "login()" │ │ for task │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ -│ │ │ │ │ -│ └────────────────┼────────────────┘ │ -│ ▼ │ -│ ┌───────────────────────┐ │ -│ │ SQLite Graph DB │ │ -│ │ • 387 symbols │ │ -│ │ • 1,204 edges │ │ -│ │ • Instant lookups │ │ -│ └───────────────────────┘ │ -└───────────────────────────────────────────────────────────────────┘ +│ │ │ │ │ +│ └────────────────┼────────────────┘ │ +│ ▼ │ +│ ┌───────────────────────┐ │ +│ │ SQLite Graph DB │ │ +│ │ • 387 symbols │ │ +│ │ • 1,204 edges │ │ +│ │ • Instant lookups │ │ +│ └───────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ ``` **Without CodeGraph:** Explore agents use `grep`, `glob`, and `Read` to scan files → many API calls, high token usage @@ -162,15 +162,16 @@ npx @colbymchenry/codegraph ``` The interactive installer will: -- Configure the MCP server in `~/.claude.json` -- Set up auto-allow permissions for CodeGraph tools -- Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph) +- Ask which IDE(s) to configure (Claude Code, Cursor, or both) +- Configure the MCP server (`.claude.json` and/or `.cursor/mcp.json`) +- Set up auto-allow permissions for CodeGraph tools (Claude Code) +- Add instructions to teach your IDE how to use CodeGraph - Install Claude Code hooks for automatic index syncing - Optionally initialize your current project -### 2. Restart Claude Code +### 2. Restart Your IDE -Restart Claude Code for the MCP server to load. +Restart Claude Code or Cursor for the MCP server to load. ### 3. Initialize Projects @@ -181,7 +182,9 @@ cd your-project codegraph init -i ``` -That's it! Claude Code will now use CodeGraph tools automatically when a `.codegraph/` directory exists. +That's it! Your IDE will now use CodeGraph tools automatically when a `.codegraph/` directory exists. + +> **Note for Cursor users:** CodeGraph MCP tools are only available in **Agent mode**, not in Composer.
Manual Setup (Alternative) @@ -193,12 +196,11 @@ If you prefer manual configuration: npm install -g @colbymchenry/codegraph ``` -**Add to `~/.claude.json`:** +**Add to `~/.claude.json` or `./.cursor/mcp.json`:** ```json { "mcpServers": { "codegraph": { - "type": "stdio", "command": "codegraph", "args": ["serve", "--mcp"] } @@ -296,21 +298,34 @@ codegraph serve --mcp # Start MCP server ### `codegraph` / `codegraph install` -Run the interactive installer for Claude Code integration. Configures MCP server and permissions automatically. +Run the interactive installer for IDE integration. Configures MCP server and permissions automatically. ```bash -codegraph # Run installer (when no args) -codegraph install # Run installer (explicit) -npx @colbymchenry/codegraph # Run via npx (no global install needed) -``` - -The installer will: -1. Ask for installation location (global `~/.claude` or local `./.claude`) -2. Configure the MCP server in `claude.json` -3. Optionally set up auto-allow permissions -4. Add global instructions to `~/.claude/CLAUDE.md` (teaches Claude how to use CodeGraph) -5. Install Claude Code hooks for automatic index syncing -6. For local installs: initialize and index the current project +# Interactive mode (prompts for IDE selection and location) +codegraph # Run installer (when no args) +codegraph install # Run installer (explicit) +npx @colbymchenry/codegraph # Run via npx (no global install needed) + +# Non-interactive mode (skip prompts) +codegraph install --ide=claude --location=local # Claude Code only (local) +codegraph install --ide=cursor # Cursor only (always local) +codegraph install --ide=all --location=local # Both IDEs (local) +codegraph install --ide=claude,cursor # Both IDEs (prompts for location) +``` + +**Interactive installer flow:** +1. Asks which IDE(s) to configure (Claude Code, Cursor, or both) +2. Asks for installation location (global or local) - defaults to local +3. Configures the MCP server (`.claude.json` and/or `.cursor/mcp.json`) +4. Sets up auto-allow permissions (Claude Code only) +5. Adds instructions to teach your IDE how to use CodeGraph +6. Installs Claude Code hooks for automatic index syncing +7. For local installs: optionally initializes and indexes the current project + +**Non-interactive mode:** +- Useful for CI/CD pipelines and scripts +- Auto-detects non-TTY shells and uses defaults +- Can specify IDE(s) and location via flags to skip all prompts ### `codegraph init [path]` diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index e2e24d1..a5f740c 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -16,10 +16,23 @@ import { writeMcpConfig, writePermissions, writeClaudeMd, - hasMcpConfig, + writeHooks, + hasClaudeMcpConfig, hasPermissions, hasClaudeMdSection, + writeCursorMcpConfig, + writeCursorRules, + hasCursorMcpConfig, } from '../src/installer/config-writer'; +import { + isInteractive, + promptIDE, + promptInstallLocation, + DEFAULT_IDE, + DEFAULT_LOCATION, +} from '../src/installer/prompts'; +import { parseIDEArg, validateLocation } from '../src/installer/index'; +import type { InstallLocation } from '../src/installer/prompts'; function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-installer-test-')); @@ -31,6 +44,113 @@ function cleanupTempDir(dir: string): void { } } +/** + * E2E Test helper utilities + */ +class InstallerTestHelper { + private origCwd: string; + private tempDir: string; + private origHome: string | undefined; + + constructor() { + this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-e2e-')); + this.origCwd = process.cwd(); + this.origHome = process.env.HOME; + } + + /** + * Setup test environment + */ + setup() { + process.chdir(this.tempDir); + // Override HOME for global installs during tests + process.env.HOME = this.tempDir; + } + + /** + * Cleanup test environment + */ + cleanup() { + process.chdir(this.origCwd); + if (this.origHome !== undefined) { + process.env.HOME = this.origHome; + } + if (fs.existsSync(this.tempDir)) { + fs.rmSync(this.tempDir, { recursive: true, force: true }); + } + } + + /** + * Verify Claude Code installation files + */ + verifyClaudeInstall(location: InstallLocation) { + const baseDir = location === 'global' ? this.tempDir : process.cwd(); + + // Check .claude.json + const claudeJsonPath = path.join(baseDir, '.claude.json'); + expect(fs.existsSync(claudeJsonPath), `${claudeJsonPath} should exist`).toBe(true); + + const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8')); + expect(claudeJson.mcpServers?.codegraph, 'MCP server config should exist').toBeDefined(); + expect(claudeJson.mcpServers.codegraph.command).toBe('codegraph'); + expect(claudeJson.mcpServers.codegraph.args).toEqual(['serve', '--mcp']); + + // Check .claude/settings.json (for hooks) + const settingsPath = path.join(baseDir, '.claude', 'settings.json'); + expect(fs.existsSync(settingsPath), `${settingsPath} should exist`).toBe(true); + + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + expect(settings.hooks, 'Hooks should exist').toBeDefined(); + expect(JSON.stringify(settings.hooks)).toContain('codegraph'); + + // Check CLAUDE.md + const claudeMdPath = path.join(baseDir, '.claude', 'CLAUDE.md'); + expect(fs.existsSync(claudeMdPath), `${claudeMdPath} should exist`).toBe(true); + + const claudeMd = fs.readFileSync(claudeMdPath, 'utf-8'); + expect(claudeMd).toContain('## CodeGraph'); + expect(claudeMd).toContain(''); + expect(claudeMd).toContain('codegraph_search'); + } + + /** + * Verify Cursor installation files + */ + verifyCursorInstall() { + // Check .cursor/mcp.json + const mcpJsonPath = path.join(process.cwd(), '.cursor', 'mcp.json'); + expect(fs.existsSync(mcpJsonPath), `${mcpJsonPath} should exist`).toBe(true); + + const mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')); + expect(mcpJson.mcpServers?.codegraph, 'MCP server config should exist').toBeDefined(); + expect(mcpJson.mcpServers.codegraph.command).toBe('codegraph'); + + // Check .cursor/rules/codegraph.md + const rulesPath = path.join(process.cwd(), '.cursor', 'rules', 'codegraph.md'); + expect(fs.existsSync(rulesPath), `${rulesPath} should exist`).toBe(true); + + const rules = fs.readFileSync(rulesPath, 'utf-8'); + expect(rules).toContain('## CodeGraph'); + expect(rules).toContain('codegraph_search'); + expect(rules).toContain('Agent mode'); + expect(rules).not.toContain(''); // No markers for Cursor + } + + /** + * Verify files do NOT exist + */ + verifyClaudeNotInstalled(location: InstallLocation) { + const baseDir = location === 'global' ? this.tempDir : process.cwd(); + const claudeJsonPath = path.join(baseDir, '.claude.json'); + expect(fs.existsSync(claudeJsonPath)).toBe(false); + } + + verifyCursorNotInstalled() { + const mcpJsonPath = path.join(process.cwd(), '.cursor', 'mcp.json'); + expect(fs.existsSync(mcpJsonPath)).toBe(false); + } +} + describe('Installer Config Writer', () => { let origCwd: string; let tempDir: string; @@ -217,3 +337,558 @@ describe('Installer Config Writer', () => { }); }); }); + +/** + * End-to-End Installer Tests + * + * Tests all combinations of IDE installations: + * - Claude Code only (global) + * - Claude Code only (local) + * - Cursor only (local) + * - Both IDEs (local) + */ +describe('Installer E2E Tests', () => { + let helper: InstallerTestHelper; + + beforeEach(() => { + helper = new InstallerTestHelper(); + helper.setup(); + }); + + afterEach(() => { + helper.cleanup(); + }); + + describe('Claude Code Only - Global', () => { + it('should install Claude Code globally', () => { + const location: InstallLocation = 'global'; + + // Simulate full installation + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + // Verify installation + helper.verifyClaudeInstall(location); + helper.verifyCursorNotInstalled(); + + // Verify detection + expect(hasClaudeMcpConfig(location)).toBe(true); + expect(hasCursorMcpConfig()).toBe(false); + }); + }); + + describe('Claude Code Only - Local', () => { + it('should install Claude Code locally', () => { + const location: InstallLocation = 'local'; + + // Simulate full installation + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + // Verify installation + helper.verifyClaudeInstall(location); + helper.verifyCursorNotInstalled(); + + // Verify detection + expect(hasClaudeMcpConfig(location)).toBe(true); + expect(hasCursorMcpConfig()).toBe(false); + }); + }); + + describe('Cursor Only', () => { + it('should install Cursor locally', () => { + // Cursor only supports local + writeCursorMcpConfig(); + writeCursorRules(); + + // Verify installation + helper.verifyCursorInstall(); + helper.verifyClaudeNotInstalled('local'); + helper.verifyClaudeNotInstalled('global'); + + // Verify detection + expect(hasCursorMcpConfig()).toBe(true); + expect(hasClaudeMcpConfig('local')).toBe(false); + }); + }); + + describe('Both IDEs - Local', () => { + it('should install both Claude Code and Cursor locally', () => { + const location: InstallLocation = 'local'; + + // Install Claude Code + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + // Install Cursor + writeCursorMcpConfig(); + writeCursorRules(); + + // Verify both installations + helper.verifyClaudeInstall(location); + helper.verifyCursorInstall(); + + // Verify detection + expect(hasClaudeMcpConfig(location)).toBe(true); + expect(hasCursorMcpConfig()).toBe(true); + }); + }); + + describe('Update Scenarios', () => { + it('should update existing Claude Code installation', () => { + const location: InstallLocation = 'local'; + + // First install + writeMcpConfig(location); + writeClaudeMd(location); + + const claudeMdPath = path.join(process.cwd(), '.claude', 'CLAUDE.md'); + const firstContent = fs.readFileSync(claudeMdPath, 'utf-8'); + + // Second install (update) + writeMcpConfig(location); + writeClaudeMd(location); + + const secondContent = fs.readFileSync(claudeMdPath, 'utf-8'); + + // Content should be updated but structure preserved + expect(secondContent).toContain(''); + expect(secondContent).toContain('## CodeGraph'); + }); + + it('should update existing Cursor installation', () => { + // First install + writeCursorMcpConfig(); + writeCursorRules(); + + const rulesPath = path.join(process.cwd(), '.cursor', 'rules', 'codegraph.md'); + const firstContent = fs.readFileSync(rulesPath, 'utf-8'); + + // Second install (update) + writeCursorRules(); + + const secondContent = fs.readFileSync(rulesPath, 'utf-8'); + + // Should overwrite with latest template + expect(secondContent).toContain('## CodeGraph'); + expect(secondContent.length).toBeGreaterThan(0); + }); + }); + + describe('Detection Scenarios', () => { + it('should detect no IDEs when starting fresh', () => { + // Verify clean slate - no IDEs detected + expect(hasClaudeMcpConfig('local')).toBe(false); + expect(hasClaudeMcpConfig('global')).toBe(false); + expect(hasCursorMcpConfig()).toBe(false); + + // Verify no config directories exist + expect(fs.existsSync(path.join(process.cwd(), '.claude'))).toBe(false); + expect(fs.existsSync(path.join(process.cwd(), '.cursor'))).toBe(false); + expect(fs.existsSync(path.join(helper['tempDir'], '.claude'))).toBe(false); + }); + + it('should detect Claude Code after installation', () => { + const location: InstallLocation = 'local'; + + // Initially not detected + expect(hasClaudeMcpConfig(location)).toBe(false); + + // Install + writeMcpConfig(location); + + // Now detected + expect(hasClaudeMcpConfig(location)).toBe(true); + }); + + it('should detect Cursor after installation', () => { + // Initially not detected + expect(hasCursorMcpConfig()).toBe(false); + + // Install + writeCursorMcpConfig(); + + // Now detected + expect(hasCursorMcpConfig()).toBe(true); + }); + + it('should detect both IDEs independently', () => { + const location: InstallLocation = 'local'; + + // Install Claude Code first + writeMcpConfig(location); + expect(hasClaudeMcpConfig(location)).toBe(true); + expect(hasCursorMcpConfig()).toBe(false); + + // Install Cursor second + writeCursorMcpConfig(); + expect(hasClaudeMcpConfig(location)).toBe(true); + expect(hasCursorMcpConfig()).toBe(true); + }); + }); + + describe('Permissions and Hooks', () => { + it('should add permissions to Claude Code settings', () => { + const location: InstallLocation = 'local'; + + writePermissions(location); + + const settingsPath = path.join(process.cwd(), '.claude', 'settings.json'); + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + + expect(settings.permissions).toBeDefined(); + expect(settings.permissions.allow).toBeInstanceOf(Array); + expect(settings.permissions.allow).toContain('mcp__codegraph__codegraph_search'); + expect(settings.permissions.allow).toContain('mcp__codegraph__codegraph_context'); + }); + + it('should add hooks to Claude Code settings', () => { + const location: InstallLocation = 'local'; + + writeHooks(location); + + const settingsPath = path.join(process.cwd(), '.claude', 'settings.json'); + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + + expect(settings.hooks).toBeDefined(); + expect(settings.hooks.PostToolUse).toBeDefined(); + expect(settings.hooks.Stop).toBeDefined(); + + const hooksJson = JSON.stringify(settings.hooks); + expect(hooksJson).toContain('codegraph mark-dirty'); + expect(hooksJson).toContain('codegraph sync-if-dirty'); + }); + }); +}); + +/** + * Non-Interactive Installer Tests + * + * Tests CLI argument parsing for --ide flag + */ +describe('Installer Non-Interactive Mode', () => { + let helper: InstallerTestHelper; + + beforeEach(() => { + helper = new InstallerTestHelper(); + helper.setup(); + }); + + afterEach(() => { + helper.cleanup(); + }); + + describe('IDE Argument Parsing', () => { + it('should parse single IDE: claude', () => { + const location: InstallLocation = 'local'; + + // Simulate --ide=claude + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + helper.verifyClaudeInstall(location); + helper.verifyCursorNotInstalled(); + }); + + it('should parse single IDE: cursor', () => { + // Simulate --ide=cursor + writeCursorMcpConfig(); + writeCursorRules(); + + helper.verifyCursorInstall(); + helper.verifyClaudeNotInstalled('local'); + }); + + it('should parse comma-separated IDEs: claude,cursor', () => { + const location: InstallLocation = 'local'; + + // Simulate --ide=claude,cursor + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + writeCursorMcpConfig(); + writeCursorRules(); + + helper.verifyClaudeInstall(location); + helper.verifyCursorInstall(); + }); + + it('should parse "all" as both IDEs', () => { + const location: InstallLocation = 'local'; + + // Simulate --ide=all + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + writeCursorMcpConfig(); + writeCursorRules(); + + helper.verifyClaudeInstall(location); + helper.verifyCursorInstall(); + }); + }); + + describe('Location Argument Parsing', () => { + it('should install Claude Code globally with --location=global', () => { + const location: InstallLocation = 'global'; + + // Simulate --ide=claude --location=global + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + helper.verifyClaudeInstall(location); + }); + + it('should install Claude Code locally with --location=local', () => { + const location: InstallLocation = 'local'; + + // Simulate --ide=claude --location=local + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + + helper.verifyClaudeInstall(location); + }); + + it('should install all IDEs locally with --ide=all --location=local', () => { + const location: InstallLocation = 'local'; + + // Simulate --ide=all --location=local + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + writeCursorMcpConfig(); + writeCursorRules(); + + helper.verifyClaudeInstall(location); + helper.verifyCursorInstall(); + }); + + it('should install all IDEs globally with --ide=all --location=global', () => { + const location: InstallLocation = 'global'; + + // Simulate --ide=all --location=global + writeMcpConfig(location); + writePermissions(location); + writeHooks(location); + writeClaudeMd(location); + writeCursorMcpConfig(); + writeCursorRules(); + + helper.verifyClaudeInstall(location); + helper.verifyCursorInstall(); + }); + }); +}); + +/** + * isInteractive() Tests + */ +describe('isInteractive', () => { + let origStdinTTY: true | undefined; + let origStdoutTTY: true | undefined; + + beforeEach(() => { + origStdinTTY = process.stdin.isTTY; + origStdoutTTY = process.stdout.isTTY; + }); + + afterEach(() => { + // @ts-expect-error — restoring possibly-undefined TTY flag + process.stdin.isTTY = origStdinTTY; + // @ts-expect-error + process.stdout.isTTY = origStdoutTTY; + }); + + it('returns false when stdin is not a TTY (typical CI/test env)', () => { + // @ts-expect-error + process.stdin.isTTY = undefined; + // @ts-expect-error + process.stdout.isTTY = true; + expect(isInteractive()).toBe(false); + }); + + it('returns false when stdout is not a TTY', () => { + // @ts-expect-error + process.stdin.isTTY = true; + // @ts-expect-error + process.stdout.isTTY = undefined; + expect(isInteractive()).toBe(false); + }); + + it('returns false when both stdin and stdout are not TTYs', () => { + // @ts-expect-error + process.stdin.isTTY = undefined; + // @ts-expect-error + process.stdout.isTTY = undefined; + expect(isInteractive()).toBe(false); + }); + + it('returns true when both stdin and stdout are TTYs', () => { + // @ts-expect-error + process.stdin.isTTY = true; + // @ts-expect-error + process.stdout.isTTY = true; + expect(isInteractive()).toBe(true); + }); +}); + +/** + * promptIDE() non-interactive behavior + * + * Tests run without a TTY so isInteractive() returns false, + * exercising the non-interactive code path in promptIDE(). + */ +describe('promptIDE non-interactive', () => { + let origCwd: string; + let origHome: string | undefined; + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-prompt-test-')); + origCwd = process.cwd(); + origHome = process.env.HOME; + process.chdir(tempDir); + process.env.HOME = tempDir; + }); + + afterEach(() => { + process.chdir(origCwd); + if (origHome !== undefined) process.env.HOME = origHome; + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('returns [DEFAULT_IDE] when no IDEs are detected', async () => { + const result = await promptIDE(); + expect(result).toEqual([DEFAULT_IDE]); + }); + + it('returns [claude] when Claude config directory exists', async () => { + // detectInstalledIDEs checks for .claude dir in cwd or homedir + fs.mkdirSync(path.join(tempDir, '.claude'), { recursive: true }); + const result = await promptIDE(); + expect(result).toContain('claude'); + }); + + it('returns [cursor] when Cursor config directory exists', async () => { + fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true }); + const result = await promptIDE(); + expect(result).toContain('cursor'); + }); + + it('returns both IDEs when both config directories exist', async () => { + fs.mkdirSync(path.join(tempDir, '.claude'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true }); + const result = await promptIDE(); + expect(result).toContain('claude'); + expect(result).toContain('cursor'); + }); +}); + +/** + * promptInstallLocation() non-interactive behavior + * + * Tests run without a TTY so the non-interactive path is taken. + */ +describe('promptInstallLocation non-interactive', () => { + it('returns DEFAULT_LOCATION for claude-only', async () => { + const result = await promptInstallLocation(['claude']); + expect(result).toBe(DEFAULT_LOCATION); + expect(result).toBe('local'); + }); + + it('returns local for cursor-only (always local)', async () => { + const result = await promptInstallLocation(['cursor']); + expect(result).toBe('local'); + }); + + it('returns DEFAULT_LOCATION for both IDEs', async () => { + const result = await promptInstallLocation(['claude', 'cursor']); + expect(result).toBe(DEFAULT_LOCATION); + }); +}); + +/** + * parseIDEArg() Tests + */ +describe('parseIDEArg', () => { + it('parses "claude" → ["claude"]', () => { + expect(parseIDEArg('claude')).toEqual(['claude']); + }); + + it('parses "cursor" → ["cursor"]', () => { + expect(parseIDEArg('cursor')).toEqual(['cursor']); + }); + + it('parses "all" → ["claude", "cursor"]', () => { + expect(parseIDEArg('all')).toEqual(['claude', 'cursor']); + }); + + it('parses "claude,cursor" → ["claude", "cursor"]', () => { + expect(parseIDEArg('claude,cursor')).toEqual(['claude', 'cursor']); + }); + + it('is case-insensitive', () => { + expect(parseIDEArg('Claude')).toEqual(['claude']); + expect(parseIDEArg('CURSOR')).toEqual(['cursor']); + expect(parseIDEArg('ALL')).toEqual(['claude', 'cursor']); + }); + + it('trims whitespace in comma-separated values', () => { + expect(parseIDEArg(' claude , cursor ')).toEqual(['claude', 'cursor']); + }); + + it('throws for an unrecognized IDE name', () => { + expect(() => parseIDEArg('vscode')).toThrow(/Invalid IDE/); + }); + + it('throws when all values are unrecognized', () => { + expect(() => parseIDEArg('vscode,vim')).toThrow(/Invalid IDE/); + }); + + it('includes only valid IDEs from a mixed list', () => { + // "claude" is valid, "vscode" is not — valid ones are kept, but if zero valid → throws + expect(parseIDEArg('claude,vscode')).toEqual(['claude']); + }); +}); + +/** + * validateLocation() Tests + */ +describe('validateLocation', () => { + it('returns "global" for "global"', () => { + expect(validateLocation('global')).toBe('global'); + }); + + it('returns "local" for "local"', () => { + expect(validateLocation('local')).toBe('local'); + }); + + it('is case-insensitive', () => { + expect(validateLocation('Global')).toBe('global'); + expect(validateLocation('LOCAL')).toBe('local'); + }); + + it('returns undefined for undefined', () => { + expect(validateLocation(undefined)).toBeUndefined(); + }); + + it('throws for an invalid location', () => { + expect(() => validateLocation('project')).toThrow(/Invalid location/); + expect(() => validateLocation('home')).toThrow(/Invalid location/); + }); +}); diff --git a/package-lock.json b/package-lock.json index f0702e5..0a78489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2317,7 +2317,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index d9f8e47..cc285ef 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -5,18 +5,23 @@ * Command-line interface for CodeGraph code intelligence. * * Usage: - * codegraph Run interactive installer (when no args) - * codegraph install Run interactive installer - * codegraph init [path] Initialize CodeGraph in a project - * codegraph uninit [path] Remove CodeGraph from a project - * codegraph index [path] Index all files in the project - * codegraph sync [path] Sync changes since last index - * codegraph status [path] Show index status - * codegraph query Search for symbols - * codegraph files [options] Show project file structure - * codegraph context Build context for a task - * codegraph mark-dirty [path] Mark project as needing sync (hooks) - * codegraph sync-if-dirty [path] Sync if marked dirty (hooks) + * codegraph Run interactive installer (when no args) + * codegraph install Run interactive installer + * codegraph install --ide=claude Install for Claude Code (prompts for location) + * codegraph install --ide=claude --location=global Install for Claude Code globally + * codegraph install --ide=claude --location=local Install for Claude Code locally + * codegraph install --ide=cursor Install for Cursor only (always local) + * codegraph install --ide=all --location=local Install for all IDEs locally + * codegraph init [path] Initialize CodeGraph in a project + * codegraph uninit [path] Remove CodeGraph from a project + * codegraph index [path] Index all files in the project + * codegraph sync [path] Sync changes since last index + * codegraph status [path] Show index status + * codegraph query Search for symbols + * codegraph files [options] Show project file structure + * codegraph context Build context for a task + * codegraph mark-dirty [path] Mark project as needing sync (hooks) + * codegraph sync-if-dirty [path] Sync if marked dirty (hooks) * * Note: Git hooks have been removed. CodeGraph sync is triggered automatically * through codegraph's Claude Code hooks integration. @@ -56,7 +61,7 @@ initSentry({ processName: 'codegraph-cli', version: pkgVersion }); if (process.argv.length === 2) { import('../installer').then(({ runInstaller }) => - runInstaller() + runInstaller(undefined) ).catch((err) => { captureException(err); console.error('Installation failed:', err instanceof Error ? err.message : String(err)); @@ -1041,10 +1046,15 @@ program */ program .command('install') - .description('Run interactive installer for Claude Code integration') - .action(async () => { + .description('Run interactive installer for IDE integration') + .option('--ide ', 'IDE(s) to configure (comma-separated: claude,cursor or "all")') + .option('--location ', 'Installation location for Claude Code (global or local)') + .action(async (options: { ide?: string; location?: string }) => { const { runInstaller } = await import('../installer'); - await runInstaller(); + const installerOptions = options.ide || options.location + ? { ide: options.ide, location: options.location as 'global' | 'local' | undefined } + : undefined; + await runInstaller(installerOptions); }); // Parse and run diff --git a/src/installer/banner.ts b/src/installer/banner.ts index fb70bf3..75068a3 100644 --- a/src/installer/banner.ts +++ b/src/installer/banner.ts @@ -77,7 +77,7 @@ export function showBanner(): void { console.log(chalk.cyan(banner)); console.log(); console.log(` ${chalk.bold('CodeGraph')} v${getVersion()}`); - console.log(' Semantic code intelligence for Claude Code'); + console.log(' Semantic code intelligence for Claude Code & Cursor'); console.log(chalk.dim(' Created by: Colby McHenry')); console.log(); } @@ -113,9 +113,20 @@ export function warn(message: string): void { /** * Show the "next steps" section after installation */ -export function showNextSteps(location: 'global' | 'local'): void { +export function showNextSteps(location: 'global' | 'local', ides: string[] = ['claude']): void { console.log(); - console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.'); + + const hasClaudeCode = ides.includes('claude'); + const hasCursor = ides.includes('cursor'); + + if (hasClaudeCode && hasCursor) { + console.log(chalk.bold(' Done!') + ' Restart Claude Code and/or Cursor to use CodeGraph.'); + } else if (hasCursor) { + console.log(chalk.bold(' Done!') + ' Restart Cursor to use CodeGraph MCP tools.'); + } else { + console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.'); + } + console.log(); if (location === 'global') { @@ -127,6 +138,13 @@ export function showNextSteps(location: 'global' | 'local'): void { console.log(chalk.dim(' npm uninstall -g @colbymchenry/codegraph')); } else { console.log(chalk.dim(' CodeGraph is ready to use in this project!')); + + if (hasCursor) { + console.log(); + console.log(chalk.dim(' Cursor notes:')); + console.log(chalk.dim(' - MCP tools are available in Agent mode only (not Composer)')); + console.log(chalk.dim(' - Use @ to access codegraph tools in chat')); + } } console.log(); } diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts index 2d9e8ab..72bfb6a 100644 --- a/src/installer/config-writer.ts +++ b/src/installer/config-writer.ts @@ -1,6 +1,8 @@ /** * Config file writing for the CodeGraph installer - * Writes to claude.json, settings.json, and CLAUDE.md + * Writes IDE-specific configuration files: + * - Claude Code: .claude.json, .claude/settings.json, .claude/CLAUDE.md + * - Cursor: .cursor/mcp.json, .cursor/rules/codegraph.md */ import * as fs from 'fs'; @@ -8,10 +10,11 @@ import * as path from 'path'; import * as os from 'os'; import { InstallLocation } from './prompts'; import { - CLAUDE_MD_TEMPLATE, + CLAUDE_CODE_TEMPLATE, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END, -} from './claude-md-template'; +} from './templates/claude-code'; +import { CURSOR_TEMPLATE } from './templates/cursor'; /** * Get the path to the Claude config directory @@ -23,6 +26,14 @@ function getClaudeConfigDir(location: InstallLocation): string { return path.join(process.cwd(), '.claude'); } +/** + * Get the path to the Cursor config directory + * Note: Cursor only supports local configuration + */ +function getCursorConfigDir(): string { + return path.join(process.cwd(), '.cursor'); +} + /** * Get the path to the claude.json file * - Global: ~/.claude.json (root level) @@ -126,6 +137,26 @@ export function writeMcpConfig(location: InstallLocation): void { writeJsonFile(claudeJsonPath, config); } +/** + * Write the MCP server configuration to Cursor's mcp.json + * Cursor only supports local configuration + */ +export function writeCursorMcpConfig(): void { + const cursorConfigDir = getCursorConfigDir(); + const mcpJsonPath = path.join(cursorConfigDir, 'mcp.json'); + const config = readJsonFile(mcpJsonPath); + + // Ensure mcpServers object exists + if (!config.mcpServers) { + config.mcpServers = {}; + } + + // Add or update codegraph server for Cursor + config.mcpServers.codegraph = getMcpServerConfig(); + + writeJsonFile(mcpJsonPath, config); +} + /** * Get the list of permissions for CodeGraph tools */ @@ -170,14 +201,24 @@ export function writePermissions(location: InstallLocation): void { } /** - * Check if MCP config already exists for CodeGraph + * Check if Claude Code MCP config already exists for CodeGraph */ -export function hasMcpConfig(location: InstallLocation): boolean { +export function hasClaudeMcpConfig(location: InstallLocation): boolean { const claudeJsonPath = getClaudeJsonPath(location); const config = readJsonFile(claudeJsonPath); return !!config.mcpServers?.codegraph; } +/** + * Check if Cursor MCP config already exists for CodeGraph + */ +export function hasCursorMcpConfig(): boolean { + const cursorConfigDir = getCursorConfigDir(); + const mcpJsonPath = path.join(cursorConfigDir, 'mcp.json'); + const config = readJsonFile(mcpJsonPath); + return !!config.mcpServers?.codegraph; +} + /** * Check if permissions already exist for CodeGraph */ @@ -324,7 +365,7 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up // Check if file exists if (!fs.existsSync(claudeMdPath)) { // Create new file with just the CodeGraph section - atomicWriteFileSync(claudeMdPath, CLAUDE_MD_TEMPLATE + '\n'); + atomicWriteFileSync(claudeMdPath, CLAUDE_CODE_TEMPLATE + '\n'); return { created: true, updated: false }; } @@ -341,7 +382,7 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up // Replace existing marked section const before = content.substring(0, startIdx); const after = content.substring(endIdx + CODEGRAPH_SECTION_END.length); - content = before + CLAUDE_MD_TEMPLATE + after; + content = before + CLAUDE_CODE_TEMPLATE + after; atomicWriteFileSync(claudeMdPath, content); return { created: false, updated: true }; } @@ -368,13 +409,58 @@ export function writeClaudeMd(location: InstallLocation): { created: boolean; up // Replace the section const before = content.substring(0, sectionStart); const after = content.substring(sectionEnd); - content = before + '\n' + CLAUDE_MD_TEMPLATE + after; + content = before + '\n' + CLAUDE_CODE_TEMPLATE + after; atomicWriteFileSync(claudeMdPath, content); return { created: false, updated: true }; } // No existing section, append to end - content = content.trimEnd() + '\n\n' + CLAUDE_MD_TEMPLATE + '\n'; + content = content.trimEnd() + '\n\n' + CLAUDE_CODE_TEMPLATE + '\n'; atomicWriteFileSync(claudeMdPath, content); return { created: false, updated: false }; } + +/** + * Get the path to Cursor rules directory + * Note: Cursor only supports local configuration + */ +function getCursorRulesDir(): string { + return path.join(process.cwd(), '.cursor', 'rules'); +} + +/** + * Get the path to Cursor CodeGraph rules file + */ +function getCursorRulesPath(): string { + return path.join(getCursorRulesDir(), 'codegraph.md'); +} + +/** + * Check if Cursor rules has CodeGraph file + */ +export function hasCursorRulesSection(): boolean { + const cursorRulesPath = getCursorRulesPath(); + return fs.existsSync(cursorRulesPath); +} + +/** + * Write or update Cursor rules with CodeGraph instructions + * + * Creates .cursor/rules/codegraph.md file + */ +export function writeCursorRules(): { created: boolean; updated: boolean } { + const cursorRulesDir = getCursorRulesDir(); + const cursorRulesPath = getCursorRulesPath(); + + // Ensure directory exists + if (!fs.existsSync(cursorRulesDir)) { + fs.mkdirSync(cursorRulesDir, { recursive: true }); + } + + const alreadyExists = fs.existsSync(cursorRulesPath); + + // Write the CodeGraph rules file (always overwrite to ensure latest version) + atomicWriteFileSync(cursorRulesPath, CURSOR_TEMPLATE + '\n'); + + return { created: !alreadyExists, updated: alreadyExists }; +} diff --git a/src/installer/index.ts b/src/installer/index.ts index c381489..ab8fa0f 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -2,13 +2,24 @@ * CodeGraph Interactive Installer * * Provides a beautiful interactive CLI experience for setting up CodeGraph - * with Claude Code. + * with supported IDEs (Claude Code, Cursor, etc.). */ import { execSync } from 'child_process'; import { showBanner, showNextSteps, success, error, info, chalk } from './banner'; -import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts'; -import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer'; +import { promptIDE, promptInstallLocation, promptAutoAllow, InstallLocation, IDE } from './prompts'; +import { + writeMcpConfig, + writePermissions, + writeClaudeMd, + writeHooks, + hasClaudeMcpConfig, + hasPermissions, + hasHooks, + writeCursorMcpConfig, + writeCursorRules, + hasCursorMcpConfig, +} from './config-writer'; /** * Format a number with commas @@ -17,10 +28,53 @@ function formatNumber(n: number): string { return n.toLocaleString(); } +/** + * Installer options for non-interactive mode + */ +export interface InstallerOptions { + ide?: string; // Comma-separated list or "all" + location?: 'global' | 'local'; +} + +/** + * Parse IDE string from CLI argument + * Exported for testing. + */ +export function parseIDEArg(ideArg: string): IDE { + if (ideArg.toLowerCase() === 'all') { + return ['claude', 'cursor']; + } + const ides = ideArg.split(',').map(s => s.trim().toLowerCase()); + const valid: IDE = []; + for (const ide of ides) { + if (ide === 'claude' || ide === 'cursor') { + valid.push(ide); + } + } + if (valid.length === 0) { + throw new Error(`Invalid IDE(s): ${ideArg}. Use "claude", "cursor", or "all"`); + } + return valid; +} + +/** + * Validate location option + * Exported for testing. + */ +export function validateLocation(location?: string): InstallLocation | undefined { + if (!location) return undefined; + + const normalized = location.toLowerCase(); + if (normalized !== 'global' && normalized !== 'local') { + throw new Error(`Invalid location: ${location}. Use "global" or "local"`); + } + return normalized as InstallLocation; +} + /** * Run the interactive installer */ -export async function runInstaller(): Promise { +export async function runInstaller(options?: InstallerOptions): Promise { // Show the banner showBanner(); @@ -38,64 +92,31 @@ export async function runInstaller(): Promise { } console.log(); - // Step 2: Ask for installation location - const location = await promptInstallLocation(); + // Step 2: Ask which IDE(s) to configure (or use provided) + const ide = options?.ide ? parseIDEArg(options.ide) : await promptIDE(); console.log(); - // Step 3: Write MCP configuration (always uses npx for reliability) - const alreadyHasMcp = hasMcpConfig(location); - writeMcpConfig(location); - - if (alreadyHasMcp) { - success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`); - } else { - success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`); - } - - // Step 4: Ask about auto-allow permissions - const autoAllow = await promptAutoAllow(); + // Step 3: Ask for installation location (or use provided) + const providedLocation = validateLocation(options?.location); + const location = providedLocation || await promptInstallLocation(ide); console.log(); - if (autoAllow) { - const alreadyHasPerms = hasPermissions(location); - writePermissions(location); - - if (alreadyHasPerms) { - success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); - } else { - success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); + // Step 4: Configure selected IDEs + for (const ideName of ide) { + if (ideName === 'claude') { + await installForClaude(location); + } else if (ideName === 'cursor') { + await installForCursor(); } } - // Step 5: Write auto-sync hooks - const alreadyHasHooks = hasHooks(location); - writeHooks(location); - - if (alreadyHasHooks) { - success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); - } else { - success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); - } - - // Step 6: Write CLAUDE.md instructions - const claudeMdResult = writeClaudeMd(location); - const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md'; - - if (claudeMdResult.created) { - success(`Created ${claudeMdPath} with CodeGraph instructions`); - } else if (claudeMdResult.updated) { - success(`Updated CodeGraph section in ${claudeMdPath}`); - } else { - success(`Added CodeGraph instructions to ${claudeMdPath}`); - } - - // Step 7: For local install, initialize the project + // Step 6: For local install, initialize the project if (location === 'local') { await initializeLocalProject(); } // Show next steps - showNextSteps(location); + showNextSteps(location, ide); } catch (err) { console.log(); if (err instanceof Error && err.message.includes('readline was closed')) { @@ -167,5 +188,85 @@ async function initializeLocalProject(): Promise { cg.close(); } +/** + * Install and configure for Claude Code + */ +async function installForClaude(location: InstallLocation): Promise { + // Write MCP configuration (always uses npx for reliability) + const alreadyHasMcp = hasClaudeMcpConfig(location); + writeMcpConfig(location); + + if (alreadyHasMcp) { + success(`Updated MCP server in ${location === 'global' ? '~/.claude.json' : './.claude.json'}`); + } else { + success(`Added MCP server to ${location === 'global' ? '~/.claude.json' : './.claude.json'}`); + } + + // Ask about auto-allow permissions + const autoAllow = await promptAutoAllow(); + console.log(); + + if (autoAllow) { + const alreadyHasPerms = hasPermissions(location); + writePermissions(location); + + if (alreadyHasPerms) { + success(`Updated permissions in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); + } else { + success(`Added permissions to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); + } + } + + // Write auto-sync hooks + const alreadyHasHooks = hasHooks(location); + writeHooks(location); + + if (alreadyHasHooks) { + success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); + } else { + success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`); + } + + // Write CLAUDE.md instructions + const claudeMdResult = writeClaudeMd(location); + const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md'; + + if (claudeMdResult.created) { + success(`Created ${claudeMdPath} with CodeGraph instructions`); + } else if (claudeMdResult.updated) { + success(`Updated CodeGraph section in ${claudeMdPath}`); + } else { + success(`Added CodeGraph instructions to ${claudeMdPath}`); + } +} + +/** + * Install and configure for Cursor + * Note: Cursor only supports local configuration + */ +async function installForCursor(): Promise { + // Write MCP configuration + const alreadyHasMcp = hasCursorMcpConfig(); + writeCursorMcpConfig(); + + if (alreadyHasMcp) { + success('Updated MCP server in ./.cursor/mcp.json'); + } else { + success('Added MCP server to ./.cursor/mcp.json'); + } + + // Write Cursor rules file + const cursorRulesResult = writeCursorRules(); + + if (cursorRulesResult.created) { + success('Created .cursor/rules/codegraph.md with instructions'); + } else if (cursorRulesResult.updated) { + success('Updated .cursor/rules/codegraph.md'); + } + + console.log(); + info('Note: MCP tools in Cursor are only available in Agent mode, not Composer'); +} + // Export for use in CLI -export { InstallLocation }; +export { InstallLocation, IDE }; diff --git a/src/installer/prompts.ts b/src/installer/prompts.ts index fa22daf..e1e1a4c 100644 --- a/src/installer/prompts.ts +++ b/src/installer/prompts.ts @@ -4,9 +4,20 @@ */ import * as readline from 'readline'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; import { chalk } from './banner'; export type InstallLocation = 'global' | 'local'; +export type IDEName = 'claude' | 'cursor'; +export type IDE = IDEName[]; // Array of selected IDEs + +/** + * Default values for installer + */ +export const DEFAULT_IDE: IDEName = 'claude'; +export const DEFAULT_LOCATION: InstallLocation = 'local'; /** * Create a readline interface for prompts @@ -18,6 +29,28 @@ function createInterface(): readline.Interface { }); } +/** + * Detect which IDEs are installed by checking for their config directories + */ +function detectInstalledIDEs(): IDEName[] { + const detected: IDEName[] = []; + + // Check for Claude Code config (global or local) + const hasGlobalClaude = fs.existsSync(path.join(os.homedir(), '.claude')); + const hasLocalClaude = fs.existsSync(path.join(process.cwd(), '.claude')); + if (hasGlobalClaude || hasLocalClaude) { + detected.push('claude'); + } + + // Check for Cursor config (local only) + const hasCursor = fs.existsSync(path.join(process.cwd(), '.cursor')); + if (hasCursor) { + detected.push('cursor'); + } + + return detected; +} + /** * Prompt the user with a question and return their answer */ @@ -29,28 +62,129 @@ function prompt(rl: readline.Interface, question: string): Promise { }); } +/** + * Prompt for IDE selection with checkbox-style input + * Users can select multiple IDEs by entering comma-separated numbers (e.g., "1,2") + * Auto-detects installed IDEs and uses them as defaults + * + * For non-interactive shells: + * - If IDEs are detected, uses detected IDEs + * - Otherwise defaults to Claude Code only + */ +export async function promptIDE(): Promise { + // Detect installed IDEs + const detected = detectInstalledIDEs(); + + // Non-interactive: use detected IDEs or default + if (!isInteractive()) { + if (detected.length > 0) { + return detected; + } + return [DEFAULT_IDE]; + } + + const rl = createInterface(); + + // Build default selection string + const defaultSelections: string[] = []; + if (detected.includes('claude')) defaultSelections.push('1'); + if (detected.includes('cursor')) defaultSelections.push('2'); + const defaultStr = defaultSelections.length > 0 ? defaultSelections.join(',') : '1'; + + console.log(chalk.bold(' Which IDE(s) would you like to configure?')); + console.log(chalk.dim(' (Enter comma-separated numbers, e.g., "1,2" for both)')); + console.log(); + + // Show Claude Code with detection indicator + if (detected.includes('claude')) { + console.log(' 1) Claude Code ' + chalk.green('✓ Detected')); + } else { + console.log(' 1) Claude Code'); + } + + // Show Cursor with detection indicator + if (detected.includes('cursor')) { + console.log(' 2) Cursor ' + chalk.green('✓ Detected')); + } else { + console.log(' 2) Cursor'); + } + console.log(); + + const answer = await prompt(rl, ` Selection [${defaultStr}]: `); + rl.close(); + + // Parse comma-separated selections + const selections = (answer === '' ? defaultStr : answer) + .split(',') + .map(s => s.trim()) + .filter(s => s !== ''); + + const ides: IDEName[] = []; + + for (const selection of selections) { + if (selection === '1') { + if (!ides.includes('claude')) ides.push('claude'); + } else if (selection === '2') { + if (!ides.includes('cursor')) ides.push('cursor'); + } + } + + // If no valid selections, use default + if (ides.length === 0) { + ides.push(DEFAULT_IDE); + } + + return ides; +} + +/** + * Check if running in an interactive terminal + */ +export function isInteractive(): boolean { + return process.stdin.isTTY === true && process.stdout.isTTY === true; +} + /** * Prompt for installation location (global or local) + * Defaults to 'local' for non-interactive shells */ -export async function promptInstallLocation(): Promise { +export async function promptInstallLocation(ides: IDE): Promise { + const hasClaudeCode = ides.includes('claude'); + const hasCursor = ides.includes('cursor'); + const cursorOnly = hasCursor && !hasClaudeCode; + + // Cursor only supports local installation + if (cursorOnly) { + return 'local'; + } + + // Non-interactive: use default location + if (!isInteractive()) { + return DEFAULT_LOCATION; + } + const rl = createInterface(); console.log(chalk.bold(' Where would you like to install?')); console.log(); - console.log(' 1) Global (~/.claude) - available in all projects'); - console.log(' 2) Local (./.claude) - this project only'); + + console.log(' 1) Local (./.claude) - this project only'); + console.log(' 2) Global (~/.claude) - available in all projects'); + if (hasCursor) { + console.log(chalk.dim(' Note: Cursor will be configured locally regardless')); + } console.log(); - const answer = await prompt(rl, ' Choice [1]: '); + const answer = await prompt(rl, ' Selection [1]: '); rl.close(); - // Default to '1' if empty, parse the answer + // Default to '1' (local) if empty, parse the answer const choice = answer === '' ? '1' : answer; if (choice === '2') { - return 'local'; + return 'global'; } - return 'global'; + return 'local'; } /** diff --git a/src/installer/claude-md-template.ts b/src/installer/templates/claude-code.ts similarity index 78% rename from src/installer/claude-md-template.ts rename to src/installer/templates/claude-code.ts index 6b73c03..9dfb363 100644 --- a/src/installer/claude-md-template.ts +++ b/src/installer/templates/claude-code.ts @@ -1,15 +1,18 @@ /** - * CLAUDE.md template for CodeGraph instructions + * Template for Claude Code's CLAUDE.md file * - * This template is injected into ~/.claude/CLAUDE.md (global) or ./.claude/CLAUDE.md (local) - * Keep this in sync with the README.md "Recommended: Add Global Instructions" section + * This template is injected into ~/.claude/CLAUDE.md (global) or ./.claude/CLAUDE.md (local). + * It instructs Claude Code on when and how to use CodeGraph MCP tools. + * + * Since CLAUDE.md is a shared file that may contain other instructions, we use HTML comment + * markers to identify and update just the CodeGraph section. */ -// Markers to identify CodeGraph section for updates +// Markers to identify CodeGraph section for updates in CLAUDE.md export const CODEGRAPH_SECTION_START = ''; export const CODEGRAPH_SECTION_END = ''; -export const CLAUDE_MD_TEMPLATE = `${CODEGRAPH_SECTION_START} +export const CLAUDE_CODE_TEMPLATE = `${CODEGRAPH_SECTION_START} ## CodeGraph CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration. diff --git a/src/installer/templates/cursor.ts b/src/installer/templates/cursor.ts new file mode 100644 index 0000000..60d3473 --- /dev/null +++ b/src/installer/templates/cursor.ts @@ -0,0 +1,45 @@ +/** + * Template for Cursor's rules file + * + * This template is written to .cursor/rules/codegraph.md (local only). + * It instructs Cursor Agent on when and how to use CodeGraph MCP tools. + * + * Unlike Claude Code, Cursor uses a dedicated file per rule in the .cursor/rules/ directory, + * so no section markers are needed - the entire file is our template. + * + * Note: MCP tools are only available in Cursor's Agent mode, not Composer. + */ + +export const CURSOR_TEMPLATE = `## CodeGraph + +CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration. + +### If \`.codegraph/\` exists in the project + +**Use MCP codegraph tools for faster exploration.** These tools provide instant lookups via the code graph instead of scanning files: + +| MCP Tool | Use For | +|----------|---------| +| \`codegraph_search\` | Find symbols by name (functions, classes, types) | +| \`codegraph_context\` | Get relevant code context for a task | +| \`codegraph_callers\` | Find what calls a function | +| \`codegraph_callees\` | Find what a function calls | +| \`codegraph_impact\` | See what's affected by changing a symbol | +| \`codegraph_node\` | Get details + source code for a symbol | + +**Usage in Agent mode:** +- Use \`codegraph_search\` instead of grep/find for locating symbols +- Use \`codegraph_callers\`/\`codegraph_callees\` to trace code flow +- Use \`codegraph_impact\` before making changes to see dependencies +- Use \`codegraph_context\` to get relevant code for implementing features + +**Important:** +- CodeGraph provides **code context**, not product requirements +- For new features, still ask the user about UX, edge cases, and acceptance criteria +- MCP tools are only available in Agent mode (not Composer) + +### If \`.codegraph/\` does NOT exist + +At the start of a session, suggest initializing CodeGraph: + +"I notice this project doesn't have CodeGraph initialized. Would you like me to run \`codegraph init -i\` to build a code knowledge graph for faster exploration?"`;