feat(cli): Antigravity run mode

Adds Antigravity as a sixth CLI backend alongside Claude Code, shell, OpenCode,
Codex and Gemini, following the existing pluggable-resolver pattern.

- `utils/antigravity-cli-resolver.ts` resolves the CLI, mirroring the other
  resolvers; `GET /api/antigravity/status` reports availability and path.
- `ANTIGRAVITY_*` joins the `ALLOWED_ENV_PREFIXES` allowlist in schemas.ts, so
  env overrides stay CLI-scoped rather than blanket-forwarded.
- Session, tmux-manager, mux-interface and types carry the new mode; secrets are
  injected via socket-scoped `tmux setenv`, never on the spawn command line, so
  the mode requires tmux with no direct PTY fallback like the other external CLIs.
- Frontend: Run-dropdown entry, agent-type option, `ag` tab badge and toolbar
  colours. `runAntigravity()` routes remote/docker cases through
  `POST /api/quick-start` and skips the local status probe for them.

Tests: test/antigravity-mode.test.ts, plus run-mode-ui and system-routes coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-04 12:59:40 +02:00
parent 1113d34ca8
commit 26cbbe0dcb
24 changed files with 638 additions and 63 deletions
+34
View File
@@ -81,12 +81,18 @@ vi.mock('../../src/utils/gemini-cli-resolver.js', () => ({
resolveGeminiDir: vi.fn(() => null),
}));
vi.mock('../../src/utils/antigravity-cli-resolver.js', () => ({
isAntigravityAvailable: vi.fn(() => false),
resolveAntigravityDir: vi.fn(() => null),
}));
import fs from 'node:fs/promises';
import { existsSync, readdirSync } from 'node:fs';
import { subagentWatcher } from '../../src/subagent-watcher.js';
import { getLifecycleLog } from '../../src/session-lifecycle-log.js';
import { isOpenCodeAvailable, resolveOpenCodeDir } from '../../src/utils/opencode-cli-resolver.js';
import { isGeminiAvailable, resolveGeminiDir } from '../../src/utils/gemini-cli-resolver.js';
import { isAntigravityAvailable, resolveAntigravityDir } from '../../src/utils/antigravity-cli-resolver.js';
const mockedReadFile = vi.mocked(fs.readFile);
const mockedWriteFile = vi.mocked(fs.writeFile);
@@ -98,6 +104,8 @@ const mockedIsOpenCodeAvailable = vi.mocked(isOpenCodeAvailable);
const mockedResolveOpenCodeDir = vi.mocked(resolveOpenCodeDir);
const mockedIsGeminiAvailable = vi.mocked(isGeminiAvailable);
const mockedResolveGeminiDir = vi.mocked(resolveGeminiDir);
const mockedIsAntigravityAvailable = vi.mocked(isAntigravityAvailable);
const mockedResolveAntigravityDir = vi.mocked(resolveAntigravityDir);
describe('system-routes', () => {
let harness: RouteTestHarness;
@@ -805,6 +813,32 @@ describe('system-routes', () => {
});
});
// ========== GET /api/antigravity/status ==========
describe('GET /api/antigravity/status', () => {
it('returns unavailable when agy is not installed', async () => {
mockedIsAntigravityAvailable.mockReturnValue(false);
mockedResolveAntigravityDir.mockReturnValue(null);
const res = await harness.app.inject({ method: 'GET', url: '/api/antigravity/status' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.available).toBe(false);
expect(body.path).toBeNull();
});
it('returns available with path when agy is installed', async () => {
mockedIsAntigravityAvailable.mockReturnValue(true);
mockedResolveAntigravityDir.mockReturnValue('/home/user/.local/bin');
const res = await harness.app.inject({ method: 'GET', url: '/api/antigravity/status' });
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.available).toBe(true);
expect(body.path).toBe('/home/user/.local/bin');
});
});
// ========== GET /api/execution/model-config ==========
describe('GET /api/execution/model-config', () => {