antigravity.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /**
  2. * Google Antigravity IDE target. Antigravity is Google's VS Code-derived
  3. * multi-agent IDE; the Gemini CLI is in the process of consolidating with
  4. * it under a single agent platform. Antigravity reads MCP server
  5. * definitions from a separate config file from the CLI.
  6. *
  7. * ## Config path: unified vs legacy
  8. *
  9. * Antigravity recently migrated to a **unified** MCP config path shared
  10. * across all Antigravity tools:
  11. *
  12. * - **Unified** (post-migration, current): `~/.gemini/config/mcp_config.json`
  13. * — signalled by the `~/.gemini/config/.migrated` marker file.
  14. * - **Legacy** (pre-migration): `~/.gemini/antigravity/mcp_config.json`
  15. * — what the github-mcp-server install guide still documents.
  16. *
  17. * We detect the marker at install time and write to the right path. On
  18. * uninstall we sweep BOTH — so a user who installed on the legacy path,
  19. * was then auto-migrated by Antigravity, and re-ran `codegraph install`
  20. * doesn't end up with stale codegraph entries in two files.
  21. *
  22. * ## Entry shape: no `type: stdio` field
  23. *
  24. * Antigravity rejects MCP entries that carry the `type: "stdio"` field
  25. * the rest of our targets use — the working entries it manages itself
  26. * (e.g. `code-review-graph`) omit it, and dropping it was load-bearing
  27. * to get codegraph to appear in the Customizations UI. We build the
  28. * entry locally instead of routing through `getMcpServerConfig()`.
  29. *
  30. * ## macOS GUI app PATH resolution
  31. *
  32. * Antigravity is a GUI Electron app. macOS gives Dock/Finder-launched
  33. * apps a stripped PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) — nvm-managed
  34. * tools live outside that, so a bare `codegraph` command fails to spawn
  35. * even when `which codegraph` resolves in the user's shell. We resolve
  36. * `codegraph` to its absolute path on macOS at install time. (Linux GUI
  37. * apps inherit user PATH; Windows uses `PATH` env directly — both are
  38. * fine with the bare command.)
  39. *
  40. * ## Shared instructions (no GEMINI.md from here)
  41. *
  42. * The IDE shares `~/.gemini/GEMINI.md` with Gemini CLI for instructions
  43. * — written by the `./gemini.ts` target. We deliberately don't touch it
  44. * here so uninstalling Antigravity without uninstalling Gemini CLI
  45. * leaves CLI instructions intact. Users who install only Antigravity
  46. * still get a working MCP integration; the prefer-codegraph-over-grep
  47. * guidance just won't be present unless they also install the gemini
  48. * target.
  49. *
  50. * ## Location
  51. *
  52. * `supportsLocation('local')` returns false — Antigravity has no
  53. * project-scoped config concept as of 2026-05.
  54. */
  55. import * as fs from 'fs';
  56. import * as path from 'path';
  57. import * as os from 'os';
  58. import { execSync } from 'child_process';
  59. import {
  60. AgentTarget,
  61. DetectionResult,
  62. InstallOptions,
  63. Location,
  64. WriteResult,
  65. } from './types';
  66. import {
  67. jsonDeepEqual,
  68. readJsonFile,
  69. writeJsonFile,
  70. } from './shared';
  71. function unifiedConfigDir(): string {
  72. return path.join(os.homedir(), '.gemini', 'config');
  73. }
  74. function unifiedMcpConfigPath(): string {
  75. return path.join(unifiedConfigDir(), 'mcp_config.json');
  76. }
  77. function legacyConfigDir(): string {
  78. return path.join(os.homedir(), '.gemini', 'antigravity');
  79. }
  80. function legacyMcpConfigPath(): string {
  81. return path.join(legacyConfigDir(), 'mcp_config.json');
  82. }
  83. function migratedMarkerPath(): string {
  84. return path.join(unifiedConfigDir(), '.migrated');
  85. }
  86. /**
  87. * Pick the right MCP config path to write to.
  88. *
  89. * Prefers the unified `~/.gemini/config/mcp_config.json` when Antigravity
  90. * has signalled it's migrated (`.migrated` marker present, OR the
  91. * unified file already exists — Antigravity creates it on first
  92. * launch post-migration). Falls back to the legacy
  93. * `~/.gemini/antigravity/mcp_config.json` for users on a pre-migration
  94. * Antigravity build.
  95. */
  96. function preferredMcpConfigPath(): string {
  97. if (fs.existsSync(migratedMarkerPath())) return unifiedMcpConfigPath();
  98. if (fs.existsSync(unifiedMcpConfigPath())) return unifiedMcpConfigPath();
  99. return legacyMcpConfigPath();
  100. }
  101. /**
  102. * Resolve the on-disk path of the `codegraph` binary so a Mac GUI app
  103. * launched from Dock/Finder (with a stripped PATH) can find it. Falls
  104. * back to the bare `codegraph` name when:
  105. *
  106. * - we're not on macOS (Linux GUI apps inherit user PATH; Windows
  107. * uses env PATH directly), OR
  108. * - the lookup fails for any reason (preserving install in restricted
  109. * environments where `which`/`command -v` aren't available).
  110. *
  111. * Resolution prefers `command -v` (built-in, no PATH manipulation),
  112. * with `which` as a fallback. Both are read via the user's interactive
  113. * shell PATH at install time — that's the right PATH for finding
  114. * nvm-managed tools like ours.
  115. */
  116. function resolveCodegraphCommand(): string {
  117. if (process.platform !== 'darwin') return 'codegraph';
  118. try {
  119. const resolved = execSync('command -v codegraph || which codegraph', {
  120. encoding: 'utf-8',
  121. stdio: ['ignore', 'pipe', 'ignore'],
  122. shell: '/bin/bash',
  123. windowsHide: true,
  124. }).trim();
  125. if (resolved && fs.existsSync(resolved)) return resolved;
  126. } catch {
  127. /* fall through to bare name */
  128. }
  129. return 'codegraph';
  130. }
  131. /**
  132. * Build the codegraph MCP-server entry for Antigravity. Distinct from
  133. * `getMcpServerConfig()` because Antigravity (a) rejects the `type`
  134. * field and (b) needs an absolute command path on macOS — see file
  135. * header.
  136. */
  137. function buildAntigravityEntry(): { command: string; args: string[] } {
  138. return {
  139. command: resolveCodegraphCommand(),
  140. args: ['serve', '--mcp'],
  141. };
  142. }
  143. class AntigravityTarget implements AgentTarget {
  144. readonly id = 'antigravity' as const;
  145. readonly displayName = 'Antigravity IDE';
  146. readonly docsUrl = 'https://antigravity.google';
  147. supportsLocation(loc: Location): boolean {
  148. return loc === 'global';
  149. }
  150. detect(loc: Location): DetectionResult {
  151. if (loc !== 'global') {
  152. return { installed: false, alreadyConfigured: false };
  153. }
  154. const file = preferredMcpConfigPath();
  155. const config = readJsonFile(file);
  156. const alreadyConfigured = !!config.mcpServers?.codegraph;
  157. // "Installed" heuristic: either the unified config dir, the legacy
  158. // config dir, or one of the config files exists. Antigravity creates
  159. // ~/.gemini/ on first launch even before MCP configs.
  160. const installed =
  161. fs.existsSync(unifiedConfigDir()) ||
  162. fs.existsSync(legacyConfigDir()) ||
  163. fs.existsSync(file);
  164. return { installed, alreadyConfigured, configPath: file };
  165. }
  166. install(loc: Location, _opts: InstallOptions): WriteResult {
  167. if (loc !== 'global') {
  168. return {
  169. files: [],
  170. notes: ['Antigravity IDE has no project-local config — re-run with --location=global.'],
  171. };
  172. }
  173. const files: WriteResult['files'] = [];
  174. files.push(writeMcpEntry());
  175. // If the user originally installed on the legacy path and Antigravity
  176. // has since migrated, strip the stale legacy entry so they don't
  177. // wind up with two competing codegraph configs.
  178. const legacyCleanup = cleanupLegacyEntry();
  179. if (legacyCleanup) files.push(legacyCleanup);
  180. return {
  181. files,
  182. notes: ['Restart Antigravity for MCP changes to take effect.'],
  183. };
  184. }
  185. uninstall(loc: Location): WriteResult {
  186. if (loc !== 'global') return { files: [] };
  187. const files: WriteResult['files'] = [];
  188. // Remove from the preferred path.
  189. const preferred = preferredMcpConfigPath();
  190. files.push(removeCodegraphFromFile(preferred));
  191. // Also sweep the OTHER path (legacy when preferred is unified, and
  192. // vice versa) — handles the migration-half-state case where codegraph
  193. // got written to one file but Antigravity now reads from the other.
  194. const other = preferred === unifiedMcpConfigPath()
  195. ? legacyMcpConfigPath()
  196. : unifiedMcpConfigPath();
  197. if (preferred !== other) {
  198. const otherResult = removeCodegraphFromFile(other);
  199. // Only surface the secondary file if we actually touched it —
  200. // a `not-found` on a file the user never had is noise.
  201. if (otherResult.action === 'removed') files.push(otherResult);
  202. }
  203. return { files };
  204. }
  205. printConfig(loc: Location): string {
  206. if (loc !== 'global') {
  207. return '# Antigravity IDE has no project-local config — use --location=global.\n';
  208. }
  209. const file = preferredMcpConfigPath();
  210. const snippet = JSON.stringify({ mcpServers: { codegraph: buildAntigravityEntry() } }, null, 2);
  211. return `# Add to ${file}\n\n${snippet}\n`;
  212. }
  213. describePaths(loc: Location): string[] {
  214. if (loc !== 'global') return [];
  215. return [preferredMcpConfigPath()];
  216. }
  217. }
  218. function writeMcpEntry(): WriteResult['files'][number] {
  219. const file = preferredMcpConfigPath();
  220. const dir = path.dirname(file);
  221. if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  222. const existing = readJsonFile(file);
  223. const before = existing.mcpServers?.codegraph;
  224. const after = buildAntigravityEntry();
  225. if (jsonDeepEqual(before, after)) {
  226. return { path: file, action: 'unchanged' };
  227. }
  228. const action: 'created' | 'updated' =
  229. before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created');
  230. if (!existing.mcpServers) existing.mcpServers = {};
  231. existing.mcpServers.codegraph = after;
  232. writeJsonFile(file, existing);
  233. return { path: file, action };
  234. }
  235. /**
  236. * Strip the codegraph entry from the legacy `~/.gemini/antigravity/mcp_config.json`
  237. * if it's present AND we're writing to the unified path. Used by install
  238. * to migrate users who had codegraph configured on the legacy path
  239. * before Antigravity migrated their config. Returns the file action for
  240. * reporting, or `null` when there's nothing to clean up.
  241. */
  242. function cleanupLegacyEntry(): WriteResult['files'][number] | null {
  243. if (preferredMcpConfigPath() !== unifiedMcpConfigPath()) return null;
  244. const legacy = legacyMcpConfigPath();
  245. if (!fs.existsSync(legacy)) return null;
  246. const config = readJsonFile(legacy);
  247. if (!config.mcpServers?.codegraph) return null;
  248. delete config.mcpServers.codegraph;
  249. if (Object.keys(config.mcpServers).length === 0) {
  250. delete config.mcpServers;
  251. }
  252. writeJsonFile(legacy, config);
  253. return { path: legacy, action: 'removed' };
  254. }
  255. function removeCodegraphFromFile(file: string): WriteResult['files'][number] {
  256. if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
  257. const config = readJsonFile(file);
  258. if (!config.mcpServers?.codegraph) return { path: file, action: 'not-found' };
  259. delete config.mcpServers.codegraph;
  260. if (Object.keys(config.mcpServers).length === 0) {
  261. delete config.mcpServers;
  262. }
  263. // Leave a now-empty `{}` in place — Antigravity manages this file and
  264. // a stray empty file is less surprising than a deletion.
  265. writeJsonFile(file, config);
  266. return { path: file, action: 'removed' };
  267. }
  268. export const antigravityTarget: AgentTarget = new AntigravityTarget();