Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,17 +246,21 @@ 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
```

| Flag | Values | Default |
|---|---|---|
| `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,...`) | prompt |
| `--location` | `global`, `local` | prompt |
| `--command <path>` | 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 <id>` | 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.
Expand Down
48 changes: 48 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,21 @@ describe('Installer targets — contract', () => {
let tmpCwd: string;
let origCwd: string;
let homeRestore: { restore: () => void };
let prevInstallCommand: string | undefined;

beforeEach(() => {
tmpHome = mkTmpDir('home');
tmpCwd = mkTmpDir('cwd');
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 });
Expand Down Expand Up @@ -164,16 +169,21 @@ describe('Installer targets — partial-state idempotency', () => {
let tmpCwd: string;
let origCwd: string;
let homeRestore: { restore: () => void };
let prevInstallCommand: string | undefined;

beforeEach(() => {
tmpHome = mkTmpDir('home');
tmpCwd = mkTmpDir('cwd');
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 });
Expand All @@ -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');
Expand Down
4 changes: 4 additions & 0 deletions site/src/content/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@ 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
```

| Flag | Values | Default |
|---|---|---|
| `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,…`) | prompt |
| `--location` | `global`, `local` | prompt |
| `--command <path>` | 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 <id>` | 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.
Expand Down
5 changes: 5 additions & 0 deletions site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
21 changes: 20 additions & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
.option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
.option('-c, --command <path>', '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 <id>', '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);
Expand All @@ -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;
}

Expand All @@ -1672,6 +1690,7 @@ program
target: opts.target,
location: opts.location as 'global' | 'local' | undefined,
autoAllow,
command: customCommand,
yes: opts.yes,
});
} catch (err) {
Expand Down
23 changes: 22 additions & 1 deletion src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -87,6 +90,15 @@ export async function runInstaller(): Promise<void> {

export async function runInstallerWithOptions(opts: RunInstallerOptions): Promise<void> {
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()}`);

Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions src/installer/targets/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
WriteResult,
} from './types';
import {
getInstallCommand,
jsonDeepEqual,
readJsonFile,
writeJsonFile,
Expand Down Expand Up @@ -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', {
Expand Down
15 changes: 14 additions & 1 deletion src/installer/targets/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'],
};
}
Expand Down