GitNexus/gitnexus/src/cli/index.ts
Nilotpal Kashyap 50c6acb108
feat(setup): implement antigravity integration setup and hook adapter… (#1730)
* feat(setup): implement antigravity integration setup and hook adapter for gitnexus

* docs(readme): list Antigravity in supported editors

* test(setup-antigravity): pin platform per-test to fix Windows CI failure

The MCP entry assertion expected `npx` directly, but on Windows
`getMcpEntry()` wraps it as `cmd /c npx ...`, which broke the Windows
runner. Pin platform to darwin in beforeEach so the existing assertion
is deterministic, restore the descriptor in afterEach, and add a
parity test for the win32 cmd-wrapper shape.

* fix(antigravity): align hook adapter to Gemini CLI schema + fix Windows CI

Rebase the Antigravity integration on the canonical Gemini CLI hooks
contract (https://geminicli.com/docs/hooks/reference/), which is the
documented schema Antigravity 2.0 inherits:

- Hook adapter: replace PreToolUse/PostToolUse with the single AfterTool
  event. BeforeTool has no documented context-injection channel in the
  Gemini contract, so augmentation runs in AfterTool where
  hookSpecificOutput.additionalContext is the documented way to append
  text to the tool result the agent reads. Stale-index hints land in the
  same channel (so the agent sees them) and are mirrored to stderr for
  terminal users. Tool-name matcher updated to Gemini CLI snake_case
  (search_file_content|glob|run_shell_command).
- Setup: write hooks to ~/.gemini/settings.json under canonical
  hooks.AfterTool[] (replaces the ad-hoc hooks.json top-level group).
  Polite-neighbor merge preserves existing user hooks. Also copy
  win-rm-list-json.ps1 alongside hook-db-lock-probe.cjs so the Windows
  MCP server ownership probe doesn't silently fail open.
- Tests: 17 regression tests covering MCP write, win32 shape, hook
  schema, polite-neighbor merge, idempotency, adapter context emission,
  stale-index hint, and skill layout.
- README: footnote documenting the AfterTool design choice and a link
  to the Gemini CLI hooks reference.

Windows CI fix: installSkillsTo previously used glob('*.md') +
glob('*/SKILL.md'), which returned zero matches under the Windows
runner's temp paths (8.3 short-name like RUNNER~1). Replace with
fs.readdir + dirent type checks — same behavior, no path quirks. This
fixes the only failing Windows job on the PR.

* fix(antigravity): address PR review — windowsHide, stale docs, dead code

Addresses the production-readiness review findings on PR #1730:

- F1 (blocker): add windowsHide:true to all four spawnSync sites in the
  Antigravity hook adapter (findCanonicalRepoRoot, runGitNexusCli's two
  branches, buildStaleIndexHint) so they don't flash console windows on
  Windows. Matches the fix #1794 already on main for the Claude hook.
- F2 (blocker): update gitnexus/README.md editor table to say AfterTool
  and link the Gemini CLI hooks reference. The published README had
  drifted to the pre-c1872b4 PreToolUse + PostToolUse schema.
- F3: rewrite the stale ~/.gemini block comment in setup.ts. It still
  described the old hooks.json + gitnexus group + grep_search design.
- F4: remove grep_search dead code from extractPattern and its doc
  comment. The registered matcher is search_file_content|glob|run_shell_command,
  so grep_search would never be invoked.
- F5: annotate timeout:10000 with a ms-unit comment noting Gemini CLI
  uses milliseconds (Claude Code uses seconds).
- F6: add the GITNEXUS_DEBUG branch to extractAugmentContext for parity
  with the Claude adapter, so suppressed augment stderr is recoverable.
- F7: stageAdapter test helper now copies win-rm-list-json.ps1 alongside
  the .cjs helpers, so the adapter's Windows lock-probe path isn't a
  silent fail-open in child-process smoke tests.

* test(antigravity): add integration tests and register in cross-platform matrix

Adds end-to-end coverage on top of the unit-level tests, per maintainer
request:

- test/integration/setup-antigravity.test.ts (10 tests): exercises the
  real setupCommand() against a temp HOME with ~/.gemini/antigravity/
  present. Verifies mcp_config.json shape, ~/.gemini/settings.json
  AfterTool entry, adapter + helpers + win-rm-list-json.ps1 copy,
  baked-in cliPath rewrite (issue #108 regression class), skill layout,
  polite-neighbor merge against existing user hooks, idempotency,
  skip-when-absent, corrupt-file safety, and key preservation.
- test/integration/antigravity-hook-e2e.test.ts (19 tests): runs the
  full install-then-execute flow — invokes setupCommand to lay down
  the adapter + helpers, then spawns the INSTALLED adapter as a real
  child process against a temp git repo + .gitnexus/. The source
  adapter cannot be spawned directly (it requires sibling .cjs helpers
  that only live in hooks/claude/); install-then-spawn mirrors the
  production codepath. Covers staleness detection across all five git
  mutation types, --embeddings propagation, polite skip on
  toolResponse.error / exit_code !== 0, augment crash-free behavior,
  cwd validation, corrupted/missing meta.json, unknown event names,
  empty stdin, and the no-.gitnexus deep-nested case.
- scripts/cross-platform-tests.ts: registers all three antigravity
  test files (unit in PLATFORM_LOGIC, two integration files in
  SPAWN_CLI) so Windows and macOS CI exercise them on every run.

* fix(antigravity): review fixes — dedup, silent-failure guard, type coercion, glob filter

- Delete mergeGeminiSettingsHooks (verbatim copy of mergeHooksJsonc),
  replace call site with the original
- Unify geminiHasGitnexusHook into hasGitnexusHook with commandFragment
  parameter; delete the duplicate
- Guard against silent adapter-copy failure: verify the adapter file
  exists before registering the AfterTool hook entry in settings.json;
  surface helper copy errors instead of swallowing
- Fix toolSucceeded type coercion: use Number() so string exit_code
  values from Gemini CLI are handled correctly
- Align glob tool extractPattern with Claude adapter's restrictive
  regex filter (/[*\/]([a-zA-Z][a-zA-Z0-9_-]{2,})/)
- Remove bounds-only toBeGreaterThan(0) assertion (DoD §2.7)
- Add antigravity adapter to HOOK_FILES windowsHide regression list

* chore(autofix): apply prettier + eslint fixes via /autofix command

* chore: trigger CI

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Test <test@example.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 14:46:17 +01:00

261 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Heap re-spawn removed — only analyze.ts needs the 8GB heap (via its own ensureHeap()).
// Removing it from here improves MCP server startup time significantly.
import { Command } from 'commander';
import { createRequire } from 'node:module';
import { createLazyAction } from './lazy-action.js';
import { registerGroupCommands } from './group.js';
import { localizeCliHelp } from './help-i18n.js';
import { t } from './i18n/index.js';
const _require = createRequire(import.meta.url);
const pkg = _require('../../package.json');
const program = new Command();
program.name('gitnexus').description('GitNexus local CLI and MCP server').version(pkg.version);
program
.command('setup')
.description(
'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, Codex',
)
.action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
program
.command('analyze [path]')
.description('Index a repository (full analysis)')
.option('-f, --force', 'Force full re-index even if up to date')
.option('--repair-fts', 'Repair/rebuild search FTS indexes without full re-analysis')
.option(
'--embeddings [limit]',
'Enable embedding generation for semantic search (off by default). ' +
'Optional [limit] overrides the 50,000-node safety cap; pass 0 to disable the cap entirely.',
)
.option(
'--drop-embeddings',
'Drop existing embeddings on rebuild. By default, an `analyze` without `--embeddings` ' +
'preserves any embeddings already present in the index.',
)
.option(
'--skills',
'Generate repo-specific skill files from detected communities ' +
'(no-op when --index-only is also set).',
)
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
.option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
.option(
'--skip-skills',
'Skip installing standard GitNexus skill files under .claude/skills/gitnexus/. ' +
'Does not suppress community skills from --skills (those use .claude/skills/generated/). ' +
'Use --index-only to skip all AI-context file injection.',
)
.option('--index-only', 'Pure index mode: skip all file injection (AGENTS.md, CLAUDE.md, skills)')
.option(
'--skip-git',
'Treat the provided path/cwd as the index root and skip parent git-root discovery',
)
.option(
'--name <alias>',
'Register this repo under a custom name in ~/.gitnexus/registry.json ' +
'(disambiguates repos whose paths share a basename, e.g. two different .../app folders)',
)
.option(
'--allow-duplicate-name',
'Register this repo even if another path already uses the same --name alias. ' +
'Leaves `-r <name>` ambiguous for the two paths; use -r <path> to disambiguate.',
)
.option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
.option(
'--max-file-size <kb>',
'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).',
)
.option(
'--worker-timeout <seconds>',
'Worker sub-batch idle timeout before retry/fallback. Default: 30.',
)
.option(
'--wal-checkpoint-threshold <bytes>',
'LadybugDB WAL auto-checkpoint threshold in bytes during analyze ' +
'(integer >= -1; default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB).',
)
.option(
'--workers <n>',
'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).',
)
.option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
.option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
.option('--embedding-sub-batch-size <n>', 'Number of chunks per embedding model call')
.option('--embedding-device <device>', 'Embedding device: auto, cpu, dml, cuda, or wasm')
.addHelpText('after', () => t('help.analyze.environment'))
.action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
program
.command('index [path...]')
.description(
'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
)
.option('-f, --force', 'Register even if meta.json is missing (stats will be empty)')
.option('--allow-non-git', 'Allow registering folders that are not Git repositories')
.action(createLazyAction(() => import('./index-repo.js'), 'indexCommand'));
program
.command('serve')
.description('Start local HTTP server for web UI connection')
.option('-p, --port <port>', 'Port number', '4747')
.option('--host <host>', 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)')
.action(createLazyAction(() => import('./serve.js'), 'serveCommand'));
program
.command('mcp')
.description('Start MCP server (stdio) — serves all indexed repos')
.action(createLazyAction(() => import('./mcp.js'), 'mcpCommand'));
program
.command('list')
.description('List all indexed repositories')
.action(createLazyAction(() => import('./list.js'), 'listCommand'));
program
.command('status')
.description('Show index status for current repo')
.action(createLazyAction(() => import('./status.js'), 'statusCommand'));
program
.command('doctor')
.description('Show runtime platform capabilities and embedding configuration')
.action(createLazyAction(() => import('./doctor.js'), 'doctorCommand'));
program
.command('clean')
.description('Delete GitNexus index for current repo')
.option('-f, --force', 'Skip confirmation prompt')
.option('--all', 'Clean all indexed repos')
.option('--lbug-sidecars', 'Clean quarantined LadybugDB missing-shadow WAL sidecars')
.action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
program
.command('remove <target>')
.description(
'Delete the GitNexus index for a registered repo (by alias, name, or absolute path). ' +
'Unlike `clean`, does not require being inside the repo. Idempotent on unknown targets.',
)
.option('-f, --force', 'Skip confirmation prompt')
.action(createLazyAction(() => import('./remove.js'), 'removeCommand'));
program
.command('wiki [path]')
.description('Generate repository wiki from knowledge graph')
.option('-f, --force', 'Force full regeneration even if up to date')
.option(
'--provider <provider>',
'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
)
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
.option(
'--base-url <url>',
'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
)
.option('--api-key <key>', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)')
.option(
'--api-version <version>',
'Azure api-version query param, e.g. 2024-10-21 (legacy Azure API only)',
)
.option(
'--reasoning-model',
'Mark deployment as reasoning model (o1/o3/o4-mini) — strips temperature, uses max_completion_tokens',
)
.option('--no-reasoning-model', 'Disable reasoning model mode (overrides saved config)')
.option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
.option('--timeout <seconds>', 'LLM request timeout in seconds (default: disabled)')
.option('--retries <n>', 'Max LLM retry attempts per request (default: 3)')
.option('--gist', 'Publish wiki as a public GitHub Gist after generation')
.option('-v, --verbose', 'Enable verbose output (show LLM commands and responses)')
.option('--review', 'Stop after grouping to review module structure before generating pages')
.option(
'--lang <lang>',
'Output language for generated documentation (e.g. english, chinese, spanish, japanese)',
)
.action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
program
.command('augment <pattern>')
.description('Augment a search pattern with knowledge graph context (used by hooks)')
.action(createLazyAction(() => import('./augment.js'), 'augmentCommand'));
program
.command('publish [path]')
.description(
'Notify the understand-quickly registry that this repo has a fresh GitNexus index. ' +
'Opt-in: requires UNDERSTAND_QUICKLY_TOKEN (fine-grained PAT with ' +
'`Repository dispatches: write` on looptech-ai/understand-quickly). ' +
'No-op without the token. See https://github.com/looptech-ai/understand-quickly.',
)
.option('--id <owner/repo>', 'Override the registry id (defaults to the origin remote)')
.option('--skip-git', 'Treat cwd as the repo root and skip parent git-root discovery')
.action(createLazyAction(() => import('./publish.js'), 'publishCommand'));
// ─── Direct Tool Commands (no MCP overhead) ────────────────────────
// These invoke LocalBackend directly for use in eval, scripts, and CI.
program
.command('query <search_query>')
.description('Search the knowledge graph for execution flows related to a concept')
.option('-r, --repo <name>', 'Target repository (omit if only one indexed)')
.option('-c, --context <text>', 'Task context to improve ranking')
.option('-g, --goal <text>', 'What you want to find')
.option('-l, --limit <n>', 'Max processes to return (default: 5)')
.option('--content', 'Include full symbol source code')
.action(createLazyAction(() => import('./tool.js'), 'queryCommand'));
program
.command('context [name]')
.description('360-degree view of a code symbol: callers, callees, processes')
.option('-r, --repo <name>', 'Target repository')
.option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
.option('-f, --file <path>', 'File path to disambiguate common names')
.option('--content', 'Include full symbol source code')
.action(createLazyAction(() => import('./tool.js'), 'contextCommand'));
program
.command('impact <target>')
.description('Blast radius analysis: what breaks if you change a symbol')
.option('-d, --direction <dir>', 'upstream (dependants) or downstream (dependencies)', 'upstream')
.option('-r, --repo <name>', 'Target repository')
.option('--depth <n>', 'Max relationship depth (default: 3)')
.option('--include-tests', 'Include test files in results')
.action(createLazyAction(() => import('./tool.js'), 'impactCommand'));
program
.command('cypher <query>')
.description('Execute raw Cypher query against the knowledge graph')
.option('-r, --repo <name>', 'Target repository')
.action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
program
.command('detect-changes')
.alias('detect_changes')
.description('Map git diff hunks to indexed symbols and affected execution flows')
.option('-s, --scope <scope>', 'What to analyze: unstaged, staged, all, or compare', 'unstaged')
.option('-b, --base-ref <ref>', 'Branch/commit for compare scope (e.g. main)')
.option('-r, --repo <name>', 'Target repository')
.action(createLazyAction(() => import('./tool.js'), 'detectChangesCommand'));
// ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
program
.command('eval-server')
.description('Start lightweight HTTP server for fast tool calls during evaluation')
.option('-p, --port <port>', 'Port number', '4848')
.option(
'--host <host>',
'Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)',
)
.option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
.action(createLazyAction(() => import('./eval-server.js'), 'evalServerCommand'));
registerGroupCommands(program);
localizeCliHelp(program);
program.parse(process.argv);