| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- /**
- * Google Antigravity IDE target. Antigravity is Google's VS Code-derived
- * multi-agent IDE; the Gemini CLI is in the process of consolidating with
- * it under a single agent platform. Antigravity reads MCP server
- * definitions from a separate config file from the CLI.
- *
- * ## Config path: unified vs legacy
- *
- * Antigravity recently migrated to a **unified** MCP config path shared
- * across all Antigravity tools:
- *
- * - **Unified** (post-migration, current): `~/.gemini/config/mcp_config.json`
- * — signalled by the `~/.gemini/config/.migrated` marker file.
- * - **Legacy** (pre-migration): `~/.gemini/antigravity/mcp_config.json`
- * — what the github-mcp-server install guide still documents.
- *
- * We detect the marker at install time and write to the right path. On
- * uninstall we sweep BOTH — so a user who installed on the legacy path,
- * was then auto-migrated by Antigravity, and re-ran `codegraph install`
- * doesn't end up with stale codegraph entries in two files.
- *
- * ## Entry shape: no `type: stdio` field
- *
- * Antigravity rejects MCP entries that carry the `type: "stdio"` field
- * the rest of our targets use — the working entries it manages itself
- * (e.g. `code-review-graph`) omit it, and dropping it was load-bearing
- * to get codegraph to appear in the Customizations UI. We build the
- * entry locally instead of routing through `getMcpServerConfig()`.
- *
- * ## macOS GUI app PATH resolution
- *
- * Antigravity is a GUI Electron app. macOS gives Dock/Finder-launched
- * apps a stripped PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) — nvm-managed
- * tools live outside that, so a bare `codegraph` command fails to spawn
- * even when `which codegraph` resolves in the user's shell. We resolve
- * `codegraph` to its absolute path on macOS at install time. (Linux GUI
- * apps inherit user PATH; Windows uses `PATH` env directly — both are
- * fine with the bare command.)
- *
- * ## Shared instructions (no GEMINI.md from here)
- *
- * The IDE shares `~/.gemini/GEMINI.md` with Gemini CLI for instructions
- * — written by the `./gemini.ts` target. We deliberately don't touch it
- * here so uninstalling Antigravity without uninstalling Gemini CLI
- * leaves CLI instructions intact. Users who install only Antigravity
- * still get a working MCP integration; the prefer-codegraph-over-grep
- * guidance just won't be present unless they also install the gemini
- * target.
- *
- * ## Location
- *
- * `supportsLocation('local')` returns false — Antigravity has no
- * project-scoped config concept as of 2026-05.
- */
- import * as fs from 'fs';
- import * as path from 'path';
- import * as os from 'os';
- import { execSync } from 'child_process';
- import {
- AgentTarget,
- DetectionResult,
- InstallOptions,
- Location,
- WriteResult,
- } from './types';
- import {
- jsonDeepEqual,
- readJsonFile,
- writeJsonFile,
- } from './shared';
- function unifiedConfigDir(): string {
- return path.join(os.homedir(), '.gemini', 'config');
- }
- function unifiedMcpConfigPath(): string {
- return path.join(unifiedConfigDir(), 'mcp_config.json');
- }
- function legacyConfigDir(): string {
- return path.join(os.homedir(), '.gemini', 'antigravity');
- }
- function legacyMcpConfigPath(): string {
- return path.join(legacyConfigDir(), 'mcp_config.json');
- }
- function migratedMarkerPath(): string {
- return path.join(unifiedConfigDir(), '.migrated');
- }
- /**
- * Pick the right MCP config path to write to.
- *
- * Prefers the unified `~/.gemini/config/mcp_config.json` when Antigravity
- * has signalled it's migrated (`.migrated` marker present, OR the
- * unified file already exists — Antigravity creates it on first
- * launch post-migration). Falls back to the legacy
- * `~/.gemini/antigravity/mcp_config.json` for users on a pre-migration
- * Antigravity build.
- */
- function preferredMcpConfigPath(): string {
- if (fs.existsSync(migratedMarkerPath())) return unifiedMcpConfigPath();
- if (fs.existsSync(unifiedMcpConfigPath())) return unifiedMcpConfigPath();
- return legacyMcpConfigPath();
- }
- /**
- * Resolve the on-disk path of the `codegraph` binary so a Mac GUI app
- * launched from Dock/Finder (with a stripped PATH) can find it. Falls
- * back to the bare `codegraph` name when:
- *
- * - we're not on macOS (Linux GUI apps inherit user PATH; Windows
- * uses env PATH directly), OR
- * - the lookup fails for any reason (preserving install in restricted
- * environments where `which`/`command -v` aren't available).
- *
- * Resolution prefers `command -v` (built-in, no PATH manipulation),
- * with `which` as a fallback. Both are read via the user's interactive
- * shell PATH at install time — that's the right PATH for finding
- * nvm-managed tools like ours.
- */
- function resolveCodegraphCommand(): string {
- if (process.platform !== 'darwin') return 'codegraph';
- try {
- const resolved = execSync('command -v codegraph || which codegraph', {
- encoding: 'utf-8',
- stdio: ['ignore', 'pipe', 'ignore'],
- shell: '/bin/bash',
- windowsHide: true,
- }).trim();
- if (resolved && fs.existsSync(resolved)) return resolved;
- } catch {
- /* fall through to bare name */
- }
- return 'codegraph';
- }
- /**
- * Build the codegraph MCP-server entry for Antigravity. Distinct from
- * `getMcpServerConfig()` because Antigravity (a) rejects the `type`
- * field and (b) needs an absolute command path on macOS — see file
- * header.
- */
- function buildAntigravityEntry(): { command: string; args: string[] } {
- return {
- command: resolveCodegraphCommand(),
- args: ['serve', '--mcp'],
- };
- }
- class AntigravityTarget implements AgentTarget {
- readonly id = 'antigravity' as const;
- readonly displayName = 'Antigravity IDE';
- readonly docsUrl = 'https://antigravity.google';
- supportsLocation(loc: Location): boolean {
- return loc === 'global';
- }
- detect(loc: Location): DetectionResult {
- if (loc !== 'global') {
- return { installed: false, alreadyConfigured: false };
- }
- const file = preferredMcpConfigPath();
- const config = readJsonFile(file);
- const alreadyConfigured = !!config.mcpServers?.codegraph;
- // "Installed" heuristic: either the unified config dir, the legacy
- // config dir, or one of the config files exists. Antigravity creates
- // ~/.gemini/ on first launch even before MCP configs.
- const installed =
- fs.existsSync(unifiedConfigDir()) ||
- fs.existsSync(legacyConfigDir()) ||
- fs.existsSync(file);
- return { installed, alreadyConfigured, configPath: file };
- }
- install(loc: Location, _opts: InstallOptions): WriteResult {
- if (loc !== 'global') {
- return {
- files: [],
- notes: ['Antigravity IDE has no project-local config — re-run with --location=global.'],
- };
- }
- const files: WriteResult['files'] = [];
- files.push(writeMcpEntry());
- // If the user originally installed on the legacy path and Antigravity
- // has since migrated, strip the stale legacy entry so they don't
- // wind up with two competing codegraph configs.
- const legacyCleanup = cleanupLegacyEntry();
- if (legacyCleanup) files.push(legacyCleanup);
- return {
- files,
- notes: ['Restart Antigravity for MCP changes to take effect.'],
- };
- }
- uninstall(loc: Location): WriteResult {
- if (loc !== 'global') return { files: [] };
- const files: WriteResult['files'] = [];
- // Remove from the preferred path.
- const preferred = preferredMcpConfigPath();
- files.push(removeCodegraphFromFile(preferred));
- // Also sweep the OTHER path (legacy when preferred is unified, and
- // vice versa) — handles the migration-half-state case where codegraph
- // got written to one file but Antigravity now reads from the other.
- const other = preferred === unifiedMcpConfigPath()
- ? legacyMcpConfigPath()
- : unifiedMcpConfigPath();
- if (preferred !== other) {
- const otherResult = removeCodegraphFromFile(other);
- // Only surface the secondary file if we actually touched it —
- // a `not-found` on a file the user never had is noise.
- if (otherResult.action === 'removed') files.push(otherResult);
- }
- return { files };
- }
- printConfig(loc: Location): string {
- if (loc !== 'global') {
- return '# Antigravity IDE has no project-local config — use --location=global.\n';
- }
- const file = preferredMcpConfigPath();
- const snippet = JSON.stringify({ mcpServers: { codegraph: buildAntigravityEntry() } }, null, 2);
- return `# Add to ${file}\n\n${snippet}\n`;
- }
- describePaths(loc: Location): string[] {
- if (loc !== 'global') return [];
- return [preferredMcpConfigPath()];
- }
- }
- function writeMcpEntry(): WriteResult['files'][number] {
- const file = preferredMcpConfigPath();
- 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 = buildAntigravityEntry();
- 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 };
- }
- /**
- * Strip the codegraph entry from the legacy `~/.gemini/antigravity/mcp_config.json`
- * if it's present AND we're writing to the unified path. Used by install
- * to migrate users who had codegraph configured on the legacy path
- * before Antigravity migrated their config. Returns the file action for
- * reporting, or `null` when there's nothing to clean up.
- */
- function cleanupLegacyEntry(): WriteResult['files'][number] | null {
- if (preferredMcpConfigPath() !== unifiedMcpConfigPath()) return null;
- const legacy = legacyMcpConfigPath();
- if (!fs.existsSync(legacy)) return null;
- const config = readJsonFile(legacy);
- if (!config.mcpServers?.codegraph) return null;
- delete config.mcpServers.codegraph;
- if (Object.keys(config.mcpServers).length === 0) {
- delete config.mcpServers;
- }
- writeJsonFile(legacy, config);
- return { path: legacy, action: 'removed' };
- }
- function removeCodegraphFromFile(file: string): WriteResult['files'][number] {
- if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
- const config = readJsonFile(file);
- if (!config.mcpServers?.codegraph) return { path: file, action: 'not-found' };
- delete config.mcpServers.codegraph;
- if (Object.keys(config.mcpServers).length === 0) {
- delete config.mcpServers;
- }
- // Leave a now-empty `{}` in place — Antigravity manages this file and
- // a stray empty file is less surprising than a deletion.
- writeJsonFile(file, config);
- return { path: file, action: 'removed' };
- }
- export const antigravityTarget: AgentTarget = new AntigravityTarget();
|