diff --git a/README.md b/README.md index 1db026097..4daec9ac4 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ The installer will: codegraph install --yes # auto-detect agents, install global codegraph install --target=cursor,claude --yes # explicit target list codegraph install --target=auto --location=local # detected agents, project-local +codegraph install --command /abs/path/to/codegraph # write a fork/wrapper path into MCP configs codegraph install --print-config codex # print snippet, no file writes ``` @@ -253,10 +254,13 @@ codegraph install --print-config codex # print snippet, no file wr |---|---|---| | `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,...`) | prompt | | `--location` | `global`, `local` | prompt | +| `--command ` | custom MCP command to write (fork, wrapper, absolute path) | `codegraph` | | `--yes` | (boolean) | prompt every step | | `--no-permissions` | (boolean) skip Claude auto-allow list | permissions on | | `--print-config ` | dump snippet for one agent and exit | — | +Use `--command` when you want agents to launch a fork checkout or wrapper script instead of the default `codegraph` binary on `PATH`. + ### 2. Restart Your Agent Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load. diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 697f8e976..735419c2f 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -60,6 +60,7 @@ describe('Installer targets — contract', () => { let tmpCwd: string; let origCwd: string; let homeRestore: { restore: () => void }; + let prevInstallCommand: string | undefined; beforeEach(() => { tmpHome = mkTmpDir('home'); @@ -67,9 +68,13 @@ describe('Installer targets — contract', () => { origCwd = process.cwd(); process.chdir(tmpCwd); homeRestore = setHome(tmpHome); + prevInstallCommand = process.env.CODEGRAPH_INSTALL_COMMAND; + delete process.env.CODEGRAPH_INSTALL_COMMAND; }); afterEach(() => { + if (prevInstallCommand === undefined) delete process.env.CODEGRAPH_INSTALL_COMMAND; + else process.env.CODEGRAPH_INSTALL_COMMAND = prevInstallCommand; homeRestore.restore(); process.chdir(origCwd); fs.rmSync(tmpHome, { recursive: true, force: true }); @@ -164,6 +169,7 @@ describe('Installer targets — partial-state idempotency', () => { let tmpCwd: string; let origCwd: string; let homeRestore: { restore: () => void }; + let prevInstallCommand: string | undefined; beforeEach(() => { tmpHome = mkTmpDir('home'); @@ -171,9 +177,13 @@ describe('Installer targets — partial-state idempotency', () => { origCwd = process.cwd(); process.chdir(tmpCwd); homeRestore = setHome(tmpHome); + prevInstallCommand = process.env.CODEGRAPH_INSTALL_COMMAND; + delete process.env.CODEGRAPH_INSTALL_COMMAND; }); afterEach(() => { + if (prevInstallCommand === undefined) delete process.env.CODEGRAPH_INSTALL_COMMAND; + else process.env.CODEGRAPH_INSTALL_COMMAND = prevInstallCommand; homeRestore.restore(); process.chdir(origCwd); fs.rmSync(tmpHome, { recursive: true, force: true }); @@ -199,6 +209,44 @@ describe('Installer targets — partial-state idempotency', () => { for (const f of third.files) expect(f.action).toBe('unchanged'); }); + it('codex: custom install command is written into config.toml', () => { + process.env.CODEGRAPH_INSTALL_COMMAND = '/tmp/codegraph-fork/bin/codegraph.js'; + const codex = getTarget('codex')!; + + codex.install('global', { autoAllow: false }); + + const configToml = path.join(tmpHome, '.codex', 'config.toml'); + const body = fs.readFileSync(configToml, 'utf-8'); + expect(body).toContain('command = "/tmp/codegraph-fork/bin/codegraph.js"'); + expect(body).toContain('args = ["serve", "--mcp"]'); + }); + + it('cursor: custom install command keeps the workspace --path wiring', () => { + process.env.CODEGRAPH_INSTALL_COMMAND = '/tmp/codegraph-fork/bin/codegraph.js'; + const cursor = getTarget('cursor')!; + + cursor.install('global', { autoAllow: false }); + + const mcpJson = path.join(tmpHome, '.cursor', 'mcp.json'); + const cfg = JSON.parse(fs.readFileSync(mcpJson, 'utf-8')); + expect(cfg.mcpServers.codegraph.command).toBe('/tmp/codegraph-fork/bin/codegraph.js'); + expect(cfg.mcpServers.codegraph.args).toEqual(['serve', '--mcp', '--path', '${workspaceFolder}']); + }); + + it('antigravity: custom install command overrides platform command resolution', () => { + process.env.CODEGRAPH_INSTALL_COMMAND = '/tmp/codegraph-fork/bin/codegraph.js'; + const antigravity = getTarget('antigravity')!; + + antigravity.install('global', { autoAllow: false }); + + const mcpJson = path.join(tmpHome, '.gemini', 'antigravity', 'mcp_config.json'); + const cfg = JSON.parse(fs.readFileSync(mcpJson, 'utf-8')); + expect(cfg.mcpServers.codegraph).toEqual({ + command: '/tmp/codegraph-fork/bin/codegraph.js', + args: ['serve', '--mcp'], + }); + }); + it('opencode: prefers .jsonc when both .json and .jsonc exist', () => { const opencode = getTarget('opencode')!; const dir = path.join(tmpHome, '.config', 'opencode'); diff --git a/site/src/content/docs/getting-started/installation.md b/site/src/content/docs/getting-started/installation.md index 4e8a0e2ce..a9d3c73e5 100644 --- a/site/src/content/docs/getting-started/installation.md +++ b/site/src/content/docs/getting-started/installation.md @@ -24,6 +24,7 @@ The installer will: codegraph install --yes # auto-detect agents, install global codegraph install --target=cursor,claude --yes # explicit target list codegraph install --target=auto --location=local # detected agents, project-local +codegraph install --command /abs/path/to/codegraph # write a fork/wrapper path into MCP configs codegraph install --print-config codex # print snippet, no file writes ``` @@ -31,10 +32,13 @@ codegraph install --print-config codex # print snippet, no file wr |---|---|---| | `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,…`) | prompt | | `--location` | `global`, `local` | prompt | +| `--command ` | custom MCP command to write (fork, wrapper, absolute path) | `codegraph` | | `--yes` | (boolean) | prompt every step | | `--no-permissions` | (boolean) skip Claude auto-allow list | permissions on | | `--print-config ` | dump snippet for one agent and exit | — | +Use `--command` when you want agents to launch a fork checkout or wrapper script instead of the default `codegraph` binary on `PATH`. + ## 2. Restart your agent Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load. diff --git a/site/src/content/docs/reference/cli.md b/site/src/content/docs/reference/cli.md index 76f0e5a31..5704aae57 100644 --- a/site/src/content/docs/reference/cli.md +++ b/site/src/content/docs/reference/cli.md @@ -6,6 +6,7 @@ description: Every CodeGraph command and the flags it accepts. ```bash codegraph # Run interactive installer codegraph install # Run installer (explicit) +codegraph install --command /abs/path/to/codegraph codegraph uninstall # Remove CodeGraph from your agents (inverse of install) codegraph init [path] # Initialize in a project (--index to also index) codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt) @@ -24,6 +25,10 @@ codegraph serve --mcp # Start MCP server ## Query commands +## install + +Use `codegraph install --command /abs/path/to/codegraph` when you want agent configs to launch a fork checkout, wrapper script, or other non-default executable instead of the bare `codegraph` command on `PATH`. + `query`, `callers`, `callees`, and `impact` all accept `--json` for machine-readable output. ```bash diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 3c3a082ff..f64ba0f12 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -29,6 +29,7 @@ import { getCodeGraphDir, isInitialized } from '../directory'; import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree'; import { createShimmerProgress } from '../ui/shimmer-progress'; import { getGlyphs } from '../ui/glyphs'; +import { CODEGRAPH_INSTALL_COMMAND_ENV } from '../installer/targets/shared'; import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from './node-version-check'; import { relaunchWithWasmRuntimeFlagsIfNeeded } from '../extraction/wasm-runtime-flags'; @@ -1626,16 +1627,19 @@ program .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)') .option('-t, --target ', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt') .option('-l, --location ', 'Install location: "global" or "local". Default: prompt') + .option('-c, --command ', 'Custom MCP command to write into agent configs (for a fork, wrapper, or absolute path)') .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on') .option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)') .option('--print-config ', 'Print MCP config snippet for the named agent and exit (no file writes)') .action(async (opts: { target?: string; location?: string; + command?: string; yes?: boolean; permissions?: boolean; printConfig?: string; }) => { + const customCommand = opts.command?.trim(); if (opts.printConfig) { const { getTarget, listTargetIds } = await import('../installer/targets/registry'); const target = getTarget(opts.printConfig); @@ -1645,7 +1649,21 @@ program process.exit(1); } const loc = (opts.location === 'local' ? 'local' : 'global') as 'global' | 'local'; - process.stdout.write(target.printConfig(loc)); + const previousInstallCommand = process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + if (customCommand) { + process.env[CODEGRAPH_INSTALL_COMMAND_ENV] = customCommand; + } else { + delete process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + } + try { + process.stdout.write(target.printConfig(loc)); + } finally { + if (previousInstallCommand === undefined) { + delete process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + } else { + process.env[CODEGRAPH_INSTALL_COMMAND_ENV] = previousInstallCommand; + } + } return; } @@ -1672,6 +1690,7 @@ program target: opts.target, location: opts.location as 'global' | 'local' | undefined, autoAllow, + command: customCommand, yes: opts.yes, }); } catch (err) { diff --git a/src/installer/index.ts b/src/installer/index.ts index ce102aa2e..977cd4213 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -22,6 +22,7 @@ import { resolveTargetFlag, } from './targets/registry'; import type { AgentTarget, Location, TargetId, WriteResult } from './targets/types'; +import { CODEGRAPH_INSTALL_COMMAND_ENV } from './targets/shared'; import { getGlyphs } from '../ui/glyphs'; // Import the lightweight submodules directly (not the ../sync barrel, which // re-exports FileWatcher and would transitively pull in ../extraction — the @@ -74,6 +75,8 @@ export interface RunInstallerOptions { * autoAllow=true, target=auto. For scripting / CI. */ yes?: boolean; + /** Custom executable or launcher to write as the MCP command. */ + command?: string; } /** @@ -87,6 +90,15 @@ export async function runInstaller(): Promise { export async function runInstallerWithOptions(opts: RunInstallerOptions): Promise { const clack = await importESM('@clack/prompts'); + const previousInstallCommand = process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + const customCommand = opts.command?.trim(); + if (customCommand) { + process.env[CODEGRAPH_INSTALL_COMMAND_ENV] = customCommand; + } else { + delete process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + } + + try { clack.intro(`CodeGraph v${getVersion()}`); @@ -106,7 +118,7 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis // Step 2: install the codegraph npm package on PATH (always offered; // matches existing behavior). Skipped when --yes (assume present). - if (!useDefaults) { + if (!useDefaults && !customCommand) { const shouldInstallGlobally = await clack.confirm({ message: 'Install the codegraph CLI on your PATH? (Required so agents can launch the MCP server)', initialValue: true, @@ -128,6 +140,8 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis } else { clack.log.info('Skipped CLI install — agents will not be able to launch the MCP server without it'); } + } else if (customCommand) { + clack.log.info(`Using custom MCP command: ${customCommand}`); } // Step 3: where the per-agent config files should land. @@ -215,6 +229,13 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis ? `Done! Restart your agent${targets.length > 1 ? 's' : ''} to use CodeGraph.` : 'Done!'; clack.outro(finalNote); + } finally { + if (previousInstallCommand === undefined) { + delete process.env[CODEGRAPH_INSTALL_COMMAND_ENV]; + } else { + process.env[CODEGRAPH_INSTALL_COMMAND_ENV] = previousInstallCommand; + } + } } export interface RunUninstallerOptions { diff --git a/src/installer/targets/antigravity.ts b/src/installer/targets/antigravity.ts index 9ecc4bc8c..659c11c1b 100644 --- a/src/installer/targets/antigravity.ts +++ b/src/installer/targets/antigravity.ts @@ -65,6 +65,7 @@ import { WriteResult, } from './types'; import { + getInstallCommand, jsonDeepEqual, readJsonFile, writeJsonFile, @@ -118,6 +119,8 @@ function preferredMcpConfigPath(): string { * nvm-managed tools like ours. */ function resolveCodegraphCommand(): string { + const configured = getInstallCommand(); + if (configured !== 'codegraph') return configured; if (process.platform !== 'darwin') return 'codegraph'; try { const resolved = execSync('command -v codegraph || which codegraph', { diff --git a/src/installer/targets/shared.ts b/src/installer/targets/shared.ts index 6d54ab570..e3aedd839 100644 --- a/src/installer/targets/shared.ts +++ b/src/installer/targets/shared.ts @@ -11,6 +11,19 @@ import * as fs from 'fs'; import * as path from 'path'; +export const CODEGRAPH_INSTALL_COMMAND_ENV = 'CODEGRAPH_INSTALL_COMMAND'; + +/** + * Resolve the MCP command the installer should write into agent configs. + * + * Default is the bare `codegraph` binary on PATH. Power users can override + * it for a local fork checkout, wrapper script, or custom launcher. + */ +export function getInstallCommand(): string { + const configured = process.env[CODEGRAPH_INSTALL_COMMAND_ENV]?.trim(); + return configured && configured.length > 0 ? configured : 'codegraph'; +} + /** * The MCP-server config block codegraph injects. Same shape across * all JSON-shaped agent configs (Claude, Cursor, opencode), only the @@ -19,7 +32,7 @@ import * as path from 'path'; export function getMcpServerConfig(): { type: string; command: string; args: string[] } { return { type: 'stdio', - command: 'codegraph', + command: getInstallCommand(), args: ['serve', '--mcp'], }; }