diff --git a/src/helpers/copilot-settings.ts b/src/helpers/copilot-settings.ts index e8a4afdf..68b2c70b 100644 --- a/src/helpers/copilot-settings.ts +++ b/src/helpers/copilot-settings.ts @@ -1,77 +1,20 @@ import { join } from "node:path"; import { existsSync, mkdirSync } from "node:fs"; -/** - * MCP server configuration that archgate injects for Copilot CLI. - * - * Copilot CLI uses the same `.github/copilot/mcp.json` format for MCP servers - * and supports git-based plugin repositories via `copilot plugin install`. - */ -export const ARCHGATE_COPILOT_MCP_CONFIG = { - mcpServers: { - archgate: { - command: "archgate", - args: ["mcp"], - }, - }, -} as const; - -type CopilotMcpConfig = Record; - -/** - * Pure, additive merge of archgate MCP server config into existing Copilot MCP config. - * - * - Preserves all existing MCP server entries - * - Adds the archgate server only if not already present - */ -export function mergeCopilotMcpConfig( - existing: CopilotMcpConfig, - archgate: typeof ARCHGATE_COPILOT_MCP_CONFIG -): CopilotMcpConfig { - const existingServers = - typeof existing.mcpServers === "object" && - existing.mcpServers !== null && - !Array.isArray(existing.mcpServers) - ? (existing.mcpServers as Record) - : {}; - - return { - ...existing, - mcpServers: { - ...existingServers, - ...archgate.mcpServers, - }, - }; -} - /** * Configure Copilot CLI settings for archgate integration. * - * Creates/updates `.github/copilot/mcp.json` with archgate MCP server. + * Creates the `.github/copilot/` directory if it does not exist. + * Plugin installation is handled separately via `archgate init --install-plugin`. * - * @returns Absolute path to the MCP config file. + * @returns Absolute path to the `.github/copilot/` directory. */ -export async function configureCopilotSettings( - projectRoot: string -): Promise { +export function configureCopilotSettings(projectRoot: string): string { const copilotDir = join(projectRoot, ".github", "copilot"); - const mcpConfigPath = join(copilotDir, "mcp.json"); - // Read existing MCP config or start with empty object - let existing: CopilotMcpConfig = {}; - if (existsSync(mcpConfigPath)) { - const content = await Bun.file(mcpConfigPath).text(); - existing = JSON.parse(content) as CopilotMcpConfig; - } - - const merged = mergeCopilotMcpConfig(existing, ARCHGATE_COPILOT_MCP_CONFIG); - - // Ensure directories exist if (!existsSync(copilotDir)) { mkdirSync(copilotDir, { recursive: true }); } - await Bun.write(mcpConfigPath, JSON.stringify(merged, null, 2) + "\n"); - - return mcpConfigPath; + return copilotDir; } diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 32eebe21..d37b4087 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -116,7 +116,7 @@ async function configureEditorSettings( case "cursor": return configureCursorSettings(projectRoot); case "vscode": { - // VS Code: .vscode/mcp.json always, marketplace URL to user settings if logged in + // VS Code: marketplace URL to user settings if logged in const { loadCredentials } = await import("./auth"); const creds = await loadCredentials(); const marketplaceUrl = creds diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index 7bf8a773..668f778e 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -2,51 +2,8 @@ import { join } from "node:path"; import { existsSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; -/** - * MCP server configuration that archgate injects into .vscode/mcp.json. - * - * VS Code uses a dedicated `.vscode/mcp.json` file for MCP server registration - * (not `.vscode/settings.json`). The `servers` key format is defined by VS Code's - * MCP configuration spec. - */ -export const ARCHGATE_VSCODE_MCP_CONFIG = { - servers: { - archgate: { - command: "archgate", - args: ["mcp"], - }, - }, -} as const; - -type VscodeMcpConfig = Record; type VscodeUserSettings = Record; -/** - * Pure, additive merge of archgate MCP server config into existing VS Code MCP config. - * - * - Preserves all existing MCP server entries - * - Adds the archgate server (overwrites if already present) - */ -export function mergeVscodeMcpConfig( - existing: VscodeMcpConfig, - archgate: typeof ARCHGATE_VSCODE_MCP_CONFIG -): VscodeMcpConfig { - const existingServers = - typeof existing.servers === "object" && - existing.servers !== null && - !Array.isArray(existing.servers) - ? (existing.servers as Record) - : {}; - - return { - ...existing, - servers: { - ...existingServers, - ...archgate.servers, - }, - }; -} - /** * VS Code's built-in default marketplaces for `chat.plugins.marketplaces`. * @@ -128,40 +85,27 @@ export function getVscodeUserSettingsPath(): string { /** * Configure VS Code settings for archgate integration. * - * 1. Creates/updates `.vscode/mcp.json` (workspace-level) with the Archgate MCP server. - * 2. If `marketplaceUrl` is provided, adds it to `chat.plugins.marketplaces` in - * the VS Code user-level settings.json (application-scoped — cannot be set per workspace). + * If `marketplaceUrl` is provided, adds it to `chat.plugins.marketplaces` in + * the VS Code user-level settings.json (application-scoped — cannot be set per workspace). * - * @returns Absolute path to the workspace MCP config file. + * @returns Absolute path to the .vscode/ directory. */ export async function configureVscodeSettings( projectRoot: string, marketplaceUrl?: string ): Promise { const vscodeDir = join(projectRoot, ".vscode"); - const mcpConfigPath = join(vscodeDir, "mcp.json"); - - // --- Workspace: .vscode/mcp.json --- - let existing: VscodeMcpConfig = {}; - if (existsSync(mcpConfigPath)) { - const content = await Bun.file(mcpConfigPath).text(); - existing = Bun.JSONC.parse(content) as VscodeMcpConfig; - } - - const merged = mergeVscodeMcpConfig(existing, ARCHGATE_VSCODE_MCP_CONFIG); if (!existsSync(vscodeDir)) { mkdirSync(vscodeDir, { recursive: true }); } - await Bun.write(mcpConfigPath, JSON.stringify(merged, null, 2) + "\n"); - // --- User-level: chat.plugins.marketplaces --- if (marketplaceUrl) { await addMarketplaceToUserSettings(marketplaceUrl); } - return mcpConfigPath; + return vscodeDir; } /** diff --git a/tests/helpers/copilot-settings.test.ts b/tests/helpers/copilot-settings.test.ts index 8cc77e94..525f922d 100644 --- a/tests/helpers/copilot-settings.test.ts +++ b/tests/helpers/copilot-settings.test.ts @@ -1,92 +1,8 @@ import { describe, expect, test, beforeEach, afterEach } from "bun:test"; -import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs"; +import { mkdtempSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { - ARCHGATE_COPILOT_MCP_CONFIG, - mergeCopilotMcpConfig, - configureCopilotSettings, -} from "../../src/helpers/copilot-settings"; - -describe("mergeCopilotMcpConfig", () => { - test("sets archgate server when existing config is empty", () => { - const result = mergeCopilotMcpConfig({}, ARCHGATE_COPILOT_MCP_CONFIG); - - expect(result.mcpServers).toEqual({ - archgate: { - command: "archgate", - args: ["mcp"], - }, - }); - }); - - test("preserves existing MCP servers", () => { - const result = mergeCopilotMcpConfig( - { - mcpServers: { - "other-server": { - command: "other", - args: ["start"], - }, - }, - }, - ARCHGATE_COPILOT_MCP_CONFIG - ); - - const servers = result.mcpServers as Record; - expect(servers["other-server"]).toEqual({ - command: "other", - args: ["start"], - }); - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); - }); - - test("overwrites existing archgate server entry", () => { - const result = mergeCopilotMcpConfig( - { - mcpServers: { - archgate: { - command: "old-command", - args: ["old"], - }, - }, - }, - ARCHGATE_COPILOT_MCP_CONFIG - ); - - const servers = result.mcpServers as Record; - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); - }); - - test("preserves unknown top-level keys", () => { - const result = mergeCopilotMcpConfig( - { customKey: "value" }, - ARCHGATE_COPILOT_MCP_CONFIG - ); - - expect(result.customKey).toBe("value"); - }); - - test("handles non-object mcpServers gracefully", () => { - const result = mergeCopilotMcpConfig( - { mcpServers: "invalid" }, - ARCHGATE_COPILOT_MCP_CONFIG - ); - - expect(result.mcpServers).toEqual({ - archgate: { - command: "archgate", - args: ["mcp"], - }, - }); - }); -}); +import { configureCopilotSettings } from "../../src/helpers/copilot-settings"; describe("configureCopilotSettings", () => { let tempDir: string; @@ -99,51 +15,23 @@ describe("configureCopilotSettings", () => { rmSync(tempDir, { recursive: true, force: true }); }); - test("creates .github/copilot/ dir and mcp.json when nothing exists", async () => { - const mcpConfigPath = await configureCopilotSettings(tempDir); + test("creates .github/copilot/ dir when nothing exists", async () => { + await configureCopilotSettings(tempDir); expect(existsSync(join(tempDir, ".github", "copilot"))).toBe(true); - expect(existsSync(mcpConfigPath)).toBe(true); - - const mcpContent = JSON.parse(await Bun.file(mcpConfigPath).text()); - expect(mcpContent.mcpServers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); }); - test("merges into existing mcp.json without overwriting other servers", async () => { - const copilotDir = join(tempDir, ".github", "copilot"); - mkdirSync(copilotDir, { recursive: true }); - - const existingConfig = { - mcpServers: { - "my-server": { command: "my-cmd", args: [] }, - }, - }; - await Bun.write( - join(copilotDir, "mcp.json"), - JSON.stringify(existingConfig, null, 2) - ); - + test("does not create mcp.json", async () => { await configureCopilotSettings(tempDir); - const content = JSON.parse( - await Bun.file(join(copilotDir, "mcp.json")).text() + expect(existsSync(join(tempDir, ".github", "copilot", "mcp.json"))).toBe( + false ); - expect(content.mcpServers["my-server"]).toEqual({ - command: "my-cmd", - args: [], - }); - expect(content.mcpServers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); }); - test("returns correct absolute path to mcp.json", async () => { - const mcpConfigPath = await configureCopilotSettings(tempDir); + test("returns path to .github/copilot/ directory", async () => { + const result = await configureCopilotSettings(tempDir); - expect(mcpConfigPath).toBe(join(tempDir, ".github", "copilot", "mcp.json")); + expect(result).toBe(join(tempDir, ".github", "copilot")); }); }); diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index 5c04ca90..f34e66b7 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -3,94 +3,12 @@ import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { - ARCHGATE_VSCODE_MCP_CONFIG, - mergeVscodeMcpConfig, mergeMarketplaceUrl, configureVscodeSettings, addMarketplaceToUserSettings, getVscodeUserSettingsPath, } from "../../src/helpers/vscode-settings"; -describe("mergeVscodeMcpConfig", () => { - test("sets archgate server when existing config is empty", () => { - const result = mergeVscodeMcpConfig({}, ARCHGATE_VSCODE_MCP_CONFIG); - - expect(result.servers).toEqual({ - archgate: { - command: "archgate", - args: ["mcp"], - }, - }); - }); - - test("preserves existing MCP servers", () => { - const result = mergeVscodeMcpConfig( - { - servers: { - "other-server": { - command: "other", - args: ["start"], - }, - }, - }, - ARCHGATE_VSCODE_MCP_CONFIG - ); - - const servers = result.servers as Record; - expect(servers["other-server"]).toEqual({ - command: "other", - args: ["start"], - }); - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); - }); - - test("overwrites existing archgate server entry", () => { - const result = mergeVscodeMcpConfig( - { - servers: { - archgate: { - command: "old-command", - args: ["old"], - }, - }, - }, - ARCHGATE_VSCODE_MCP_CONFIG - ); - - const servers = result.servers as Record; - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); - }); - - test("preserves unknown top-level keys", () => { - const result = mergeVscodeMcpConfig( - { inputs: [{ type: "promptString", id: "key" }] }, - ARCHGATE_VSCODE_MCP_CONFIG - ); - - expect(result.inputs).toEqual([{ type: "promptString", id: "key" }]); - }); - - test("handles non-object servers gracefully", () => { - const result = mergeVscodeMcpConfig( - { servers: "invalid" }, - ARCHGATE_VSCODE_MCP_CONFIG - ); - - expect(result.servers).toEqual({ - archgate: { - command: "archgate", - args: ["mcp"], - }, - }); - }); -}); - describe("mergeMarketplaceUrl", () => { const URL = "https://user:token@plugins.archgate.dev/archgate.git"; @@ -146,80 +64,22 @@ describe("configureVscodeSettings", () => { rmSync(tempDir, { recursive: true, force: true }); }); - test("creates .vscode/ dir and mcp.json when nothing exists", async () => { - const mcpConfigPath = await configureVscodeSettings(tempDir); - - expect(existsSync(join(tempDir, ".vscode"))).toBe(true); - expect(existsSync(mcpConfigPath)).toBe(true); - - const content = JSON.parse(await Bun.file(mcpConfigPath).text()); - expect(content.servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); - }); - - test("merges into existing mcp.json without overwriting other servers", async () => { - const vscodeDir = join(tempDir, ".vscode"); - mkdirSync(vscodeDir, { recursive: true }); - - const existingConfig = { - servers: { - "my-server": { command: "my-cmd", args: [] }, - }, - }; - await Bun.write( - join(vscodeDir, "mcp.json"), - JSON.stringify(existingConfig, null, 2) - ); - + test("creates .vscode/ dir when nothing exists", async () => { await configureVscodeSettings(tempDir); - const content = JSON.parse( - await Bun.file(join(vscodeDir, "mcp.json")).text() - ); - expect(content.servers["my-server"]).toEqual({ - command: "my-cmd", - args: [], - }); - expect(content.servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); + expect(existsSync(join(tempDir, ".vscode"))).toBe(true); }); - test("parses JSONC (comments + trailing commas) in existing mcp.json", async () => { - const vscodeDir = join(tempDir, ".vscode"); - mkdirSync(vscodeDir, { recursive: true }); - - // Write JSONC with comments and trailing comma — as VS Code produces - const jsoncContent = `{ - // MCP servers - "servers": { - "my-server": { "command": "my-cmd", "args": [] }, - } - }`; - await Bun.write(join(vscodeDir, "mcp.json"), jsoncContent); - + test("does not create mcp.json", async () => { await configureVscodeSettings(tempDir); - const content = JSON.parse( - await Bun.file(join(vscodeDir, "mcp.json")).text() - ); - expect(content.servers["my-server"]).toEqual({ - command: "my-cmd", - args: [], - }); - expect(content.servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); + expect(existsSync(join(tempDir, ".vscode", "mcp.json"))).toBe(false); }); - test("returns correct absolute path to mcp.json", async () => { - const mcpConfigPath = await configureVscodeSettings(tempDir); + test("returns path to .vscode/ directory", async () => { + const result = await configureVscodeSettings(tempDir); - expect(mcpConfigPath).toBe(join(tempDir, ".vscode", "mcp.json")); + expect(result).toBe(join(tempDir, ".vscode")); }); });