From 4b16a85d30ddfe3bb769cfb159d36e0c0099bba8 Mon Sep 17 00:00:00 2001 From: pengfei Date: Thu, 21 May 2026 16:42:32 +0800 Subject: [PATCH] feat: support antigravity target in installer --- README.md | 11 +- __tests__/installer-targets.test.ts | 42 +++++- package-lock.json | 1 - src/bin/codegraph.ts | 2 +- src/installer/targets/antigravity.ts | 210 +++++++++++++++++++++++++++ src/installer/targets/registry.ts | 2 + src/installer/targets/types.ts | 2 +- 7 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 src/installer/targets/antigravity.ts diff --git a/README.md b/README.md index faf357bc..5d43d949 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # CodeGraph -### Supercharge Claude Code, Cursor, Codex, OpenCode, and Hermes Agent with Semantic Code Intelligence +### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, and Antigravity with Semantic Code Intelligence **~35% cheaper · ~70% fewer tool calls · 100% local** @@ -19,6 +19,7 @@ [![Codex CLI](https://img.shields.io/badge/Codex_CLI-supported-blueviolet.svg)](#supported-agents) [![opencode](https://img.shields.io/badge/opencode-supported-blueviolet.svg)](#supported-agents) [![Hermes Agent](https://img.shields.io/badge/Hermes_Agent-supported-blueviolet.svg)](#supported-agents) +[![Antigravity](https://img.shields.io/badge/Antigravity-supported-blueviolet.svg)](#supported-agents) @@ -41,7 +42,7 @@ npx @colbymchenry/codegraph # zero-install, or: npm i -g @colbymchenry/codegraph ``` -CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent. +CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, and Antigravity. ### Initialize Projects @@ -171,7 +172,7 @@ npx @colbymchenry/codegraph ``` The installer will: -- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent** +- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Antigravity** - Prompt to install `codegraph` on your PATH (so agents can launch the MCP server) - Ask whether configs apply to all your projects or just this one - Write each chosen agent's MCP server config + an instructions file (e.g. `CLAUDE.md`, `.cursor/rules/codegraph.mdc`, `~/.codex/AGENTS.md`) @@ -197,7 +198,7 @@ codegraph install --print-config codex # print snippet, no file wr ### 2. Restart Your Agent -Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent) for the MCP server to load. +Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Antigravity) for the MCP server to load. ### 3. Initialize Projects @@ -534,7 +535,7 @@ MIT
-**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, and Hermes Agent** +**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, and Antigravity** [Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 59e869e2..34cda9c4 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -494,7 +494,6 @@ describe('Installer targets — partial-state idempotency', () => { expect(legacy.mcpServers.codegraph).toBeUndefined(); expect(legacy.mcpServers.other).toBeDefined(); }); - // ---- Legacy auto-sync hook cleanup ---- // Pre-0.8 installs wrote `codegraph mark-dirty` / `sync-if-dirty` // hooks to settings.json. Both subcommands were removed from the CLI, @@ -608,6 +607,46 @@ describe('Installer targets — partial-state idempotency', () => { // Both events emptied → the whole `hooks` object is removed. expect(after.hooks).toBeUndefined(); }); + + it('antigravity: install writes mcp_config.json with correct format', () => { + const target = getTarget('antigravity')!; + target.install('global', { autoAllow: true }); + + const configPath = path.join(tmpHome, '.gemini', 'config', 'mcp_config.json'); + expect(fs.existsSync(configPath)).toBe(true); + + const content = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + expect(content.mcpServers).toBeDefined(); + expect(content.mcpServers.codegraph).toBeDefined(); + expect(content.mcpServers.codegraph.command).toBe('codegraph'); + expect(content.mcpServers.codegraph.args).toEqual(['serve', '--mcp']); + expect(content.mcpServers.codegraph.env).toEqual({}); + expect(content.mcpServers.codegraph.type).toBeUndefined(); + + const agentsMd = path.join(tmpHome, '.gemini', 'GEMINI.md'); + expect(fs.existsSync(agentsMd)).toBe(true); + }); + + it('antigravity: uninstall removes only mcpServers.codegraph, preserving siblings', () => { + const target = getTarget('antigravity')!; + const configPath = path.join(tmpHome, '.gemini', 'config', 'mcp_config.json'); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + + const initialConfig = { + mcpServers: { + other: { command: 'other-command' } + } + }; + fs.writeFileSync(configPath, JSON.stringify(initialConfig, null, 2) + '\n'); + + target.install('global', { autoAllow: true }); + target.uninstall('global'); + + const content = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + expect(content.mcpServers).toBeDefined(); + expect(content.mcpServers.other).toEqual({ command: 'other-command' }); + expect(content.mcpServers.codegraph).toBeUndefined(); + }); }); describe('Installer targets — registry', () => { @@ -617,6 +656,7 @@ describe('Installer targets — registry', () => { expect(getTarget('codex')?.id).toBe('codex'); expect(getTarget('opencode')?.id).toBe('opencode'); expect(getTarget('hermes')?.id).toBe('hermes'); + expect(getTarget('antigravity')?.id).toBe('antigravity'); expect(getTarget('not-a-real-target')).toBeUndefined(); }); diff --git a/package-lock.json b/package-lock.json index 36c592b1..ff5f07e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1431,7 +1431,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 6bc63b3f..9e325b26 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1611,7 +1611,7 @@ program */ program .command('install') - .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)') + .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, antigravity)') .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('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on') diff --git a/src/installer/targets/antigravity.ts b/src/installer/targets/antigravity.ts new file mode 100644 index 00000000..800450ea --- /dev/null +++ b/src/installer/targets/antigravity.ts @@ -0,0 +1,210 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + AgentTarget, + DetectionResult, + InstallOptions, + Location, + WriteResult, +} from './types'; +import { + jsonDeepEqual, + readJsonFile, + removeMarkedSection, + replaceOrAppendMarkedSection, + writeJsonFile, +} from './shared'; +import { + CODEGRAPH_SECTION_END, + CODEGRAPH_SECTION_START, + INSTRUCTIONS_TEMPLATE, +} from '../instructions-template'; + +/** + * Returns the path to the antigravity configuration directory. + * Under ~/.gemini/config + */ +function configDir(): string { + return path.join(os.homedir(), '.gemini', 'config'); +} + +/** + * Returns the path to the antigravity MCP configuration JSON file. + * Under ~/.gemini/config/mcp_config.json + */ +function mcpConfigPath(): string { + return path.join(configDir(), 'mcp_config.json'); +} + +/** + * Returns the path to the GEMINI.md instruction file for antigravity. + * Under ~/.gemini/GEMINI.md + */ +function instructionsPath(): string { + return path.join(os.homedir(), '.gemini', 'GEMINI.md'); +} + +/** + * Builds the canonical MCP configuration object for antigravity. + * Excludes the "type" field and includes "env: {}" as requested. + */ +function buildAntigravityMcpConfig(): { command: string; args: string[]; env: Record } { + return { + command: 'codegraph', + args: ['serve', '--mcp'], + env: {}, + }; +} + +/** + * Writes the antigravity MCP entry into ~/.gemini/config/mcp_config.json. + * Preserves sibling servers and creates directory/file if they do not exist. + */ +function writeMcpEntry(): WriteResult['files'][number] { + const file = mcpConfigPath(); + const dir = path.dirname(file); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const existing = readJsonFile(file); + const before = existing.mcpServers?.codegraph; + const after = buildAntigravityMcpConfig(); + + if (jsonDeepEqual(before, after)) { + return { path: file, action: 'unchanged' }; + } + + const action: 'created' | 'updated' = before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created'); + if (!existing.mcpServers) existing.mcpServers = {}; + existing.mcpServers.codegraph = after; + writeJsonFile(file, existing); + return { path: file, action }; +} + +/** + * Writes the markdown agent instructions into ~/.gemini/config/AGENTS.md. + * Appends or replaces the section with the CodeGraph markers. + */ +function writeInstructionsEntry(): WriteResult['files'][number] { + const file = instructionsPath(); + const dir = path.dirname(file); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const action = replaceOrAppendMarkedSection( + file, + INSTRUCTIONS_TEMPLATE, + CODEGRAPH_SECTION_START, + CODEGRAPH_SECTION_END, + ); + const mapped: 'created' | 'updated' | 'unchanged' = + action === 'created' ? 'created' + : action === 'unchanged' ? 'unchanged' + : 'updated'; + return { path: file, action: mapped }; +} + +class AntigravityTarget implements AgentTarget { + readonly id = 'antigravity' as const; + readonly displayName = 'antigravity'; + readonly docsUrl = 'https://antigravity.google/download'; + + /** + * Returns true if the location is supported. + * antigravity target only supports global installation. + */ + supportsLocation(loc: Location): boolean { + return loc === 'global'; + } + + /** + * Detects whether antigravity target has been installed and configured. + */ + detect(loc: Location): DetectionResult { + if (loc !== 'global') { + return { installed: false, alreadyConfigured: false }; + } + const mcpPath = mcpConfigPath(); + const config = readJsonFile(mcpPath); + const alreadyConfigured = !!config.mcpServers?.codegraph; + const installed = fs.existsSync(path.join(os.homedir(), '.gemini')) || fs.existsSync(mcpPath); + return { installed, alreadyConfigured, configPath: mcpPath }; + } + + /** + * Installs codegraph MCP configurations and instructions for antigravity. + */ + install(loc: Location, _opts: InstallOptions): WriteResult { + if (loc !== 'global') { + return { + files: [], + notes: ['antigravity has no project-local config — re-run with --location=global to install.'], + }; + } + const files: WriteResult['files'] = []; + + files.push(writeMcpEntry()); + files.push(writeInstructionsEntry()); + + return { + files, + notes: ['Restart antigravity for MCP changes to take effect.'], + }; + } + + /** + * Uninstalls codegraph configurations and instructions for antigravity. + */ + uninstall(loc: Location): WriteResult { + if (loc !== 'global') return { files: [] }; + const files: WriteResult['files'] = []; + + const mcpPath = mcpConfigPath(); + const config = readJsonFile(mcpPath); + if (config.mcpServers?.codegraph) { + delete config.mcpServers.codegraph; + if (Object.keys(config.mcpServers).length === 0) { + delete config.mcpServers; + } + writeJsonFile(mcpPath, config); + files.push({ path: mcpPath, action: 'removed' }); + } else { + files.push({ path: mcpPath, action: 'not-found' }); + } + + const instr = instructionsPath(); + const action = removeMarkedSection(instr, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END); + files.push({ path: instr, action }); + + return { files }; + } + + /** + * Prints the configuration snippet for manual pasting. + */ + printConfig(loc: Location): string { + if (loc !== 'global') { + return '# antigravity has no project-local config — use --location=global.\n'; + } + const target = mcpConfigPath(); + const snippet = JSON.stringify( + { + mcpServers: { + codegraph: buildAntigravityMcpConfig(), + }, + }, + null, + 2 + ); + return `# Add to ${target}\n\n${snippet}\n`; + } + + /** + * Returns list of paths created/modified by this target. + */ + describePaths(loc: Location): string[] { + if (loc !== 'global') return []; + return [mcpConfigPath(), instructionsPath()]; + } +} + +export const antigravityTarget: AgentTarget = new AntigravityTarget(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 0091ab64..102ecd54 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -13,6 +13,7 @@ import { cursorTarget } from './cursor'; import { codexTarget } from './codex'; import { opencodeTarget } from './opencode'; import { hermesTarget } from './hermes'; +import { antigravityTarget } from './antigravity'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, @@ -20,6 +21,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ codexTarget, opencodeTarget, hermesTarget, + antigravityTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 290f13ce..158a1e8d 100644 --- a/src/installer/targets/types.ts +++ b/src/installer/targets/types.ts @@ -19,7 +19,7 @@ export type Location = 'global' | 'local'; * lookup. New targets add a value here when they're added to the * registry. Keep these short and lowercase. */ -export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes'; +export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'antigravity'; /** * Result of `target.detect(location)`.