diff --git a/src/mitm/detection/antigravity.ts b/src/mitm/detection/antigravity.ts new file mode 100644 index 0000000000..ca67c24eb0 --- /dev/null +++ b/src/mitm/detection/antigravity.ts @@ -0,0 +1,33 @@ +/** + * Antigravity IDE installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + // macOS + "/Applications/Antigravity.app", + path.join(HOME, "Applications", "Antigravity.app"), + // Linux (AppImage / system install) + "/usr/bin/antigravity", + "/usr/local/bin/antigravity", + path.join(HOME, ".local", "bin", "antigravity"), + // Windows + path.join( + process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"), + "Programs", + "Antigravity", + "Antigravity.exe" + ), +]; + +export function detectAntigravity(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/claudeCode.ts b/src/mitm/detection/claudeCode.ts new file mode 100644 index 0000000000..2d724aa834 --- /dev/null +++ b/src/mitm/detection/claudeCode.ts @@ -0,0 +1,29 @@ +/** + * Claude Code (Anthropic CLI) installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/usr/local/bin/claude", + "/usr/bin/claude", + path.join(HOME, ".local", "bin", "claude"), + path.join(HOME, ".npm-global", "bin", "claude"), + path.join(HOME, ".claude"), + path.join( + process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"), + "npm", + "claude.cmd" + ), +]; + +export function detectClaudeCode(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/codex.ts b/src/mitm/detection/codex.ts new file mode 100644 index 0000000000..2d045f7904 --- /dev/null +++ b/src/mitm/detection/codex.ts @@ -0,0 +1,29 @@ +/** + * OpenAI Codex CLI installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/usr/local/bin/codex", + "/usr/bin/codex", + path.join(HOME, ".local", "bin", "codex"), + path.join(HOME, ".npm-global", "bin", "codex"), + path.join(HOME, "node_modules", ".bin", "codex"), + path.join( + process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"), + "npm", + "codex.cmd" + ), +]; + +export function detectCodex(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/copilot.ts b/src/mitm/detection/copilot.ts new file mode 100644 index 0000000000..4d838090e4 --- /dev/null +++ b/src/mitm/detection/copilot.ts @@ -0,0 +1,39 @@ +/** + * GitHub Copilot installation detection. + * + * Detection strategy: look for the Copilot extension folder inside the user's + * VS Code (or fork) extensions directory. Purely filesystem-based — no shell + * interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); + +const EXTENSIONS_DIRS = [ + path.join(HOME, ".vscode", "extensions"), + path.join(HOME, ".vscode-insiders", "extensions"), + path.join(HOME, ".cursor", "extensions"), +]; + +export function detectCopilot(): DetectionResult { + for (const dir of EXTENSIONS_DIRS) { + try { + if (!fs.existsSync(dir)) continue; + const entries = fs.readdirSync(dir); + for (const name of entries) { + // Copilot extensions are named like `github.copilot-1.x.x`, + // `github.copilot-chat-...`. Match by prefix only. + const lower = name.toLowerCase(); + if (lower.startsWith("github.copilot")) { + return { installed: true, path: path.join(dir, name) }; + } + } + } catch { + // Permission or transient fs error — skip this directory. + } + } + return { installed: false }; +} diff --git a/src/mitm/detection/cursor.ts b/src/mitm/detection/cursor.ts new file mode 100644 index 0000000000..19d1f9ab32 --- /dev/null +++ b/src/mitm/detection/cursor.ts @@ -0,0 +1,31 @@ +/** + * Cursor IDE installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/Applications/Cursor.app", + path.join(HOME, "Applications", "Cursor.app"), + "/usr/bin/cursor", + "/usr/local/bin/cursor", + path.join(HOME, ".local", "bin", "cursor"), + path.join(HOME, ".cursor"), + path.join( + process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"), + "Programs", + "cursor", + "Cursor.exe" + ), +]; + +export function detectCursor(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/index.ts b/src/mitm/detection/index.ts new file mode 100644 index 0000000000..bb59c93889 --- /dev/null +++ b/src/mitm/detection/index.ts @@ -0,0 +1,53 @@ +/** + * Detection dispatcher. + * + * `detectAgent(id)` returns whether the given AgentBridge target is installed + * on the current machine. All detection probes are filesystem-only — they + * never spawn shells or interpolate runtime paths (Hard Rule #13). + * + * Trae is intentionally absent from the dispatch table: its viability is still + * under investigation, so callers receive `{ installed: false }` until the + * upstream surface is confirmed (see `targets/trae.ts`). + */ +import type { AgentId, DetectionResult } from "../types"; +import { detectAntigravity } from "./antigravity"; +import { detectKiro } from "./kiro"; +import { detectCopilot } from "./copilot"; +import { detectCodex } from "./codex"; +import { detectCursor } from "./cursor"; +import { detectZed } from "./zed"; +import { detectClaudeCode } from "./claudeCode"; +import { detectOpenCode } from "./openCode"; + +export const DETECTORS: Record DetectionResult> = { + antigravity: detectAntigravity, + kiro: detectKiro, + copilot: detectCopilot, + codex: detectCodex, + cursor: detectCursor, + zed: detectZed, + "claude-code": detectClaudeCode, + "open-code": detectOpenCode, + trae: () => ({ installed: false }), +}; + +export function detectAgent(id: AgentId): DetectionResult { + const fn = DETECTORS[id]; + if (!fn) return { installed: false }; + try { + return fn(); + } catch { + return { installed: false }; + } +} + +export { + detectAntigravity, + detectKiro, + detectCopilot, + detectCodex, + detectCursor, + detectZed, + detectClaudeCode, + detectOpenCode, +}; diff --git a/src/mitm/detection/kiro.ts b/src/mitm/detection/kiro.ts new file mode 100644 index 0000000000..d637154df8 --- /dev/null +++ b/src/mitm/detection/kiro.ts @@ -0,0 +1,30 @@ +/** + * Kiro IDE installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/Applications/Kiro.app", + path.join(HOME, "Applications", "Kiro.app"), + "/usr/bin/kiro", + "/usr/local/bin/kiro", + path.join(HOME, ".local", "bin", "kiro"), + path.join( + process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"), + "Programs", + "Kiro", + "Kiro.exe" + ), +]; + +export function detectKiro(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/openCode.ts b/src/mitm/detection/openCode.ts new file mode 100644 index 0000000000..7e4f2d5359 --- /dev/null +++ b/src/mitm/detection/openCode.ts @@ -0,0 +1,32 @@ +/** + * OpenCode installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/Applications/OpenCode.app", + path.join(HOME, "Applications", "OpenCode.app"), + "/usr/bin/opencode", + "/usr/local/bin/opencode", + path.join(HOME, ".local", "bin", "opencode"), + path.join(HOME, ".opencode"), + path.join(HOME, ".config", "opencode"), + path.join( + process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"), + "Programs", + "OpenCode", + "OpenCode.exe" + ), +]; + +export function detectOpenCode(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +} diff --git a/src/mitm/detection/zed.ts b/src/mitm/detection/zed.ts new file mode 100644 index 0000000000..805d008aea --- /dev/null +++ b/src/mitm/detection/zed.ts @@ -0,0 +1,26 @@ +/** + * Zed editor installation detection. + * Purely filesystem-based — no shell interpolation (Hard Rule #13). + */ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { DetectionResult } from "../types"; + +const HOME = os.homedir(); +const PATHS = [ + "/Applications/Zed.app", + path.join(HOME, "Applications", "Zed.app"), + "/usr/bin/zed", + "/usr/local/bin/zed", + path.join(HOME, ".local", "bin", "zed"), + path.join(HOME, ".local", "share", "zed"), + path.join(HOME, ".config", "zed"), +]; + +export function detectZed(): DetectionResult { + for (const p of PATHS) { + if (fs.existsSync(p)) return { installed: true, path: p }; + } + return { installed: false }; +}