/**
 * Install Lisa's MCP server config into agy's user-level mcp_config.json.
 *
 * The agy CLI (1.0.3) auto-loads MCP servers ONLY from the user-global
 * `~/.gemini/config/mcp_config.json` (shared with the Antigravity 2.0 desktop
 * app). A project-scope `<project>/.agents/mcp_config.json` is NOT read by the
 * agy CLI — verified-by-run (ticket-1054); the project path is retained here
 * only as a non-default option for callers/tools that target it directly. MCP
 * is NOT a plugin component on agy (plugin-bundled MCP is ignored) — the config
 * lives outside any plugin and is consumed independently.
 *
 * Cross-project caveat: because delivery is user-global, the most recent
 * `lisa apply` carrying MCP servers wins for every project on the machine. The
 * tagged-merge below preserves host-authored entries, but Lisa-managed entries
 * reflect whichever project last applied.
 *
 * Schema differences from Claude/Codex:
 *   - agy uses `serverUrl` for HTTP transport (NOT `url` or `httpUrl`).
 *   - stdio servers use the same `command`/`args`/`env` shape as Claude/Codex.
 *
 * Tagged-merge convention: Lisa-owned entries carry `_lisaManaged: true` at
 * the per-server level so the installer can rewrite Lisa's entries while
 * preserving host-authored entries on re-runs (mirrors the
 * `src/codex/hooks-merger.ts` pattern).
 * @module agy/mcp-installer
 */
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { existsSync, readFileSync } from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
/** Marker field tagged onto every Lisa-owned MCP server entry. */
export const LISA_MANAGED_MARKER = "_lisaManaged";
/**
 * Where agy reads user-scope MCP config (shared with Antigravity desktop).
 * @returns Absolute path to `~/.gemini/config/mcp_config.json`.
 */
export function defaultUserMcpConfigPath() {
    return path.join(os.homedir(), ".gemini", "config", "mcp_config.json");
}
/**
 * Project-scope MCP config path. NOTE: the agy CLI does NOT auto-load this path
 * (only the user-global file is read — ticket-1054); kept for callers/tools
 * that target a per-workspace file directly. `lisa apply` uses user scope.
 * @param destDir - Absolute path to the host project root.
 * @returns Absolute path to `<destDir>/.agents/mcp_config.json`.
 */
export function defaultProjectMcpConfigPath(destDir) {
    return path.join(destDir, ".agents", "mcp_config.json");
}
/**
 * Resolve the agy MCP config path for a given scope.
 *
 * `lisa apply` uses `scope: "user"` because the agy CLI only auto-loads the
 * user-global `~/.gemini/config/mcp_config.json` (the project-scope file is
 * never read — ticket-1054). The `"project"` scope remains available for
 * callers that explicitly want a per-workspace file, but it is NOT what the agy
 * CLI consumes. The default stays `"project"` for backward compatibility of
 * this pure resolver; `processAgyEmit` passes `"user"` explicitly.
 * @param opts - Scope selector and (for project scope) the host project root.
 * @param opts.scope - `"project"` (default) or `"user"`.
 * @param opts.destDir - Host project root; required for project scope.
 * @returns Absolute path to the mcp_config.json for the requested scope.
 */
export function resolveAgyMcpConfigPath(opts) {
    const scope = opts.scope ?? "project";
    if (scope === "user")
        return defaultUserMcpConfigPath();
    if (opts.destDir === undefined || opts.destDir === "") {
        throw new Error("resolveAgyMcpConfigPath: project scope requires a destDir (host project root)");
    }
    return defaultProjectMcpConfigPath(opts.destDir);
}
/**
 * Translate a Claude/Codex-shape MCP entry to agy's shape.
 *
 * Rules:
 *   - stdio entries: pass through `command`/`args`/`env` unchanged.
 *   - http entries: rename `url` → `serverUrl`, drop the `type` field, keep
 *     `headers` if present.
 *   - sse entries: agy SSE support is unverified at the schema level; pass
 *     through `serverUrl` and headers for parity with HTTP. Caller should
 *     verify before relying on SSE on agy.
 *
 * The Lisa-managed marker is attached at write time, not in this pure transform.
 * @param input Claude/Codex-format MCP server entry.
 * @returns agy-format MCP server entry.
 */
export function translateMcpEntryToAgy(input) {
    if (input.command !== undefined) {
        return {
            command: input.command,
            ...(input.args !== undefined ? { args: input.args } : {}),
            ...(input.env !== undefined ? { env: input.env } : {}),
        };
    }
    if (input.url !== undefined) {
        return {
            serverUrl: input.url,
            ...(input.headers !== undefined ? { headers: input.headers } : {}),
        };
    }
    // Empty / unknown shape — return as-is minus the type field.
    return {};
}
/**
 * Collect Lisa's MCP servers from the built plugin `.mcp.json` files (base plus
 * each detected stack), returning the merged Claude/Codex-shape server map.
 *
 * agy ignores plugin-bundled MCP, so `processAgyEmit` reads these files and
 * installs the result into agy's aggregate `mcp_config.json` via
 * `installAgyMcpConfig` (which translates `url` → `serverUrl`). Shallow-merged
 * base-first, so a later stack overrides the base on a name collision.
 * @param pluginRoot - Absolute path to the `plugins/` directory.
 * @param detectedTypes - Detected project types (e.g. `["expo"]`).
 * @returns Merged map of server name → Claude/Codex-shape entry (`{}` if none).
 */
export function collectLisaMcpServers(pluginRoot, detectedTypes) {
    const pluginNames = ["lisa", ...detectedTypes.map(t => `lisa-${t}`)];
    return pluginNames.reduce((acc, name) => {
        const mcpPath = path.join(pluginRoot, name, ".mcp.json");
        if (!existsSync(mcpPath))
            return acc;
        try {
            const parsed = JSON.parse(readFileSync(mcpPath, "utf8"));
            return parsed.mcpServers ? { ...acc, ...parsed.mcpServers } : acc;
        }
        catch (err) {
            // Malformed .mcp.json: skip it but surface why, so a typo is debuggable
            // instead of silently dropping the plugin's MCP servers.
            console.warn(`[lisa] Skipping unreadable MCP config ${mcpPath}: ${err instanceof Error ? err.message : String(err)}`);
            return acc;
        }
    }, {});
}
/**
 * Read the existing mcp_config.json file (or empty when absent).
 * @param filePath - Absolute path to the config file.
 * @returns Parsed object or {} when the file is missing.
 */
async function readExistingConfig(filePath) {
    if (!existsSync(filePath))
        return {};
    try {
        const raw = await readFile(filePath, "utf8");
        if (raw.trim() === "")
            return {};
        return JSON.parse(raw);
    }
    catch {
        return {};
    }
}
/**
 * Install Lisa's MCP servers into agy's mcp_config.json using tagged-merge.
 *
 * Tagged-merge rules:
 *   1. Read existing mcpServers (if any).
 *   2. Strip every entry where `_lisaManaged === true`.
 *   3. Add Lisa's entries with `_lisaManaged: true` attached.
 *   4. Host entries (no marker) are preserved verbatim.
 *
 * The target path defaults to `~/.gemini/config/mcp_config.json`. Pass
 * `targetPath` to override (e.g. for the project-level `.agents/mcp_config.json`).
 * @param lisaMcpServers - Lisa's MCP servers in Claude/Codex shape, keyed by
 *   server name.
 * @param targetPath - Absolute path to the mcp_config.json file. Defaults to
 *   the user-level path.
 * @returns Result with the path written and entry counts.
 */
export async function installAgyMcpConfig(lisaMcpServers, targetPath = defaultUserMcpConfigPath()) {
    await mkdir(path.dirname(targetPath), { recursive: true });
    const existing = await readExistingConfig(targetPath);
    const existingServers = existing.mcpServers ?? {};
    // Strip Lisa-managed entries from the existing config (immutable filter).
    const hostEntries = Object.fromEntries(Object.entries(existingServers).filter(([, entry]) => entry?.[LISA_MANAGED_MARKER] !== true));
    // Translate Lisa's entries and tag them (immutable map).
    const lisaEntries = Object.fromEntries(Object.entries(lisaMcpServers).map(([name, claudeEntry]) => [
        name,
        {
            ...translateMcpEntryToAgy(claudeEntry),
            [LISA_MANAGED_MARKER]: true,
        },
    ]));
    const merged = {
        ...existing,
        mcpServers: { ...hostEntries, ...lisaEntries },
    };
    await writeFile(targetPath, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
    return {
        path: targetPath,
        lisaEntryCount: Object.keys(lisaEntries).length,
        hostEntryCount: Object.keys(hostEntries).length,
    };
}
//# sourceMappingURL=mcp-installer.js.map