From f330a67d43949d6b6a813141b1b2717d1fb3fa80 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 4 Mar 2026 23:36:24 +0100 Subject: [PATCH 01/13] feat: add VS Code and Copilot CLI as editor targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for two new editors in `archgate init`: - VS Code (`--editor vscode`): configures .vscode/settings.json with the git marketplace URL and MCP server - Copilot CLI (`--editor copilot`): auto-installs via `copilot plugin install` or prints manual commands, configures .github/copilot/mcp.json Both use the same git-based plugin format already served by the plugins service — no backend changes needed. Includes documentation guides for both editors and sidebar entries. Co-Authored-By: Claude Opus 4.6 --- docs/astro.config.mjs | 8 + .../docs/guides/copilot-cli-plugin.mdx | 147 ++++++++++++++++++ .../src/content/docs/guides/vscode-plugin.mdx | 136 ++++++++++++++++ src/commands/init.ts | 70 ++++++--- src/helpers/copilot-settings.ts | 77 +++++++++ src/helpers/init-project.ts | 64 +++++++- src/helpers/plugin-install.ts | 62 ++++++++ src/helpers/vscode-settings.ts | 114 ++++++++++++++ 8 files changed, 651 insertions(+), 27 deletions(-) create mode 100644 docs/src/content/docs/guides/copilot-cli-plugin.mdx create mode 100644 docs/src/content/docs/guides/vscode-plugin.mdx create mode 100644 src/helpers/copilot-settings.ts create mode 100644 src/helpers/vscode-settings.ts diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 348edcf7..67f2f9f3 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -121,6 +121,14 @@ export default defineConfig({ label: "Claude Code Plugin", slug: "guides/claude-code-plugin", }, + { + label: "VS Code Plugin", + slug: "guides/vscode-plugin", + }, + { + label: "Copilot CLI Plugin", + slug: "guides/copilot-cli-plugin", + }, { label: "Cursor Integration", slug: "guides/cursor-integration", diff --git a/docs/src/content/docs/guides/copilot-cli-plugin.mdx b/docs/src/content/docs/guides/copilot-cli-plugin.mdx new file mode 100644 index 00000000..70599561 --- /dev/null +++ b/docs/src/content/docs/guides/copilot-cli-plugin.mdx @@ -0,0 +1,147 @@ +--- +title: Copilot CLI Plugin +description: Install the Archgate plugin for GitHub Copilot CLI with AI governance. +--- + +The Archgate Copilot CLI plugin gives AI agents working in [GitHub Copilot CLI](https://github.com/features/copilot) a structured governance workflow. Agents read your ADRs before writing code, validate after, and capture new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). + +## How it works + +Copilot CLI supports plugin installation from git repositories using `copilot plugin install`. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate.git`, which Copilot CLI recognizes natively -- the same `.claude-plugin/plugin.json` manifest format works for both Claude Code and Copilot CLI. + +## Installation + +:::note[Beta access required] +The Copilot CLI plugin is currently in beta. [Sign up at plugins.archgate.dev](https://plugins.archgate.dev) to get access before following the steps below. +::: + +### 1. Log in with GitHub + +Authenticate with your GitHub account to obtain a plugin token: + +```bash +archgate login +``` + +This starts a GitHub Device Flow. The CLI displays a one-time code and URL -- open the URL in your browser, enter the code, and authorize. Once complete, credentials are stored in `~/.archgate/credentials`. + +### 2. Initialize your project with the plugin + +Run `archgate init` with the `--editor copilot` flag: + +```bash +archgate init --editor copilot +``` + +If you are already logged in and the `copilot` CLI is on your PATH, the plugin is installed automatically via: + +```bash +copilot plugin install https://:@plugins.archgate.dev/archgate.git +``` + +If the `copilot` CLI is not found, the command prints the manual command for you to run. + +To explicitly request plugin installation: + +```bash +archgate init --editor copilot --install-plugin +``` + +### Generated files + +The command creates the following configuration: + +| File | Purpose | +| -------------------------- | ---------------------------------------------------- | +| `.github/copilot/mcp.json` | MCP server connection so Copilot CLI can access ADRs | + +If `.github/copilot/mcp.json` already exists, Archgate merges its configuration additively -- existing MCP server entries are preserved. + +The MCP configuration: + +```json +{ + "mcpServers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } +} +``` + +### Manual installation + +If the `copilot` CLI is not found during `archgate init`, you can install the plugin manually: + +```bash +copilot plugin install https://:@plugins.archgate.dev/archgate.git +``` + +You can find your authenticated URL by running `archgate login` and checking `~/.archgate/credentials`. + +## What the plugin provides + +The plugin adds role-based skills to Copilot CLI. Each skill encapsulates a specific part of the governance workflow, so agents follow the same process every time. + +### Skills included + +| Skill | Purpose | +| -------------------------- | ------------------------------------------------------------------------------------- | +| `archgate:developer` | General development agent that reads ADRs before coding and validates after | +| `archgate:architect` | Validates code changes against all project ADRs for structural compliance | +| `archgate:quality-manager` | Reviews rule coverage and proposes new ADRs when patterns emerge | +| `archgate:adr-author` | Creates and edits ADRs following project conventions | +| `archgate:onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | + +## Initial setup with onboard + +After installation, run the `archgate:onboard` skill in your project once. This skill: + +1. Explores your codebase structure (directories, key files, package configuration) +2. Interviews you about your team's conventions, constraints, and architectural decisions +3. Creates an initial set of ADRs based on your responses +4. Sets up the `.archgate/` directory with your first rules + +The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. + +## How it works in practice + +The plugin follows a structured workflow for every coding task: + +### 1. Read applicable ADRs + +When the developer gives a coding task, the agent reads all ADRs that apply to the files being changed. The MCP server provides a condensed briefing with the **Decision** and **Do's and Don'ts** sections from each relevant ADR. + +### 2. Write code following ADR constraints + +The agent writes code that complies with the constraints from the ADRs. The Do's and Don'ts sections serve as concrete guardrails. + +### 3. Validate changes + +After writing code, the agent runs `archgate check` to execute automated rules against the changes. Any violations are fixed before proceeding. + +### 4. Architect review + +The agent invokes `archgate:architect` to validate structural ADR compliance beyond what automated rules catch. + +### 5. Capture learnings + +The agent invokes `archgate:quality-manager` to review the work and identify patterns worth capturing as new ADRs. + +## MCP server integration + +The plugin communicates with your project's ADRs through the Archgate MCP server. The server provides: + +- **ADR content** -- full text of any ADR, accessible by ID +- **Review context** -- condensed briefings of all ADRs applicable to a set of changed files +- **Rule checking** -- execution of automated rules with violation reporting +- **ADR listing** -- inventory of all ADRs in the project with metadata + +The MCP server runs locally and reads directly from your `.archgate/adrs/` directory. No data leaves your machine. + +## Tips + +- **Commit `.github/copilot/mcp.json`** to share the MCP configuration with your team. +- **Run onboard once per project** to generate your initial ADRs from your actual codebase. +- **Keep ADR rule files up to date** -- the agent enforces what the rules check for. diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx new file mode 100644 index 00000000..5e0b5a3d --- /dev/null +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -0,0 +1,136 @@ +--- +title: VS Code Plugin +description: Install the Archgate plugin in VS Code for AI-assisted development with governance. +--- + +The Archgate VS Code plugin gives AI agents working in [VS Code](https://code.visualstudio.com/) a structured governance workflow. Agents read your ADRs before writing code, validate after, and capture new patterns for the team -- the same workflow available in the [Claude Code plugin](/guides/claude-code-plugin/). + +## How it works + +VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate.git`. When you add this marketplace to your VS Code settings, VS Code discovers and installs the plugin automatically. + +The plugin uses the same format as Claude Code plugins (`.claude-plugin/plugin.json`), which VS Code's agent plugin system recognizes natively. + +## Installation + +:::note[Beta access required] +The VS Code plugin is currently in beta. [Sign up at plugins.archgate.dev](https://plugins.archgate.dev) to get access before following the steps below. +::: + +### 1. Log in with GitHub + +Authenticate with your GitHub account to obtain a plugin token: + +```bash +archgate login +``` + +This starts a GitHub Device Flow. The CLI displays a one-time code and URL -- open the URL in your browser, enter the code, and authorize. Once complete, credentials are stored in `~/.archgate/credentials`. + +### 2. Initialize your project with the plugin + +Run `archgate init` with the `--editor vscode` flag: + +```bash +archgate init --editor vscode +``` + +If you are already logged in, this command: + +1. Creates the `.archgate/` governance directory (ADRs, lint rules) +2. Configures `.vscode/settings.json` with the authenticated marketplace URL +3. Registers the Archgate MCP server in `.vscode/settings.json` + +To explicitly request plugin installation: + +```bash +archgate init --editor vscode --install-plugin +``` + +### Generated settings + +The command adds the following to `.vscode/settings.json` (existing settings are preserved): + +```json +{ + "chat.plugins.marketplaces": [ + "https://:@plugins.archgate.dev/archgate.git" + ], + "mcp": { + "servers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } + } +} +``` + +If `.vscode/settings.json` already exists, Archgate merges its settings additively -- existing entries are preserved. + +## What the plugin provides + +The plugin adds role-based skills to VS Code's AI agent. Each skill encapsulates a specific part of the governance workflow, so agents follow the same process every time. + +### Skills included + +| Skill | Purpose | +| -------------------------- | ------------------------------------------------------------------------------------- | +| `archgate:developer` | General development agent that reads ADRs before coding and validates after | +| `archgate:architect` | Validates code changes against all project ADRs for structural compliance | +| `archgate:quality-manager` | Reviews rule coverage and proposes new ADRs when patterns emerge | +| `archgate:adr-author` | Creates and edits ADRs following project conventions | +| `archgate:onboard` | One-time setup: explores the codebase, interviews the developer, creates initial ADRs | + +## Initial setup with onboard + +After installation, run the `archgate:onboard` skill in your project once. This skill: + +1. Explores your codebase structure (directories, key files, package configuration) +2. Interviews you about your team's conventions, constraints, and architectural decisions +3. Creates an initial set of ADRs based on your responses +4. Sets up the `.archgate/` directory with your first rules + +The onboard skill is designed to run once per project. After onboarding, the other skills handle day-to-day development. + +## How it works in practice + +The plugin follows a structured workflow for every coding task: + +### 1. Read applicable ADRs + +When the developer gives a coding task, the agent reads all ADRs that apply to the files being changed. The MCP server provides a condensed briefing with the **Decision** and **Do's and Don'ts** sections from each relevant ADR. + +### 2. Write code following ADR constraints + +The agent writes code that complies with the constraints from the ADRs. The Do's and Don'ts sections serve as concrete guardrails. + +### 3. Validate changes + +After writing code, the agent runs `archgate check` to execute automated rules against the changes. Any violations are fixed before proceeding. + +### 4. Architect review + +The agent invokes `archgate:architect` to validate structural ADR compliance beyond what automated rules catch. + +### 5. Capture learnings + +The agent invokes `archgate:quality-manager` to review the work and identify patterns worth capturing as new ADRs. + +## MCP server integration + +The plugin communicates with your project's ADRs through the Archgate MCP server. The server provides: + +- **ADR content** -- full text of any ADR, accessible by ID +- **Review context** -- condensed briefings of all ADRs applicable to a set of changed files +- **Rule checking** -- execution of automated rules with violation reporting +- **ADR listing** -- inventory of all ADRs in the project with metadata + +The MCP server runs locally and reads directly from your `.archgate/adrs/` directory. No data leaves your machine. + +## Tips + +- **Commit `.vscode/settings.json`** to share the marketplace and MCP configuration with your team. +- **Run onboard once per project** to generate your initial ADRs from your actual codebase. +- **Keep ADR rule files up to date** -- the agent enforces what the rules check for. diff --git a/src/commands/init.ts b/src/commands/init.ts index 0c0c2692..874b383c 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -5,7 +5,21 @@ import { initProject } from "../helpers/init-project"; import type { EditorTarget } from "../helpers/init-project"; import { loadCredentials } from "../helpers/auth"; -const VALID_EDITORS = ["claude", "cursor"] as const; +const VALID_EDITORS = ["claude", "cursor", "vscode", "copilot"] as const; + +const EDITOR_LABELS: Record = { + claude: "Claude Code", + cursor: "Cursor", + vscode: "VS Code", + copilot: "Copilot CLI", +}; + +const EDITOR_DIRS: Record = { + claude: ".claude/", + cursor: ".cursor/", + vscode: ".vscode/", + copilot: ".github/copilot/", +}; export function registerInitCommand(program: Command) { program @@ -13,7 +27,7 @@ export function registerInitCommand(program: Command) { .description("Initialize Archgate governance in the current project") .option( "--editor ", - "editor integration to configure (claude, cursor)", + "editor integration to configure (claude, cursor, vscode, copilot)", "claude" ) .option( @@ -39,38 +53,26 @@ export function registerInitCommand(program: Command) { installPlugin, }); + const editorTarget = editor as EditorTarget; + const label = EDITOR_LABELS[editorTarget]; + const dir = EDITOR_DIRS[editorTarget]; + console.log(`Initialized Archgate governance in ${result.projectRoot}`); console.log(` adrs/ - architecture decision records`); console.log(` lint/ - linter-specific rules`); - if (editor === "cursor") { - console.log(` .cursor/ - Cursor settings configured`); - } else { - console.log(` .claude/ - Claude Code settings configured`); - } + console.log(` ${dir.padEnd(13)}- ${label} settings configured`); // Plugin install output if (result.plugin?.installed) { console.log(""); if (result.plugin.autoInstalled) { - logInfo( - editor === "cursor" - ? "Archgate plugin installed for Cursor." - : "Archgate plugin installed for Claude Code." - ); + logInfo(`Archgate plugin installed for ${label}.`); if (result.plugin.detail) { console.log(` ${result.plugin.detail}`); } } else { - // Claude Code — claude CLI not found, show manual commands - logWarn( - "Claude CLI not found. To install the plugin manually, run:" - ); - console.log( - ` ${styleText("bold", "claude plugin marketplace add")} ${result.plugin.detail}` - ); - console.log( - ` ${styleText("bold", "claude plugin install")} archgate@archgate` - ); + // CLI not found for this editor — show manual commands + printManualInstructions(editorTarget, result.plugin.detail); } } else if (installPlugin) { // User wanted plugin but no credentials @@ -85,3 +87,27 @@ export function registerInitCommand(program: Command) { } }); } + +/** + * Print manual plugin installation instructions when the editor CLI is not available. + */ +function printManualInstructions(editor: EditorTarget, detail?: string): void { + switch (editor) { + case "claude": + logWarn("Claude CLI not found. To install the plugin manually, run:"); + console.log( + ` ${styleText("bold", "claude plugin marketplace add")} ${detail}` + ); + console.log( + ` ${styleText("bold", "claude plugin install")} archgate@archgate` + ); + break; + case "copilot": + logWarn("Copilot CLI not found. To install the plugin manually, run:"); + console.log(` ${styleText("bold", "copilot plugin install")} ${detail}`); + break; + default: + // vscode and cursor auto-install always — should not reach here + break; + } +} diff --git a/src/helpers/copilot-settings.ts b/src/helpers/copilot-settings.ts new file mode 100644 index 00000000..e8a4afdf --- /dev/null +++ b/src/helpers/copilot-settings.ts @@ -0,0 +1,77 @@ +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. + * + * @returns Absolute path to the MCP config file. + */ +export async function configureCopilotSettings( + projectRoot: string +): Promise { + 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; +} diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 4187c77e..4201feb9 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -4,8 +4,10 @@ import { createPathIfNotExists, projectPaths } from "./paths"; import { generateExampleAdr } from "./adr-templates"; import { configureClaudeSettings } from "./claude-settings"; import { configureCursorSettings } from "./cursor-settings"; +import { configureVscodeSettings } from "./vscode-settings"; +import { configureCopilotSettings } from "./copilot-settings"; -export type EditorTarget = "claude" | "cursor"; +export type EditorTarget = "claude" | "cursor" | "vscode" | "copilot"; export interface InitOptions { editor?: EditorTarget; @@ -86,10 +88,7 @@ Archgate standardizes \`.archgate/lint/\` as the location for linter rules that ); const editor = options?.editor ?? "claude"; - const editorSettingsPath = - editor === "cursor" - ? await configureCursorSettings(projectRoot) - : await configureClaudeSettings(projectRoot); + const editorSettingsPath = await configureEditorSettings(projectRoot, editor); // Plugin installation (optional — requires stored credentials) let plugin: PluginResult | undefined; @@ -106,6 +105,34 @@ Archgate standardizes \`.archgate/lint/\` as the location for linter rules that }; } +/** + * Route editor settings configuration to the appropriate helper. + */ +async function configureEditorSettings( + projectRoot: string, + editor: EditorTarget +): Promise { + switch (editor) { + case "cursor": + return configureCursorSettings(projectRoot); + case "vscode": { + // VS Code needs the marketplace URL for settings — use a placeholder + // that gets replaced during plugin install if credentials exist. + const { loadCredentials } = await import("./auth"); + const creds = await loadCredentials(); + const { buildMarketplaceUrl } = await import("./plugin-install"); + const url = creds + ? buildMarketplaceUrl(creds) + : "https://plugins.archgate.dev/archgate.git"; + return configureVscodeSettings(projectRoot, url); + } + case "copilot": + return configureCopilotSettings(projectRoot); + default: + return configureClaudeSettings(projectRoot); + } +} + /** * Attempt to install the archgate plugin using stored credentials. * Returns null-safe result — never throws. @@ -130,6 +157,33 @@ async function tryInstallPlugin( }; } + if (editor === "vscode") { + const { installVscodePlugin } = await import("./plugin-install"); + await installVscodePlugin(projectRoot, credentials); + return { + installed: true, + autoInstalled: true, + detail: "Configured marketplace URL in .vscode/settings.json", + }; + } + + if (editor === "copilot") { + const { isCopilotCliAvailable, installCopilotPlugin, buildMarketplaceUrl } = + await import("./plugin-install"); + + if (await isCopilotCliAvailable()) { + try { + await installCopilotPlugin(credentials); + return { installed: true, autoInstalled: true }; + } catch { + // Fall through to manual instructions + } + } + + const url = buildMarketplaceUrl(credentials); + return { installed: true, detail: url }; + } + // Claude Code — try auto-install via `claude` CLI, fall back to manual URL const { isClaudeCliAvailable, installClaudePlugin, buildMarketplaceUrl } = await import("./plugin-install"); diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index 8058d6fc..9f08e9dc 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -2,6 +2,8 @@ * plugin-install.ts — Download and install the archgate plugin for supported editors. * * - Claude Code: auto-installs via `claude` CLI, or prints manual commands as fallback + * - VS Code: configures .vscode/settings.json with marketplace URL (git-based plugin) + * - Copilot CLI: auto-installs via `copilot` CLI, or prints manual commands as fallback * - Cursor: downloads cursor.tar.gz from the plugins service and extracts it */ @@ -113,6 +115,66 @@ export async function installCursorPlugin( return extractedFiles; } +// --------------------------------------------------------------------------- +// VS Code — configure marketplace URL in .vscode/settings.json +// --------------------------------------------------------------------------- + +/** + * Install the archgate plugin for VS Code by configuring the marketplace URL. + * + * VS Code agent plugins are installed from git-based marketplaces. This adds the + * authenticated marketplace URL to `.vscode/settings.json` so VS Code can discover + * and install the plugin automatically. + */ +export async function installVscodePlugin( + projectRoot: string, + credentials: StoredCredentials +): Promise { + const { configureVscodeSettings } = await import("./vscode-settings"); + const url = buildMarketplaceUrl(credentials); + return configureVscodeSettings(projectRoot, url); +} + +// --------------------------------------------------------------------------- +// Copilot CLI — CLI auto-install + manual fallback +// --------------------------------------------------------------------------- + +/** + * Check whether the `copilot` CLI is available on the system PATH. + */ +export async function isCopilotCliAvailable(): Promise { + const result = await $`copilot --version`.nothrow().quiet(); + return result.exitCode === 0; +} + +/** + * Install the archgate plugin via the `copilot` CLI. + * + * Runs: + * copilot plugin install + * + * Throws on failure so the caller can fall back to manual instructions. + */ +export async function installCopilotPlugin( + credentials: StoredCredentials +): Promise { + const url = buildMarketplaceUrl(credentials); + + logDebug("Installing archgate plugin via copilot CLI"); + const installResult = await $`copilot plugin install ${url}` + .nothrow() + .quiet(); + if (installResult.exitCode !== 0) { + throw new Error( + `copilot plugin install failed (exit ${installResult.exitCode})` + ); + } +} + +// --------------------------------------------------------------------------- +// Shared — tar extraction helper +// --------------------------------------------------------------------------- + /** * Extract a .tar.gz buffer to a destination directory. * Uses system tar (available on macOS, Linux, and Windows 10+). diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts new file mode 100644 index 00000000..f468e93e --- /dev/null +++ b/src/helpers/vscode-settings.ts @@ -0,0 +1,114 @@ +import { join } from "node:path"; +import { existsSync, mkdirSync } from "node:fs"; + +/** + * VS Code settings that archgate injects into .vscode/settings.json. + * + * - `chat.plugins.marketplaces`: registers the archgate git marketplace so + * VS Code's agent plugin system can discover and install the plugin. + * - MCP server configuration for the archgate governance tools. + */ +export const ARCHGATE_VSCODE_SETTINGS = { + "chat.plugins.marketplaces": [] as string[], + mcp: { + servers: { + archgate: { + command: "archgate", + args: ["mcp"], + }, + }, + }, +} as const; + +type VscodeSettings = Record; + +/** + * Deduplicate an array of strings while preserving order. + */ +function dedup(arr: string[]): string[] { + return [...new Set(arr)]; +} + +/** + * Pure, additive merge of archgate settings into existing VS Code settings. + * + * - `chat.plugins.marketplaces`: append marketplace URL with dedup + * - `mcp.servers`: add archgate server, preserve existing servers + * - All existing user settings are preserved (unknown keys pass through) + */ +export function mergeVscodeSettings( + existing: VscodeSettings, + marketplaceUrl: string +): VscodeSettings { + const merged: VscodeSettings = { ...existing }; + + // Marketplace URLs: append with dedup + const existingMarketplaces = Array.isArray( + merged["chat.plugins.marketplaces"] + ) + ? (merged["chat.plugins.marketplaces"] as string[]) + : []; + merged["chat.plugins.marketplaces"] = dedup([ + ...existingMarketplaces, + marketplaceUrl, + ]); + + // MCP servers: additive merge + const existingMcp = + typeof merged.mcp === "object" && + merged.mcp !== null && + !Array.isArray(merged.mcp) + ? (merged.mcp as Record) + : {}; + + const existingServers = + typeof existingMcp.servers === "object" && + existingMcp.servers !== null && + !Array.isArray(existingMcp.servers) + ? (existingMcp.servers as Record) + : {}; + + merged.mcp = { + ...existingMcp, + servers: { + ...existingServers, + ...ARCHGATE_VSCODE_SETTINGS.mcp.servers, + }, + }; + + return merged; +} + +/** + * Configure VS Code settings for archgate integration. + * + * Reads existing `.vscode/settings.json` (if any), merges archgate + * settings additively (marketplace URL + MCP server), and writes the result. + * + * @returns Absolute path to the settings file. + */ +export async function configureVscodeSettings( + projectRoot: string, + marketplaceUrl: string +): Promise { + const vscodeDir = join(projectRoot, ".vscode"); + const settingsPath = join(vscodeDir, "settings.json"); + + // Read existing settings or start with empty object + let existing: VscodeSettings = {}; + if (existsSync(settingsPath)) { + const content = await Bun.file(settingsPath).text(); + existing = JSON.parse(content) as VscodeSettings; + } + + const merged = mergeVscodeSettings(existing, marketplaceUrl); + + // Ensure .vscode/ directory exists + if (!existsSync(vscodeDir)) { + mkdirSync(vscodeDir, { recursive: true }); + } + + await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + "\n"); + + return settingsPath; +} From 33580ca870531817d19a244d584d7a6db827129d Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 4 Mar 2026 23:47:26 +0100 Subject: [PATCH 02/13] feat: add ARCH-007 (cross-platform subprocess execution) ADR and rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bun.$ hangs on Windows due to pipe deadlocks — this was discovered in production and fixed in ca33377. This commit: - Creates ARCH-007 ADR mandating Bun.spawn over Bun.$ for all subprocess execution, with automated rule (no-bun-shell) that blocks Bun.$ usage - Updates ARCH-006 to remove Bun.$ recommendations and add Windows caveat - Cherry-picks the Bun.spawn migration from ca33377 (git-files, upgrade, plugin-install) and fixes remaining Bun.$ usage in git.ts Co-Authored-By: Claude Opus 4.6 --- .archgate/adrs/ARCH-006-dependency-policy.md | 18 +- ...007-cross-platform-subprocess-execution.md | 182 ++++++++++++++++++ ...oss-platform-subprocess-execution.rules.ts | 63 ++++++ src/commands/upgrade.ts | 10 +- src/engine/git-files.ts | 48 +++-- src/helpers/git.ts | 43 +++-- src/helpers/plugin-install.ts | 58 ++++-- 7 files changed, 367 insertions(+), 55 deletions(-) create mode 100644 .archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md create mode 100644 .archgate/adrs/ARCH-007-cross-platform-subprocess-execution.rules.ts diff --git a/.archgate/adrs/ARCH-006-dependency-policy.md b/.archgate/adrs/ARCH-006-dependency-policy.md index 3a86e669..4b0b6db0 100644 --- a/.archgate/adrs/ARCH-006-dependency-policy.md +++ b/.archgate/adrs/ARCH-006-dependency-policy.md @@ -10,7 +10,9 @@ files: ["package.json"] Minimizing dependencies reduces supply chain risk, install size, and maintenance burden. Every production dependency is a trust relationship: the project trusts the dependency's maintainers, their CI/CD pipeline, and every transitive dependency in the tree. Supply chain attacks targeting popular npm packages (event-stream, ua-parser-js, colors.js) have demonstrated that this trust is frequently exploited. -Bun provides many built-in capabilities that eliminate the need for external packages — file I/O (`Bun.file`, `Bun.write`), HTTP server, shell commands (`Bun.$`), glob (`Bun.Glob`), TOML/YAML parsing, and testing. The fewer external packages in the dependency tree, the smaller the attack surface and the faster the install. +Bun provides many built-in capabilities that eliminate the need for external packages — file I/O (`Bun.file`, `Bun.write`), HTTP server, subprocess execution (`Bun.spawn`), glob (`Bun.Glob`), TOML/YAML parsing, and testing. The fewer external packages in the dependency tree, the smaller the attack surface and the faster the install. + +> **Note on `Bun.$` (Bun shell):** The `Bun.$` template literal API hangs on Windows because the shell subprocess does not properly close stdin/stdout pipes, causing deadlocks that freeze the process. This project uses `Bun.spawn` (array-based, no shell) exclusively for cross-platform subprocess execution. See commit `ca33377` for the migration. **Alternatives considered:** @@ -43,7 +45,9 @@ Development dependencies (`devDependencies`) are less restricted but should stil ### Do -- Use Bun built-ins for file I/O (`Bun.file`, `Bun.write`), HTTP, shell commands (`Bun.$`), glob (`Bun.Glob`), testing (`bun:test`) +- Use Bun built-ins for file I/O (`Bun.file`, `Bun.write`), HTTP, subprocess execution (`Bun.spawn`), glob (`Bun.Glob`), testing (`bun:test`) +- Use `Bun.spawn` with array-based arguments for all subprocess execution — it works correctly on macOS, Linux, and Windows +- **DON'T** use `Bun.$` (Bun shell template literals) for subprocess execution — it hangs on Windows due to pipe handling issues - Justify any new production dependency in a PR description - Keep `devDependencies` for tooling only (linting, formatting, commitlint) - Review the transitive dependency tree before adding a package @@ -70,8 +74,10 @@ await Bun.write("output.json", JSON.stringify(data)); const glob = new Bun.Glob("src/**/*.ts"); const files = Array.from(glob.scanSync({ cwd: projectRoot })); -// Shell commands — use Bun built-in -const result = await Bun.$`git ls-files`.text(); +// Subprocess execution — use Bun.spawn (cross-platform, no shell) +const proc = Bun.spawn(["git", "ls-files"], { stdout: "pipe", stderr: "pipe" }); +const result = await new Response(proc.stdout).text(); +await proc.exited; // Colors — use node:util built-in (not chalk) import { styleText } from "node:util"; @@ -109,12 +115,12 @@ const subset = pick(obj, ["a", "b"]); ### Negative -- **Bun built-in documentation is less comprehensive** — Some Bun APIs (`Bun.Glob`, `Bun.$`) have less documentation and fewer community examples than their npm counterparts (`glob`, `execa`). Contributors may need to reference Bun's source or test files. +- **Bun built-in documentation is less comprehensive** — Some Bun APIs (`Bun.Glob`, `Bun.spawn`) have less documentation and fewer community examples than their npm counterparts (`glob`, `execa`). Contributors may need to reference Bun's source or test files. - **Bun API surface may change** — Bun is actively developing and some APIs may change between minor versions. Pinning via `.prototools` mitigates but does not eliminate this risk. ### Risks -- **Bun API instability** — Bun built-in APIs (especially newer ones like `Bun.Glob`, `Bun.$`) may introduce breaking changes or behavioral differences between versions. +- **Bun API instability** — Bun built-in APIs (especially newer ones like `Bun.Glob`, `Bun.spawn`) may introduce breaking changes or behavioral differences between versions. The `Bun.$` shell API is explicitly avoided due to Windows pipe deadlocks discovered in production. - **Mitigation:** The project pins Bun version via `.prototools` (currently 1.3.8). API changes are caught during controlled upgrades with full test suite validation. - **Bun built-in feature gaps** — Some functionality may be missing from Bun built-ins (e.g., advanced glob options, streaming HTTP edge cases). If a Bun built-in lacks a critical feature, the fallback is to add an approved dependency with full justification. - **Mitigation:** The approved dependency list exists precisely for this case. The threshold is "Bun cannot do this," not "Bun can do this but an npm package is slightly more convenient." diff --git a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md new file mode 100644 index 00000000..01477ad4 --- /dev/null +++ b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md @@ -0,0 +1,182 @@ +--- +id: ARCH-007 +title: Cross-Platform Subprocess Execution +domain: architecture +rules: true +files: + - "src/**/*.ts" +--- + +## Context + +The Archgate CLI runs on macOS, Linux, and Windows. Several operations require spawning subprocesses: git commands (`git ls-files`, `git diff`), editor CLI calls (`claude plugin install`, `copilot plugin install`), archive extraction (`tar -xzf`), and package management (`npm install -g`). These subprocesses must work identically on all three platforms. + +Bun provides two subprocess APIs: + +- **`Bun.$` (shell template literals)** — A shell-like API that pipes commands through a subprocess shell. Convenient syntax (`await Bun.$\`git ls-files\`.text()`), but relies on platform-specific shell behavior. +- **`Bun.spawn` (array-based)** — A lower-level API that executes a command directly (no intermediate shell). Takes an array of arguments, explicit pipe configuration, and returns a process handle with `stdout`, `stderr`, and `exited` properties. + +**The problem:** `Bun.$` hangs on Windows. The shell subprocess does not properly close stdin/stdout pipes, causing deadlocks that block the calling thread indefinitely. When the Archgate CLI runs as an MCP server inside Claude Code or Cursor, this deadlock freezes the entire editor's agent interface — the user must force-kill the process. This was discovered in production and fixed in commit `ca33377`, which replaced all `Bun.$` calls with `Bun.spawn`. + +**Alternatives considered:** + +- **`Bun.$` with `.nothrow().quiet()`** — Adding error handling modifiers does not resolve the pipe deadlock. The hang occurs at the pipe level before any Bun-level error handling takes effect. +- **`node:child_process` (`execFile`, `spawn`)** — Node.js subprocess APIs work cross-platform but are callback-based or require manual stream wiring. `Bun.spawn` provides the same array-based execution model with native Promise/async support and direct `Bun.file`-compatible stdout. +- **Third-party libraries (`execa`, `cross-spawn`)** — These add production dependencies that [ARCH-006](./ARCH-006-dependency-policy.md) explicitly prohibits when Bun built-ins suffice. `Bun.spawn` covers all use cases without external packages. + +For Archgate, every subprocess call is either a git command, an editor CLI invocation, or a system tool (`tar`, `npm`). All of these are simple array-based command executions that do not require shell features (pipes, globbing, redirection). `Bun.spawn` is the correct tool. + +## Decision + +All subprocess execution in the Archgate CLI MUST use `Bun.spawn` with array-based arguments. The `Bun.$` shell template literal API is **forbidden** in all source files. + +This decision covers: + +- Git operations (`git ls-files`, `git diff`, `git status`) +- Editor CLI calls (`claude plugin marketplace add`, `copilot plugin install`) +- System tool invocations (`tar`, `npm`) +- Any other subprocess execution added in the future + +This decision does NOT cover: + +- Test files — test helpers may use `Bun.$` if tests only run on a single platform (though `Bun.spawn` is still preferred) +- Build scripts — scripts that explicitly target a single platform are exempt + +`Bun.spawn` will be used alongside: + +- [ARCH-006 — Dependency Policy](./ARCH-006-dependency-policy.md) — `Bun.spawn` is a Bun built-in, no external dependency needed +- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — Subprocess failures MUST be handled with proper error messages and exit codes + +## Do's and Don'ts + +### Do + +- **DO** use `Bun.spawn(["command", "arg1", "arg2"], { stdout: "pipe", stderr: "pipe" })` for commands whose output you need to capture +- **DO** read stdout via `new Response(proc.stdout).text()` — this is the idiomatic Bun pattern for consuming a `ReadableStream` +- **DO** always `await proc.exited` after reading stdout to ensure the process has terminated +- **DO** use `stdout: "inherit"` and `stderr: "inherit"` for commands whose output should go directly to the terminal (e.g., `npm install -g`) +- **DO** wrap CLI availability checks in `try/catch` returning a boolean — the command may not exist on the system +- **DO** pass `cwd` via the options object when the command must run in a specific directory +- **DO** extract a helper function (e.g., `run(cmd, opts)` or `runGit(args, cwd)`) when multiple subprocess calls share the same pattern within a module + +### Don't + +- **DON'T** use `Bun.$` template literals (`Bun.$\`command\``) — they hang on Windows due to pipe deadlocks +- **DON'T** import `$` from `"bun"` — this is the Bun shell API that causes Windows deadlocks +- **DON'T** use shell features (pipes `|`, redirects `>`, globbing `*`) in subprocess arguments — `Bun.spawn` executes commands directly without a shell +- **DON'T** forget to `await proc.exited` — reading stdout alone does not guarantee the process has terminated +- **DON'T** use `node:child_process` when `Bun.spawn` provides the same capability — prefer Bun built-ins per [ARCH-006](./ARCH-006-dependency-policy.md) + +## Implementation Pattern + +### Good Example + +```typescript +// Capture command output (git, tar, etc.) +async function run( + cmd: string[], + opts?: { cwd?: string } +): Promise<{ exitCode: number; stdout: string }> { + const proc = Bun.spawn(cmd, { + cwd: opts?.cwd, + stdout: "pipe", + stderr: "pipe", + }); + const stdout = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + return { exitCode, stdout }; +} + +// Usage +const { exitCode, stdout } = await run(["git", "diff", "--cached", "--name-only"], { cwd: projectRoot }); +const files = stdout.trim().split("\n").filter(Boolean); +``` + +```typescript +// CLI availability check +async function isClaudeCliAvailable(): Promise { + try { + const { exitCode } = await run(["claude", "--version"]); + return exitCode === 0; + } catch { + return false; // Command not found on PATH + } +} +``` + +```typescript +// Inherit output for interactive/visible commands +const proc = Bun.spawn(["npm", "install", "-g", "archgate@latest"], { + stdout: "inherit", + stderr: "inherit", +}); +const exitCode = await proc.exited; +``` + +### Bad Example + +```typescript +// BAD: Bun.$ hangs on Windows — pipe deadlock +import { $ } from "bun"; +const result = await $`git ls-files`.text(); + +// BAD: .nothrow().quiet() does not fix the pipe issue +const result = await $`git diff --cached --name-only`.nothrow().quiet().text(); + +// BAD: Shell features don't work with Bun.spawn +Bun.spawn(["git diff --cached | head -5"]); // This is a single argument, not a pipeline +``` + +## Consequences + +### Positive + +- **Cross-platform reliability** — `Bun.spawn` works identically on macOS, Linux, and Windows. No platform-specific pipe handling differences. +- **No deadlocks** — Array-based execution avoids the stdin/stdout pipe issues that cause `Bun.$` to hang on Windows. +- **MCP server safety** — The CLI runs as a long-lived MCP server inside editors. A subprocess deadlock would freeze the entire agent interface. `Bun.spawn` eliminates this risk. +- **Explicit argument handling** — Array-based arguments prevent shell injection vulnerabilities. Each argument is passed directly to the command, not interpreted by a shell. +- **No shell dependency** — The command does not require a shell interpreter (bash, cmd.exe, PowerShell) to be available or configured correctly. +- **Consistent error handling** — `proc.exited` returns a Promise that resolves to the exit code, making error checking uniform across all subprocess calls. + +### Negative + +- **More verbose syntax** — `Bun.spawn(["git", "ls-files"], { stdout: "pipe" })` is more verbose than `Bun.$\`git ls-files\``. The convenience of template literals is lost. + - This is mitigated by extracting `run()` or `runGit()` helper functions within each module. +- **No shell features** — Pipelines (`cmd1 | cmd2`), redirects (`> file`), and glob expansion (`*.ts`) are not available. Each must be implemented in JavaScript. + - The Archgate CLI does not use any of these shell features. All subprocess calls are simple command executions. +- **Manual stream consumption** — Reading stdout requires `new Response(proc.stdout).text()` instead of the simpler `.text()` chain on `Bun.$`. + +### Risks + +- **Future Bun.$ fix** — Bun may fix the Windows pipe issue in a future version, making `Bun.$` safe again. At that point, the project could relax this restriction. + - **Mitigation:** The ADR stands until verified on all three platforms with the fixed Bun version. A relaxation requires updating this ADR with the minimum safe Bun version. +- **Complex subprocess needs** — A future feature may require shell features (pipelines, redirects) that `Bun.spawn` cannot provide. + - **Mitigation:** Implement the pipeline logic in JavaScript (spawn multiple processes, pipe streams manually). If this becomes frequent, evaluate adding a subprocess helper library as an approved dependency under [ARCH-006](./ARCH-006-dependency-policy.md). + +## Compliance and Enforcement + +### Automated Enforcement + +- **Archgate rule** `ARCH-007/no-bun-shell`: Scans all TypeScript source files for `Bun.$` usage and `$` imports from `"bun"`. Severity: `error` (hard blocker). + +### Manual Enforcement + +Code reviewers MUST verify: + +1. No `Bun.$` template literals appear in new or modified code +2. No `import { $ } from "bun"` or `import { $, ... } from "bun"` statements exist +3. All subprocess calls use `Bun.spawn` with array-based arguments +4. `proc.exited` is awaited after reading stdout/stderr +5. CLI availability checks are wrapped in `try/catch` + +### Exceptions + +Test files (`tests/**/*.ts`) MAY use `Bun.$` if the test targets a single platform. However, `Bun.spawn` is still preferred for consistency. Any exception must be documented with a comment explaining why `Bun.$` is acceptable in that specific case. + +## References + +- [ARCH-006 — Dependency Policy](./ARCH-006-dependency-policy.md) — Mandates Bun built-ins over external packages; updated to remove `Bun.$` recommendation +- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — Defines error handling standards for subprocess failures +- [Bun.spawn documentation](https://bun.sh/docs/api/spawn) +- [Bun.$ documentation](https://bun.sh/docs/runtime/shell) — Documents the shell API that this ADR prohibits +- Commit `ca33377` — The production fix that migrated all `Bun.$` calls to `Bun.spawn` diff --git a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.rules.ts b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.rules.ts new file mode 100644 index 00000000..b1dbbaf0 --- /dev/null +++ b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.rules.ts @@ -0,0 +1,63 @@ +import { defineRules } from "../../src/formats/rules"; + +export default defineRules({ + "no-bun-shell": { + description: + "Subprocess execution must use Bun.spawn, not Bun.$ (shell hangs on Windows)", + async check(ctx) { + const files = ctx.scopedFiles.filter( + (f) => !f.includes("tests/") && !f.includes(".archgate/") + ); + + // Check for Bun.$ template literal usage + const bunShellMatches = await Promise.all( + files.map((file) => ctx.grep(file, /Bun\.\$`/)) + ); + for (const fileMatches of bunShellMatches) { + for (const m of fileMatches) { + ctx.report.violation({ + message: + "Do not use Bun.$ template literals — they hang on Windows due to pipe deadlocks. Use Bun.spawn instead.", + file: m.file, + line: m.line, + fix: "Replace Bun.$`cmd args` with Bun.spawn(['cmd', 'args'], { stdout: 'pipe', stderr: 'pipe' })", + }); + } + } + + // Check for $ import from "bun" (the shell API) + const dollarImportMatches = await Promise.all( + files.map((file) => + ctx.grep(file, /import\s*\{[^}]*\$[^}]*\}\s*from\s*["']bun["']/) + ) + ); + for (const fileMatches of dollarImportMatches) { + for (const m of fileMatches) { + ctx.report.violation({ + message: + 'Do not import $ from "bun" — the Bun shell API hangs on Windows. Use Bun.spawn instead.', + file: m.file, + line: m.line, + fix: 'Remove the $ import from "bun" and replace shell calls with Bun.spawn', + }); + } + } + + // Check for await $` pattern (destructured $ usage) + const destructuredMatches = await Promise.all( + files.map((file) => ctx.grep(file, /await\s+\$`/)) + ); + for (const fileMatches of destructuredMatches) { + for (const m of fileMatches) { + ctx.report.violation({ + message: + "Do not use $` template literals (destructured Bun shell) — they hang on Windows. Use Bun.spawn instead.", + file: m.file, + line: m.line, + fix: "Replace $`cmd args` with Bun.spawn(['cmd', 'args'], { stdout: 'pipe', stderr: 'pipe' })", + }); + } + } + }, + }, +}); diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts index 5bec1e46..c4a6c9ec 100644 --- a/src/commands/upgrade.ts +++ b/src/commands/upgrade.ts @@ -1,5 +1,5 @@ import type { Command } from "@commander-js/extra-typings"; -import { $, semver } from "bun"; +import { semver } from "bun"; import { logError } from "../helpers/log"; const NPM_REGISTRY = "https://registry.npmjs.org/archgate/latest"; @@ -58,9 +58,13 @@ export function registerUpgradeCommand(program: Command) { console.log(`Upgrading ${currentVersion} -> ${latestVersion}...`); - const result = await $`npm install -g archgate@latest`.nothrow(); + const proc = Bun.spawn(["npm", "install", "-g", "archgate@latest"], { + stdout: "inherit", + stderr: "inherit", + }); + const exitCode = await proc.exited; - if (result.exitCode !== 0) { + if (exitCode !== 0) { logError( "Failed to install the latest version via npm.", "Try running `npm install -g archgate@latest` manually." diff --git a/src/engine/git-files.ts b/src/engine/git-files.ts index 589c00d5..07464250 100644 --- a/src/engine/git-files.ts +++ b/src/engine/git-files.ts @@ -1,15 +1,32 @@ /** Git file-listing utilities for ADR scope resolution and change detection. */ +/** + * Run a git command using Bun.spawn (cross-platform, no shell). + * Bun.$ hangs on Windows due to pipe handling issues — this is the safe alternative. + */ +async function runGit(args: string[], cwd: string): Promise { + const proc = Bun.spawn(["git", ...args], { + cwd, + stdout: "pipe", + stderr: "pipe", + }); + const text = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(`git ${args[0]} exited with code ${exitCode}`); + } + return text; +} + /** Get all git-tracked (non-ignored) files in the project. */ export async function getGitTrackedFiles( projectRoot: string ): Promise | null> { try { - const result = - await Bun.$`git ls-files --cached --others --exclude-standard` - .cwd(projectRoot) - .quiet() - .text(); + const result = await runGit( + ["ls-files", "--cached", "--others", "--exclude-standard"], + projectRoot + ); return new Set(result.trim().split("\n").filter(Boolean)); } catch { return null; @@ -40,10 +57,10 @@ export async function resolveScopedFiles( /** Get changed files from git staging area. */ export async function getStagedFiles(projectRoot: string): Promise { try { - const result = await Bun.$`git diff --cached --name-only` - .cwd(projectRoot) - .quiet() - .text(); + const result = await runGit( + ["diff", "--cached", "--name-only"], + projectRoot + ); return result.trim().split("\n").filter(Boolean); } catch { return []; @@ -53,14 +70,11 @@ export async function getStagedFiles(projectRoot: string): Promise { /** Get all changed files (staged + unstaged). */ export async function getChangedFiles(projectRoot: string): Promise { try { - const staged = await Bun.$`git diff --cached --name-only` - .cwd(projectRoot) - .quiet() - .text(); - const unstaged = await Bun.$`git diff --name-only` - .cwd(projectRoot) - .quiet() - .text(); + const staged = await runGit( + ["diff", "--cached", "--name-only"], + projectRoot + ); + const unstaged = await runGit(["diff", "--name-only"], projectRoot); const all = new Set([ ...staged.trim().split("\n").filter(Boolean), ...unstaged.trim().split("\n").filter(Boolean), diff --git a/src/helpers/git.ts b/src/helpers/git.ts index 4c600fd4..8b22444e 100644 --- a/src/helpers/git.ts +++ b/src/helpers/git.ts @@ -1,19 +1,25 @@ -import { $ } from "bun"; import { logDebug } from "./log"; -export function installGit() { +export async function installGit() { if (Bun.which("git")) { logDebug("Git is already installed"); return; } console.log("Git is not installed. Installing..."); - if (process.platform === "darwin") return $`brew install git`; - if (process.platform === "linux") return $`sudo apt-get install -y git`; - if (process.platform === "win32") + if (process.platform === "win32") { throw new Error( "Git is not installed. Install it from https://git-scm.com/download/win and make sure it is on your PATH." ); - throw new Error("Unsupported platform"); + } + const cmd = + process.platform === "darwin" + ? ["brew", "install", "git"] + : ["sudo", "apt-get", "install", "-y", "git"]; + const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" }); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(`Failed to install git (exit code ${exitCode})`); + } } /** @@ -21,12 +27,25 @@ export function installGit() { */ export async function getChangedFiles(projectRoot: string): Promise { try { - const result = - await $`git diff --name-only && git diff --cached --name-only` - .cwd(projectRoot) - .quiet() - .text(); - const files = result.trim().split("\n").filter(Boolean); + const spawnOpts = { + cwd: projectRoot, + stdout: "pipe" as const, + stderr: "pipe" as const, + }; + const unstaged = Bun.spawn(["git", "diff", "--name-only"], spawnOpts); + const staged = Bun.spawn( + ["git", "diff", "--cached", "--name-only"], + spawnOpts + ); + const [unstagedText, stagedText] = await Promise.all([ + new Response(unstaged.stdout).text(), + new Response(staged.stdout).text(), + ]); + await Promise.all([unstaged.exited, staged.exited]); + const files = [ + ...unstagedText.trim().split("\n"), + ...stagedText.trim().split("\n"), + ].filter(Boolean); return [...new Set(files)]; } catch { return []; diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index 9f08e9dc..42bacfc8 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -9,12 +9,29 @@ import { join } from "node:path"; import { mkdirSync, unlinkSync } from "node:fs"; -import { $ } from "bun"; import { logDebug } from "./log"; import type { StoredCredentials } from "./auth"; const PLUGINS_API = "https://plugins.archgate.dev"; +/** + * Run a command using Bun.spawn (cross-platform, no shell). + * Returns { exitCode, stdout }. + */ +async function run( + cmd: string[], + opts?: { cwd?: string } +): Promise<{ exitCode: number; stdout: string }> { + const proc = Bun.spawn(cmd, { + cwd: opts?.cwd, + stdout: "pipe", + stderr: "pipe", + }); + const stdout = await new Response(proc.stdout).text(); + const exitCode = await proc.exited; + return { exitCode, stdout }; +} + // --------------------------------------------------------------------------- // Claude Code — CLI auto-install + manual fallback // --------------------------------------------------------------------------- @@ -30,8 +47,12 @@ export function buildMarketplaceUrl(credentials: StoredCredentials): string { * Check whether the `claude` CLI is available on the system PATH. */ export async function isClaudeCliAvailable(): Promise { - const result = await $`claude --version`.nothrow().quiet(); - return result.exitCode === 0; + try { + const { exitCode } = await run(["claude", "--version"]); + return exitCode === 0; + } catch { + return false; + } } /** @@ -49,9 +70,7 @@ export async function installClaudePlugin( const url = buildMarketplaceUrl(credentials); logDebug("Adding archgate marketplace to claude CLI"); - const addResult = await $`claude plugin marketplace add ${url}` - .nothrow() - .quiet(); + const addResult = await run(["claude", "plugin", "marketplace", "add", url]); if (addResult.exitCode !== 0) { throw new Error( `claude plugin marketplace add failed (exit ${addResult.exitCode})` @@ -59,9 +78,12 @@ export async function installClaudePlugin( } logDebug("Installing archgate plugin via claude CLI"); - const installResult = await $`claude plugin install archgate@archgate` - .nothrow() - .quiet(); + const installResult = await run([ + "claude", + "plugin", + "install", + "archgate@archgate", + ]); if (installResult.exitCode !== 0) { throw new Error( `claude plugin install failed (exit ${installResult.exitCode})` @@ -143,8 +165,12 @@ export async function installVscodePlugin( * Check whether the `copilot` CLI is available on the system PATH. */ export async function isCopilotCliAvailable(): Promise { - const result = await $`copilot --version`.nothrow().quiet(); - return result.exitCode === 0; + try { + const { exitCode } = await run(["copilot", "--version"]); + return exitCode === 0; + } catch { + return false; + } } /** @@ -161,9 +187,7 @@ export async function installCopilotPlugin( const url = buildMarketplaceUrl(credentials); logDebug("Installing archgate plugin via copilot CLI"); - const installResult = await $`copilot plugin install ${url}` - .nothrow() - .quiet(); + const installResult = await run(["copilot", "plugin", "install", url]); if (installResult.exitCode !== 0) { throw new Error( `copilot plugin install failed (exit ${installResult.exitCode})` @@ -191,7 +215,7 @@ async function extractTarGz( mkdirSync(destDir, { recursive: true }); // Extract using tar (available on macOS, Linux, and Windows 10+) - const result = await $`tar -xzf ${tmpArchive} -C ${destDir}`.nothrow(); + const result = await run(["tar", "-xzf", tmpArchive, "-C", destDir]); if (result.exitCode !== 0) { throw new Error( @@ -200,8 +224,8 @@ async function extractTarGz( } // List extracted files for reporting - const listResult = await $`tar -tzf ${tmpArchive}`.nothrow().text(); - const files = listResult + const listResult = await run(["tar", "-tzf", tmpArchive]); + const files = listResult.stdout .split("\n") .map((f) => f.trim()) .filter(Boolean); From 92f2b48db15df456b68f0a1d4ec009d81e281a1d Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Wed, 4 Mar 2026 23:53:37 +0100 Subject: [PATCH 03/13] style: format ARCH-007 ADR with Prettier Co-Authored-By: Claude Opus 4.6 --- .../adrs/ARCH-007-cross-platform-subprocess-execution.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md index 01477ad4..5286457a 100644 --- a/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md +++ b/.archgate/adrs/ARCH-007-cross-platform-subprocess-execution.md @@ -88,7 +88,10 @@ async function run( } // Usage -const { exitCode, stdout } = await run(["git", "diff", "--cached", "--name-only"], { cwd: projectRoot }); +const { exitCode, stdout } = await run( + ["git", "diff", "--cached", "--name-only"], + { cwd: projectRoot } +); const files = stdout.trim().split("\n").filter(Boolean); ``` From 3725c4a4634bef240bc37fb57c87939d29247d25 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 00:22:40 +0100 Subject: [PATCH 04/13] test: add settings helper tests and pt-br guide translations Add test suites for vscode-settings and copilot-settings helpers (ARCH-005 compliance). Add full Portuguese translations for the VS Code and Copilot CLI plugin guide pages (GEN-002 compliance). Co-Authored-By: Claude Opus 4.6 --- .../docs/pt-br/guides/copilot-cli-plugin.mdx | 147 ++++++++++++++ .../docs/pt-br/guides/vscode-plugin.mdx | 136 +++++++++++++ tests/helpers/copilot-settings.test.ts | 149 +++++++++++++++ tests/helpers/vscode-settings.test.ts | 179 ++++++++++++++++++ 4 files changed, 611 insertions(+) create mode 100644 docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx create mode 100644 docs/src/content/docs/pt-br/guides/vscode-plugin.mdx create mode 100644 tests/helpers/copilot-settings.test.ts create mode 100644 tests/helpers/vscode-settings.test.ts diff --git a/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx b/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx new file mode 100644 index 00000000..4d67cf1b --- /dev/null +++ b/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx @@ -0,0 +1,147 @@ +--- +title: Plugin para Copilot CLI +description: Instale o plugin Archgate para GitHub Copilot CLI com governança de IA. +--- + +O plugin Archgate para Copilot CLI oferece aos agentes de IA que trabalham no [GitHub Copilot CLI](https://github.com/features/copilot) um fluxo de governança estruturado. Os agentes leem seus ADRs antes de escrever código, validam depois e capturam novos padrões para a equipe -- o mesmo fluxo disponível no [plugin para Claude Code](/pt-br/guides/claude-code-plugin/). + +## Como funciona + +O Copilot CLI suporta instalação de plugins a partir de repositórios git usando `copilot plugin install`. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate.git`, que o Copilot CLI reconhece nativamente -- o mesmo formato de manifesto `.claude-plugin/plugin.json` funciona tanto para o Claude Code quanto para o Copilot CLI. + +## Instalação + +:::note[Acesso beta necessário] +O plugin para Copilot CLI está atualmente em beta. [Cadastre-se em plugins.archgate.dev](https://plugins.archgate.dev) para obter acesso antes de seguir os passos abaixo. +::: + +### 1. Faça login com o GitHub + +Autentique-se com sua conta do GitHub para obter um token do plugin: + +```bash +archgate login +``` + +Isso inicia um GitHub Device Flow. O CLI exibe um código único e uma URL -- abra a URL no seu navegador, insira o código e autorize. Ao concluir, as credenciais são armazenadas em `~/.archgate/credentials`. + +### 2. Inicialize seu projeto com o plugin + +Execute `archgate init` com a flag `--editor copilot`: + +```bash +archgate init --editor copilot +``` + +Se você já estiver logado e o CLI `copilot` estiver no seu PATH, o plugin é instalado automaticamente via: + +```bash +copilot plugin install https://:@plugins.archgate.dev/archgate.git +``` + +Se o CLI `copilot` não for encontrado, o comando exibe o comando manual para você executar. + +Para solicitar explicitamente a instalação do plugin: + +```bash +archgate init --editor copilot --install-plugin +``` + +### Arquivos gerados + +O comando cria a seguinte configuração: + +| Arquivo | Propósito | +| -------------------------- | ------------------------------------------------------------- | +| `.github/copilot/mcp.json` | Conexão do servidor MCP para que o Copilot CLI acesse os ADRs | + +Se `.github/copilot/mcp.json` já existir, o Archgate mescla sua configuração de forma aditiva -- entradas de servidores MCP existentes são preservadas. + +A configuração MCP: + +```json +{ + "mcpServers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } +} +``` + +### Instalação manual + +Se o CLI `copilot` não for encontrado durante `archgate init`, você pode instalar o plugin manualmente: + +```bash +copilot plugin install https://:@plugins.archgate.dev/archgate.git +``` + +Você pode encontrar sua URL autenticada executando `archgate login` e verificando `~/.archgate/credentials`. + +## O que o plugin oferece + +O plugin adiciona skills baseadas em papéis ao Copilot CLI. Cada skill encapsula uma parte específica do fluxo de governança, garantindo que os agentes sigam o mesmo processo todas as vezes. + +### Skills incluídas + +| Skill | Propósito | +| -------------------------- | ------------------------------------------------------------------------------------------ | +| `archgate:developer` | Agente de desenvolvimento geral que lê ADRs antes de codificar e valida depois | +| `archgate:architect` | Valida alterações de código contra todos os ADRs do projeto para conformidade estrutural | +| `archgate:quality-manager` | Revisa a cobertura de regras e propõe novos ADRs quando padrões emergem | +| `archgate:adr-author` | Cria e edita ADRs seguindo as convenções do projeto | +| `archgate:onboard` | Configuração única: explora o codebase, entrevista o desenvolvedor e cria os ADRs iniciais | + +## Configuração inicial com onboard + +Após a instalação, execute a skill `archgate:onboard` no seu projeto uma vez. Essa skill: + +1. Explora a estrutura do seu codebase (diretórios, arquivos-chave, configuração de pacotes) +2. Entrevista você sobre as convenções, restrições e decisões arquiteturais da sua equipe +3. Cria um conjunto inicial de ADRs com base nas suas respostas +4. Configura o diretório `.archgate/` com suas primeiras regras + +A skill de onboard foi projetada para ser executada uma vez por projeto. Após o onboarding, as outras skills cuidam do desenvolvimento no dia a dia. + +## Como funciona na prática + +O plugin segue um fluxo estruturado para cada tarefa de codificação: + +### 1. Ler os ADRs aplicáveis + +Quando o desenvolvedor atribui uma tarefa de codificação, o agente lê todos os ADRs que se aplicam aos arquivos sendo alterados. O servidor MCP fornece um resumo condensado com as seções **Decision** e **Do's and Don'ts** de cada ADR relevante. + +### 2. Escrever código seguindo as restrições dos ADRs + +O agente escreve código que cumpre as restrições dos ADRs. As seções Do's and Don'ts servem como guardrails concretos. + +### 3. Validar as alterações + +Após escrever o código, o agente executa `archgate check` para rodar as regras automatizadas contra as alterações. Qualquer violação é corrigida antes de prosseguir. + +### 4. Revisão do arquiteto + +O agente invoca `archgate:architect` para validar a conformidade estrutural com os ADRs além do que as regras automatizadas capturam. + +### 5. Capturar aprendizados + +O agente invoca `archgate:quality-manager` para revisar o trabalho e identificar padrões que valem ser capturados como novos ADRs. + +## Integração com o servidor MCP + +O plugin se comunica com os ADRs do seu projeto por meio do servidor MCP do Archgate. O servidor fornece: + +- **Conteúdo de ADRs** -- texto completo de qualquer ADR, acessível por ID +- **Contexto de revisão** -- resumos condensados de todos os ADRs aplicáveis a um conjunto de arquivos alterados +- **Verificação de regras** -- execução de regras automatizadas com relatório de violações +- **Listagem de ADRs** -- inventário de todos os ADRs do projeto com metadados + +O servidor MCP roda localmente e lê diretamente do seu diretório `.archgate/adrs/`. Nenhum dado sai da sua máquina. + +## Dicas + +- **Faça commit do `.github/copilot/mcp.json`** para compartilhar a configuração MCP com sua equipe. +- **Execute o onboard uma vez por projeto** para gerar seus ADRs iniciais a partir do seu codebase real. +- **Mantenha os arquivos de regras dos ADRs atualizados** -- o agente aplica o que as regras verificam. diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx new file mode 100644 index 00000000..2b2f3ae3 --- /dev/null +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -0,0 +1,136 @@ +--- +title: Plugin para VS Code +description: Instale o plugin Archgate no VS Code para desenvolvimento assistido por IA com governança. +--- + +O plugin Archgate para VS Code oferece aos agentes de IA que trabalham no [VS Code](https://code.visualstudio.com/) um fluxo de governança estruturado. Os agentes leem seus ADRs antes de escrever código, validam depois e capturam novos padrões para a equipe -- o mesmo fluxo disponível no [plugin para Claude Code](/pt-br/guides/claude-code-plugin/). + +## Como funciona + +O VS Code suporta **plugins de agente** instalados a partir de marketplaces baseados em git. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate.git`. Quando você adiciona esse marketplace às suas configurações do VS Code, o VS Code descobre e instala o plugin automaticamente. + +O plugin usa o mesmo formato dos plugins do Claude Code (`.claude-plugin/plugin.json`), que o sistema de plugins de agente do VS Code reconhece nativamente. + +## Instalação + +:::note[Acesso beta necessário] +O plugin para VS Code está atualmente em beta. [Cadastre-se em plugins.archgate.dev](https://plugins.archgate.dev) para obter acesso antes de seguir os passos abaixo. +::: + +### 1. Faça login com o GitHub + +Autentique-se com sua conta do GitHub para obter um token do plugin: + +```bash +archgate login +``` + +Isso inicia um GitHub Device Flow. O CLI exibe um código único e uma URL -- abra a URL no seu navegador, insira o código e autorize. Ao concluir, as credenciais são armazenadas em `~/.archgate/credentials`. + +### 2. Inicialize seu projeto com o plugin + +Execute `archgate init` com a flag `--editor vscode`: + +```bash +archgate init --editor vscode +``` + +Se você já estiver logado, este comando: + +1. Cria o diretório de governança `.archgate/` (ADRs, regras de lint) +2. Configura `.vscode/settings.json` com a URL autenticada do marketplace +3. Registra o servidor MCP do Archgate em `.vscode/settings.json` + +Para solicitar explicitamente a instalação do plugin: + +```bash +archgate init --editor vscode --install-plugin +``` + +### Configurações geradas + +O comando adiciona o seguinte ao `.vscode/settings.json` (configurações existentes são preservadas): + +```json +{ + "chat.plugins.marketplaces": [ + "https://:@plugins.archgate.dev/archgate.git" + ], + "mcp": { + "servers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } + } +} +``` + +Se `.vscode/settings.json` já existir, o Archgate mescla suas configurações de forma aditiva -- entradas existentes são preservadas. + +## O que o plugin oferece + +O plugin adiciona skills baseadas em papéis ao agente de IA do VS Code. Cada skill encapsula uma parte específica do fluxo de governança, garantindo que os agentes sigam o mesmo processo todas as vezes. + +### Skills incluídas + +| Skill | Propósito | +| -------------------------- | ------------------------------------------------------------------------------------------ | +| `archgate:developer` | Agente de desenvolvimento geral que lê ADRs antes de codificar e valida depois | +| `archgate:architect` | Valida alterações de código contra todos os ADRs do projeto para conformidade estrutural | +| `archgate:quality-manager` | Revisa a cobertura de regras e propõe novos ADRs quando padrões emergem | +| `archgate:adr-author` | Cria e edita ADRs seguindo as convenções do projeto | +| `archgate:onboard` | Configuração única: explora o codebase, entrevista o desenvolvedor e cria os ADRs iniciais | + +## Configuração inicial com onboard + +Após a instalação, execute a skill `archgate:onboard` no seu projeto uma vez. Essa skill: + +1. Explora a estrutura do seu codebase (diretórios, arquivos-chave, configuração de pacotes) +2. Entrevista você sobre as convenções, restrições e decisões arquiteturais da sua equipe +3. Cria um conjunto inicial de ADRs com base nas suas respostas +4. Configura o diretório `.archgate/` com suas primeiras regras + +A skill de onboard foi projetada para ser executada uma vez por projeto. Após o onboarding, as outras skills cuidam do desenvolvimento no dia a dia. + +## Como funciona na prática + +O plugin segue um fluxo estruturado para cada tarefa de codificação: + +### 1. Ler os ADRs aplicáveis + +Quando o desenvolvedor atribui uma tarefa de codificação, o agente lê todos os ADRs que se aplicam aos arquivos sendo alterados. O servidor MCP fornece um resumo condensado com as seções **Decision** e **Do's and Don'ts** de cada ADR relevante. + +### 2. Escrever código seguindo as restrições dos ADRs + +O agente escreve código que cumpre as restrições dos ADRs. As seções Do's and Don'ts servem como guardrails concretos. + +### 3. Validar as alterações + +Após escrever o código, o agente executa `archgate check` para rodar as regras automatizadas contra as alterações. Qualquer violação é corrigida antes de prosseguir. + +### 4. Revisão do arquiteto + +O agente invoca `archgate:architect` para validar a conformidade estrutural com os ADRs além do que as regras automatizadas capturam. + +### 5. Capturar aprendizados + +O agente invoca `archgate:quality-manager` para revisar o trabalho e identificar padrões que valem ser capturados como novos ADRs. + +## Integração com o servidor MCP + +O plugin se comunica com os ADRs do seu projeto por meio do servidor MCP do Archgate. O servidor fornece: + +- **Conteúdo de ADRs** -- texto completo de qualquer ADR, acessível por ID +- **Contexto de revisão** -- resumos condensados de todos os ADRs aplicáveis a um conjunto de arquivos alterados +- **Verificação de regras** -- execução de regras automatizadas com relatório de violações +- **Listagem de ADRs** -- inventário de todos os ADRs do projeto com metadados + +O servidor MCP roda localmente e lê diretamente do seu diretório `.archgate/adrs/`. Nenhum dado sai da sua máquina. + +## Dicas + +- **Faça commit do `.vscode/settings.json`** para compartilhar a configuração do marketplace e MCP com sua equipe. +- **Execute o onboard uma vez por projeto** para gerar seus ADRs iniciais a partir do seu codebase real. +- **Mantenha os arquivos de regras dos ADRs atualizados** -- o agente aplica o que as regras verificam. diff --git a/tests/helpers/copilot-settings.test.ts b/tests/helpers/copilot-settings.test.ts new file mode 100644 index 00000000..8cc77e94 --- /dev/null +++ b/tests/helpers/copilot-settings.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, existsSync, mkdirSync } 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"], + }, + }); + }); +}); + +describe("configureCopilotSettings", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-copilot-settings-test-")); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + test("creates .github/copilot/ dir and mcp.json when nothing exists", async () => { + const mcpConfigPath = 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) + ); + + await configureCopilotSettings(tempDir); + + const content = JSON.parse( + await Bun.file(join(copilotDir, "mcp.json")).text() + ); + 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); + + expect(mcpConfigPath).toBe(join(tempDir, ".github", "copilot", "mcp.json")); + }); +}); diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts new file mode 100644 index 00000000..830cc289 --- /dev/null +++ b/tests/helpers/vscode-settings.test.ts @@ -0,0 +1,179 @@ +import { describe, expect, test, beforeEach, afterEach } from "bun:test"; +import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { + mergeVscodeSettings, + configureVscodeSettings, +} from "../../src/helpers/vscode-settings"; + +const MARKETPLACE_URL = "https://user:token@plugins.archgate.dev/archgate.git"; + +describe("mergeVscodeSettings", () => { + test("sets marketplace URL and MCP server when existing settings are empty", () => { + const result = mergeVscodeSettings({}, MARKETPLACE_URL); + + expect(result["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); + const mcp = result.mcp as Record; + const servers = mcp.servers as Record; + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); + + test("appends marketplace URL with dedup", () => { + const result = mergeVscodeSettings( + { "chat.plugins.marketplaces": ["https://other.git", MARKETPLACE_URL] }, + MARKETPLACE_URL + ); + + expect(result["chat.plugins.marketplaces"]).toEqual([ + "https://other.git", + MARKETPLACE_URL, + ]); + }); + + test("preserves existing MCP servers", () => { + const result = mergeVscodeSettings( + { + mcp: { + servers: { + "other-server": { command: "other", args: ["start"] }, + }, + }, + }, + MARKETPLACE_URL + ); + + const mcp = result.mcp as Record; + const servers = mcp.servers as Record; + expect(servers["other-server"]).toEqual({ + command: "other", + args: ["start"], + }); + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); + + test("preserves unknown top-level keys", () => { + const result = mergeVscodeSettings( + { "editor.fontSize": 14, "workbench.colorTheme": "One Dark" }, + MARKETPLACE_URL + ); + + expect(result["editor.fontSize"]).toBe(14); + expect(result["workbench.colorTheme"]).toBe("One Dark"); + }); + + test("handles non-array marketplaces gracefully", () => { + const result = mergeVscodeSettings( + { "chat.plugins.marketplaces": "not-an-array" }, + MARKETPLACE_URL + ); + + expect(result["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); + }); + + test("handles non-object mcp gracefully", () => { + const result = mergeVscodeSettings({ mcp: "invalid" }, MARKETPLACE_URL); + + const mcp = result.mcp as Record; + const servers = mcp.servers as Record; + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); + + test("handles non-object mcp.servers gracefully", () => { + const result = mergeVscodeSettings( + { mcp: { servers: "invalid" } }, + MARKETPLACE_URL + ); + + const mcp = result.mcp as Record; + const servers = mcp.servers as Record; + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); +}); + +describe("configureVscodeSettings", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-vscode-settings-test-")); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + test("creates .vscode/ dir and settings.json when neither exists", async () => { + const settingsPath = await configureVscodeSettings( + tempDir, + MARKETPLACE_URL + ); + + expect(existsSync(join(tempDir, ".vscode"))).toBe(true); + expect(existsSync(settingsPath)).toBe(true); + + const content = JSON.parse(await Bun.file(settingsPath).text()); + expect(content["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); + const servers = content.mcp.servers; + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); + + test("merges into existing settings.json without overwriting user entries", async () => { + const vscodeDir = join(tempDir, ".vscode"); + mkdirSync(vscodeDir, { recursive: true }); + + const existingSettings = { + "editor.fontSize": 14, + "chat.plugins.marketplaces": ["https://other.git"], + mcp: { + servers: { + "my-server": { command: "my-cmd", args: [] }, + }, + }, + }; + await Bun.write( + join(vscodeDir, "settings.json"), + JSON.stringify(existingSettings, null, 2) + ); + + await configureVscodeSettings(tempDir, MARKETPLACE_URL); + + const content = JSON.parse( + await Bun.file(join(vscodeDir, "settings.json")).text() + ); + expect(content["editor.fontSize"]).toBe(14); + expect(content["chat.plugins.marketplaces"]).toContain("https://other.git"); + expect(content["chat.plugins.marketplaces"]).toContain(MARKETPLACE_URL); + expect(content.mcp.servers["my-server"]).toEqual({ + command: "my-cmd", + args: [], + }); + expect(content.mcp.servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); + }); + + test("returns correct absolute path", async () => { + const settingsPath = await configureVscodeSettings( + tempDir, + MARKETPLACE_URL + ); + + expect(settingsPath).toBe(join(tempDir, ".vscode", "settings.json")); + }); +}); From e681e270ea68cd41cfcbdef12ce1aacf28b8507c Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 00:47:54 +0100 Subject: [PATCH 05/13] fix: use .vscode/mcp.json for MCP and user settings for marketplace URL VS Code uses a dedicated .vscode/mcp.json file for MCP servers (with "servers" key), not .vscode/settings.json. The chat.plugins.marketplaces setting is application-scoped and must be set in user-level settings. Changes: - Rewrite vscode-settings.ts to write .vscode/mcp.json (workspace) and user settings.json (marketplace URL) separately - Auto-detect user settings path per platform (Win/Mac/Linux) - Remove installVscodePlugin from plugin-install.ts (no longer needed) - Update docs (en + pt-br) to reflect the two-file approach - Update tests for new merge functions Co-Authored-By: Claude Opus 4.6 --- .../src/content/docs/guides/vscode-plugin.mdx | 57 ++++-- .../docs/pt-br/guides/vscode-plugin.mdx | 57 ++++-- src/commands/init.ts | 2 +- src/helpers/init-project.ts | 18 +- src/helpers/plugin-install.ts | 22 +- src/helpers/vscode-settings.ts | 178 ++++++++++------ tests/helpers/vscode-settings.test.ts | 192 +++++++++--------- 7 files changed, 312 insertions(+), 214 deletions(-) diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx index 5e0b5a3d..185915b8 100644 --- a/docs/src/content/docs/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -7,7 +7,7 @@ The Archgate VS Code plugin gives AI agents working in [VS Code](https://code.vi ## How it works -VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate.git`. When you add this marketplace to your VS Code settings, VS Code discovers and installs the plugin automatically. +VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate.git`. When you add this marketplace to your VS Code user settings, VS Code discovers and installs the plugin automatically. The plugin uses the same format as Claude Code plugins (`.claude-plugin/plugin.json`), which VS Code's agent plugin system recognizes natively. @@ -38,8 +38,16 @@ archgate init --editor vscode If you are already logged in, this command: 1. Creates the `.archgate/` governance directory (ADRs, lint rules) -2. Configures `.vscode/settings.json` with the authenticated marketplace URL -3. Registers the Archgate MCP server in `.vscode/settings.json` +2. Creates `.vscode/mcp.json` with the Archgate MCP server configuration +3. Adds the authenticated marketplace URL to your VS Code **user settings** + +The `chat.plugins.marketplaces` setting is application-scoped in VS Code, so it cannot be set per-workspace. The CLI automatically writes it to your user-level `settings.json`: + +| Platform | User settings path | +| -------- | ------------------------------------------------------- | +| Windows | `%APPDATA%\Code\User\settings.json` | +| macOS | `~/Library/Application Support/Code/User/settings.json` | +| Linux | `~/.config/Code/User/settings.json` | To explicitly request plugin installation: @@ -47,27 +55,43 @@ To explicitly request plugin installation: archgate init --editor vscode --install-plugin ``` -### Generated settings +### Generated files + +The command creates or updates the following files: + +| File | Scope | Purpose | +| -------------------- | --------- | -------------------------------------------------------- | +| `.vscode/mcp.json` | Workspace | MCP server configuration so VS Code can access your ADRs | +| User `settings.json` | User | `chat.plugins.marketplaces` with the authenticated URL | -The command adds the following to `.vscode/settings.json` (existing settings are preserved): +If `.vscode/mcp.json` already exists, Archgate merges its configuration additively -- existing server entries are preserved. The user settings file is also merged additively -- existing settings are never overwritten. + +The workspace MCP configuration (`.vscode/mcp.json`): ```json { - "chat.plugins.marketplaces": [ - "https://:@plugins.archgate.dev/archgate.git" - ], - "mcp": { - "servers": { - "archgate": { - "command": "archgate", - "args": ["mcp"] - } + "servers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] } } } ``` -If `.vscode/settings.json` already exists, Archgate merges its settings additively -- existing entries are preserved. +The user-level marketplace setting (added to your `settings.json`): + +```json +{ + "chat.plugins.marketplaces": [ + "https://:@plugins.archgate.dev/archgate.git" + ] +} +``` + +### Manual setup + +If you prefer not to let the CLI modify your user settings, you can add the marketplace URL manually. Open VS Code's user settings JSON (`Ctrl+Shift+P` → "Preferences: Open User Settings (JSON)") and add the `chat.plugins.marketplaces` entry shown above. You can find your authenticated URL by running `archgate login` and checking `~/.archgate/credentials`. ## What the plugin provides @@ -131,6 +155,7 @@ The MCP server runs locally and reads directly from your `.archgate/adrs/` direc ## Tips -- **Commit `.vscode/settings.json`** to share the marketplace and MCP configuration with your team. +- **Commit `.vscode/mcp.json`** to share the MCP configuration with your team. +- **Each developer runs `archgate init --editor vscode`** after `archgate login` to configure their user-level marketplace URL. - **Run onboard once per project** to generate your initial ADRs from your actual codebase. - **Keep ADR rule files up to date** -- the agent enforces what the rules check for. diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx index 2b2f3ae3..f068843f 100644 --- a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -7,7 +7,7 @@ O plugin Archgate para VS Code oferece aos agentes de IA que trabalham no [VS Co ## Como funciona -O VS Code suporta **plugins de agente** instalados a partir de marketplaces baseados em git. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate.git`. Quando você adiciona esse marketplace às suas configurações do VS Code, o VS Code descobre e instala o plugin automaticamente. +O VS Code suporta **plugins de agente** instalados a partir de marketplaces baseados em git. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate.git`. Quando você adiciona esse marketplace às suas configurações de usuário do VS Code, o VS Code descobre e instala o plugin automaticamente. O plugin usa o mesmo formato dos plugins do Claude Code (`.claude-plugin/plugin.json`), que o sistema de plugins de agente do VS Code reconhece nativamente. @@ -38,8 +38,16 @@ archgate init --editor vscode Se você já estiver logado, este comando: 1. Cria o diretório de governança `.archgate/` (ADRs, regras de lint) -2. Configura `.vscode/settings.json` com a URL autenticada do marketplace -3. Registra o servidor MCP do Archgate em `.vscode/settings.json` +2. Cria `.vscode/mcp.json` com a configuração do servidor MCP do Archgate +3. Adiciona a URL autenticada do marketplace nas suas **configurações de usuário** do VS Code + +A configuração `chat.plugins.marketplaces` tem escopo de aplicação no VS Code, então não pode ser definida por workspace. O CLI escreve automaticamente no seu `settings.json` de nível de usuário: + +| Plataforma | Caminho das configurações de usuário | +| ---------- | ------------------------------------------------------- | +| Windows | `%APPDATA%\Code\User\settings.json` | +| macOS | `~/Library/Application Support/Code/User/settings.json` | +| Linux | `~/.config/Code/User/settings.json` | Para solicitar explicitamente a instalação do plugin: @@ -47,27 +55,43 @@ Para solicitar explicitamente a instalação do plugin: archgate init --editor vscode --install-plugin ``` -### Configurações geradas +### Arquivos gerados + +O comando cria ou atualiza os seguintes arquivos: + +| Arquivo | Escopo | Propósito | +| -------------------------- | --------- | ------------------------------------------------------------------- | +| `.vscode/mcp.json` | Workspace | Configuração do servidor MCP para que o VS Code acesse os seus ADRs | +| `settings.json` do usuário | Usuário | `chat.plugins.marketplaces` com a URL autenticada | -O comando adiciona o seguinte ao `.vscode/settings.json` (configurações existentes são preservadas): +Se `.vscode/mcp.json` já existir, o Archgate mescla sua configuração de forma aditiva -- entradas de servidores existentes são preservadas. O arquivo de configurações de usuário também é mesclado de forma aditiva -- configurações existentes nunca são sobrescritas. + +A configuração MCP do workspace (`.vscode/mcp.json`): ```json { - "chat.plugins.marketplaces": [ - "https://:@plugins.archgate.dev/archgate.git" - ], - "mcp": { - "servers": { - "archgate": { - "command": "archgate", - "args": ["mcp"] - } + "servers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] } } } ``` -Se `.vscode/settings.json` já existir, o Archgate mescla suas configurações de forma aditiva -- entradas existentes são preservadas. +A configuração do marketplace no nível de usuário (adicionada ao seu `settings.json`): + +```json +{ + "chat.plugins.marketplaces": [ + "https://:@plugins.archgate.dev/archgate.git" + ] +} +``` + +### Configuração manual + +Se preferir não deixar o CLI modificar suas configurações de usuário, você pode adicionar a URL do marketplace manualmente. Abra o JSON de configurações de usuário do VS Code (`Ctrl+Shift+P` → "Preferences: Open User Settings (JSON)") e adicione a entrada `chat.plugins.marketplaces` mostrada acima. Você pode encontrar sua URL autenticada executando `archgate login` e verificando `~/.archgate/credentials`. ## O que o plugin oferece @@ -131,6 +155,7 @@ O servidor MCP roda localmente e lê diretamente do seu diretório `.archgate/ad ## Dicas -- **Faça commit do `.vscode/settings.json`** para compartilhar a configuração do marketplace e MCP com sua equipe. +- **Faça commit do `.vscode/mcp.json`** para compartilhar a configuração MCP com sua equipe. +- **Cada desenvolvedor executa `archgate init --editor vscode`** após `archgate login` para configurar a URL do marketplace no nível de usuário. - **Execute o onboard uma vez por projeto** para gerar seus ADRs iniciais a partir do seu codebase real. - **Mantenha os arquivos de regras dos ADRs atualizados** -- o agente aplica o que as regras verificam. diff --git a/src/commands/init.ts b/src/commands/init.ts index 874b383c..50bfce12 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -107,7 +107,7 @@ function printManualInstructions(editor: EditorTarget, detail?: string): void { console.log(` ${styleText("bold", "copilot plugin install")} ${detail}`); break; default: - // vscode and cursor auto-install always — should not reach here + // cursor auto-installs always — should not reach here break; } } diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 4201feb9..878fbfa8 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -116,15 +116,13 @@ async function configureEditorSettings( case "cursor": return configureCursorSettings(projectRoot); case "vscode": { - // VS Code needs the marketplace URL for settings — use a placeholder - // that gets replaced during plugin install if credentials exist. + // VS Code: .vscode/mcp.json always, marketplace URL to user settings if logged in const { loadCredentials } = await import("./auth"); const creds = await loadCredentials(); - const { buildMarketplaceUrl } = await import("./plugin-install"); - const url = creds - ? buildMarketplaceUrl(creds) - : "https://plugins.archgate.dev/archgate.git"; - return configureVscodeSettings(projectRoot, url); + const marketplaceUrl = creds + ? (await import("./plugin-install")).buildMarketplaceUrl(creds) + : undefined; + return configureVscodeSettings(projectRoot, marketplaceUrl); } case "copilot": return configureCopilotSettings(projectRoot); @@ -158,12 +156,12 @@ async function tryInstallPlugin( } if (editor === "vscode") { - const { installVscodePlugin } = await import("./plugin-install"); - await installVscodePlugin(projectRoot, credentials); + // VS Code marketplace URL is already added to user settings by configureEditorSettings. + // The --install-plugin flag is a no-op for VS Code since init handles everything. return { installed: true, autoInstalled: true, - detail: "Configured marketplace URL in .vscode/settings.json", + detail: "Marketplace URL added to VS Code user settings", }; } diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index 42bacfc8..e37fa3c9 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -2,7 +2,7 @@ * plugin-install.ts — Download and install the archgate plugin for supported editors. * * - Claude Code: auto-installs via `claude` CLI, or prints manual commands as fallback - * - VS Code: configures .vscode/settings.json with marketplace URL (git-based plugin) + * - VS Code: marketplace URL for manual user-settings configuration (application-scoped) * - Copilot CLI: auto-installs via `copilot` CLI, or prints manual commands as fallback * - Cursor: downloads cursor.tar.gz from the plugins service and extracts it */ @@ -137,26 +137,6 @@ export async function installCursorPlugin( return extractedFiles; } -// --------------------------------------------------------------------------- -// VS Code — configure marketplace URL in .vscode/settings.json -// --------------------------------------------------------------------------- - -/** - * Install the archgate plugin for VS Code by configuring the marketplace URL. - * - * VS Code agent plugins are installed from git-based marketplaces. This adds the - * authenticated marketplace URL to `.vscode/settings.json` so VS Code can discover - * and install the plugin automatically. - */ -export async function installVscodePlugin( - projectRoot: string, - credentials: StoredCredentials -): Promise { - const { configureVscodeSettings } = await import("./vscode-settings"); - const url = buildMarketplaceUrl(credentials); - return configureVscodeSettings(projectRoot, url); -} - // --------------------------------------------------------------------------- // Copilot CLI — CLI auto-install + manual fallback // --------------------------------------------------------------------------- diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index f468e93e..818e96e2 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -1,26 +1,51 @@ import { join } from "node:path"; import { existsSync, mkdirSync } from "node:fs"; +import { homedir } from "node:os"; /** - * VS Code settings that archgate injects into .vscode/settings.json. + * MCP server configuration that archgate injects into .vscode/mcp.json. * - * - `chat.plugins.marketplaces`: registers the archgate git marketplace so - * VS Code's agent plugin system can discover and install the plugin. - * - MCP server configuration for the archgate governance tools. + * 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_SETTINGS = { - "chat.plugins.marketplaces": [] as string[], - mcp: { - servers: { - archgate: { - command: "archgate", - args: ["mcp"], - }, +export const ARCHGATE_VSCODE_MCP_CONFIG = { + servers: { + archgate: { + command: "archgate", + args: ["mcp"], }, }, } as const; -type VscodeSettings = Record; +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, + }, + }; +} /** * Deduplicate an array of strings while preserving order. @@ -30,84 +55,121 @@ function dedup(arr: string[]): string[] { } /** - * Pure, additive merge of archgate settings into existing VS Code settings. - * - * - `chat.plugins.marketplaces`: append marketplace URL with dedup - * - `mcp.servers`: add archgate server, preserve existing servers - * - All existing user settings are preserved (unknown keys pass through) + * Add a marketplace URL to the `chat.plugins.marketplaces` array in a VS Code + * user settings object. Preserves all other settings. Deduplicates URLs. */ -export function mergeVscodeSettings( - existing: VscodeSettings, +export function mergeMarketplaceUrl( + existing: VscodeUserSettings, marketplaceUrl: string -): VscodeSettings { - const merged: VscodeSettings = { ...existing }; +): VscodeUserSettings { + const merged: VscodeUserSettings = { ...existing }; - // Marketplace URLs: append with dedup const existingMarketplaces = Array.isArray( merged["chat.plugins.marketplaces"] ) ? (merged["chat.plugins.marketplaces"] as string[]) : []; + merged["chat.plugins.marketplaces"] = dedup([ ...existingMarketplaces, marketplaceUrl, ]); - // MCP servers: additive merge - const existingMcp = - typeof merged.mcp === "object" && - merged.mcp !== null && - !Array.isArray(merged.mcp) - ? (merged.mcp as Record) - : {}; - - const existingServers = - typeof existingMcp.servers === "object" && - existingMcp.servers !== null && - !Array.isArray(existingMcp.servers) - ? (existingMcp.servers as Record) - : {}; - - merged.mcp = { - ...existingMcp, - servers: { - ...existingServers, - ...ARCHGATE_VSCODE_SETTINGS.mcp.servers, - }, - }; - return merged; } +/** + * Resolve the path to VS Code's user-level settings.json. + * + * - Windows: %APPDATA%/Code/User/settings.json + * - macOS: ~/Library/Application Support/Code/User/settings.json + * - Linux: ~/.config/Code/User/settings.json + */ +export function getVscodeUserSettingsPath(): string { + const platform = process.platform; + if (platform === "win32") { + const appData = + process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"); + return join(appData, "Code", "User", "settings.json"); + } + if (platform === "darwin") { + return join( + homedir(), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ); + } + // Linux and others + return join(homedir(), ".config", "Code", "User", "settings.json"); +} + /** * Configure VS Code settings for archgate integration. * - * Reads existing `.vscode/settings.json` (if any), merges archgate - * settings additively (marketplace URL + MCP server), and writes the result. + * 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). * - * @returns Absolute path to the settings file. + * @returns Absolute path to the workspace MCP config file. */ export async function configureVscodeSettings( projectRoot: string, - marketplaceUrl: string + marketplaceUrl?: string ): Promise { const vscodeDir = join(projectRoot, ".vscode"); - const settingsPath = join(vscodeDir, "settings.json"); + const mcpConfigPath = join(vscodeDir, "mcp.json"); - // Read existing settings or start with empty object - let existing: VscodeSettings = {}; - if (existsSync(settingsPath)) { - const content = await Bun.file(settingsPath).text(); - existing = JSON.parse(content) as VscodeSettings; + // --- Workspace: .vscode/mcp.json --- + let existing: VscodeMcpConfig = {}; + if (existsSync(mcpConfigPath)) { + const content = await Bun.file(mcpConfigPath).text(); + existing = JSON.parse(content) as VscodeMcpConfig; } - const merged = mergeVscodeSettings(existing, marketplaceUrl); + const merged = mergeVscodeMcpConfig(existing, ARCHGATE_VSCODE_MCP_CONFIG); - // Ensure .vscode/ directory exists 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; +} + +/** + * Add the marketplace URL to VS Code's user-level settings.json. + * + * Reads the existing file (if any), merges the URL into + * `chat.plugins.marketplaces`, and writes back. Creates parent + * directories if they don't exist. + */ +export async function addMarketplaceToUserSettings( + marketplaceUrl: string +): Promise { + const settingsPath = getVscodeUserSettingsPath(); + const settingsDir = join(settingsPath, ".."); + + let existing: VscodeUserSettings = {}; + if (existsSync(settingsPath)) { + const content = await Bun.file(settingsPath).text(); + existing = JSON.parse(content) as VscodeUserSettings; + } + + const merged = mergeMarketplaceUrl(existing, marketplaceUrl); + + if (!existsSync(settingsDir)) { + mkdirSync(settingsDir, { recursive: true }); + } + await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + "\n"); return settingsPath; diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index 830cc289..07de3673 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -3,51 +3,38 @@ import { mkdtempSync, rmSync, existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { - mergeVscodeSettings, + ARCHGATE_VSCODE_MCP_CONFIG, + mergeVscodeMcpConfig, + mergeMarketplaceUrl, configureVscodeSettings, } from "../../src/helpers/vscode-settings"; -const MARKETPLACE_URL = "https://user:token@plugins.archgate.dev/archgate.git"; +describe("mergeVscodeMcpConfig", () => { + test("sets archgate server when existing config is empty", () => { + const result = mergeVscodeMcpConfig({}, ARCHGATE_VSCODE_MCP_CONFIG); -describe("mergeVscodeSettings", () => { - test("sets marketplace URL and MCP server when existing settings are empty", () => { - const result = mergeVscodeSettings({}, MARKETPLACE_URL); - - expect(result["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); - const mcp = result.mcp as Record; - const servers = mcp.servers as Record; - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], + expect(result.servers).toEqual({ + archgate: { + command: "archgate", + args: ["mcp"], + }, }); }); - test("appends marketplace URL with dedup", () => { - const result = mergeVscodeSettings( - { "chat.plugins.marketplaces": ["https://other.git", MARKETPLACE_URL] }, - MARKETPLACE_URL - ); - - expect(result["chat.plugins.marketplaces"]).toEqual([ - "https://other.git", - MARKETPLACE_URL, - ]); - }); - test("preserves existing MCP servers", () => { - const result = mergeVscodeSettings( + const result = mergeVscodeMcpConfig( { - mcp: { - servers: { - "other-server": { command: "other", args: ["start"] }, + servers: { + "other-server": { + command: "other", + args: ["start"], }, }, }, - MARKETPLACE_URL + ARCHGATE_VSCODE_MCP_CONFIG ); - const mcp = result.mcp as Record; - const servers = mcp.servers as Record; + const servers = result.servers as Record; expect(servers["other-server"]).toEqual({ command: "other", args: ["start"], @@ -58,48 +45,84 @@ describe("mergeVscodeSettings", () => { }); }); - test("preserves unknown top-level keys", () => { - const result = mergeVscodeSettings( - { "editor.fontSize": 14, "workbench.colorTheme": "One Dark" }, - MARKETPLACE_URL + test("overwrites existing archgate server entry", () => { + const result = mergeVscodeMcpConfig( + { + servers: { + archgate: { + command: "old-command", + args: ["old"], + }, + }, + }, + ARCHGATE_VSCODE_MCP_CONFIG ); - expect(result["editor.fontSize"]).toBe(14); - expect(result["workbench.colorTheme"]).toBe("One Dark"); + const servers = result.servers as Record; + expect(servers.archgate).toEqual({ + command: "archgate", + args: ["mcp"], + }); }); - test("handles non-array marketplaces gracefully", () => { - const result = mergeVscodeSettings( - { "chat.plugins.marketplaces": "not-an-array" }, - MARKETPLACE_URL + test("preserves unknown top-level keys", () => { + const result = mergeVscodeMcpConfig( + { inputs: [{ type: "promptString", id: "key" }] }, + ARCHGATE_VSCODE_MCP_CONFIG ); - expect(result["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); + expect(result.inputs).toEqual([{ type: "promptString", id: "key" }]); }); - test("handles non-object mcp gracefully", () => { - const result = mergeVscodeSettings({ mcp: "invalid" }, MARKETPLACE_URL); + test("handles non-object servers gracefully", () => { + const result = mergeVscodeMcpConfig( + { servers: "invalid" }, + ARCHGATE_VSCODE_MCP_CONFIG + ); - const mcp = result.mcp as Record; - const servers = mcp.servers as Record; - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], + expect(result.servers).toEqual({ + archgate: { + command: "archgate", + args: ["mcp"], + }, }); }); +}); + +describe("mergeMarketplaceUrl", () => { + const URL = "https://user:token@plugins.archgate.dev/archgate.git"; + + test("adds marketplace URL to empty settings", () => { + const result = mergeMarketplaceUrl({}, URL); + expect(result["chat.plugins.marketplaces"]).toEqual([URL]); + }); - test("handles non-object mcp.servers gracefully", () => { - const result = mergeVscodeSettings( - { mcp: { servers: "invalid" } }, - MARKETPLACE_URL + test("appends URL with dedup", () => { + const result = mergeMarketplaceUrl( + { "chat.plugins.marketplaces": ["https://other.git", URL] }, + URL ); + expect(result["chat.plugins.marketplaces"]).toEqual([ + "https://other.git", + URL, + ]); + }); - const mcp = result.mcp as Record; - const servers = mcp.servers as Record; - expect(servers.archgate).toEqual({ - command: "archgate", - args: ["mcp"], - }); + test("handles non-array marketplaces gracefully", () => { + const result = mergeMarketplaceUrl( + { "chat.plugins.marketplaces": "not-an-array" }, + URL + ); + expect(result["chat.plugins.marketplaces"]).toEqual([URL]); + }); + + test("preserves other settings keys", () => { + const result = mergeMarketplaceUrl( + { "editor.fontSize": 14, "workbench.colorTheme": "One Dark" }, + URL + ); + expect(result["editor.fontSize"]).toBe(14); + expect(result["workbench.colorTheme"]).toBe("One Dark"); }); }); @@ -114,66 +137,51 @@ describe("configureVscodeSettings", () => { rmSync(tempDir, { recursive: true, force: true }); }); - test("creates .vscode/ dir and settings.json when neither exists", async () => { - const settingsPath = await configureVscodeSettings( - tempDir, - MARKETPLACE_URL - ); + 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(settingsPath)).toBe(true); + expect(existsSync(mcpConfigPath)).toBe(true); - const content = JSON.parse(await Bun.file(settingsPath).text()); - expect(content["chat.plugins.marketplaces"]).toEqual([MARKETPLACE_URL]); - const servers = content.mcp.servers; - expect(servers.archgate).toEqual({ + const content = JSON.parse(await Bun.file(mcpConfigPath).text()); + expect(content.servers.archgate).toEqual({ command: "archgate", args: ["mcp"], }); }); - test("merges into existing settings.json without overwriting user entries", async () => { + test("merges into existing mcp.json without overwriting other servers", async () => { const vscodeDir = join(tempDir, ".vscode"); mkdirSync(vscodeDir, { recursive: true }); - const existingSettings = { - "editor.fontSize": 14, - "chat.plugins.marketplaces": ["https://other.git"], - mcp: { - servers: { - "my-server": { command: "my-cmd", args: [] }, - }, + const existingConfig = { + servers: { + "my-server": { command: "my-cmd", args: [] }, }, }; await Bun.write( - join(vscodeDir, "settings.json"), - JSON.stringify(existingSettings, null, 2) + join(vscodeDir, "mcp.json"), + JSON.stringify(existingConfig, null, 2) ); - await configureVscodeSettings(tempDir, MARKETPLACE_URL); + await configureVscodeSettings(tempDir); const content = JSON.parse( - await Bun.file(join(vscodeDir, "settings.json")).text() + await Bun.file(join(vscodeDir, "mcp.json")).text() ); - expect(content["editor.fontSize"]).toBe(14); - expect(content["chat.plugins.marketplaces"]).toContain("https://other.git"); - expect(content["chat.plugins.marketplaces"]).toContain(MARKETPLACE_URL); - expect(content.mcp.servers["my-server"]).toEqual({ + expect(content.servers["my-server"]).toEqual({ command: "my-cmd", args: [], }); - expect(content.mcp.servers.archgate).toEqual({ + expect(content.servers.archgate).toEqual({ command: "archgate", args: ["mcp"], }); }); - test("returns correct absolute path", async () => { - const settingsPath = await configureVscodeSettings( - tempDir, - MARKETPLACE_URL - ); + test("returns correct absolute path to mcp.json", async () => { + const mcpConfigPath = await configureVscodeSettings(tempDir); - expect(settingsPath).toBe(join(tempDir, ".vscode", "settings.json")); + expect(mcpConfigPath).toBe(join(tempDir, ".vscode", "mcp.json")); }); }); From 93c55cb836d2985242b2cbfbc78d73f0ec8bb722 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 00:52:11 +0100 Subject: [PATCH 06/13] docs: add VS Code 1.99+ version requirement notice Agent plugins require VS Code February 2026 release (1.99) or later. Added caution callout to both English and pt-br guide pages. Co-Authored-By: Claude Opus 4.6 --- docs/src/content/docs/guides/vscode-plugin.mdx | 4 ++++ docs/src/content/docs/pt-br/guides/vscode-plugin.mdx | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx index 185915b8..6b313d27 100644 --- a/docs/src/content/docs/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -13,6 +13,10 @@ The plugin uses the same format as Claude Code plugins (`.claude-plugin/plugin.j ## Installation +:::caution[VS Code version requirement] +Agent plugins require **VS Code 1.99 (February 2026 release) or later**. Earlier versions do not support git-based agent plugin marketplaces. Check your version with `code --version`. +::: + :::note[Beta access required] The VS Code plugin is currently in beta. [Sign up at plugins.archgate.dev](https://plugins.archgate.dev) to get access before following the steps below. ::: diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx index f068843f..f792df17 100644 --- a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -13,6 +13,10 @@ O plugin usa o mesmo formato dos plugins do Claude Code (`.claude-plugin/plugin. ## Instalação +:::caution[Versão do VS Code necessária] +Plugins de agente requerem **VS Code 1.99 (lançamento de fevereiro de 2026) ou posterior**. Versões anteriores não suportam marketplaces de plugins de agente baseados em git. Verifique sua versão com `code --version`. +::: + :::note[Acesso beta necessário] O plugin para VS Code está atualmente em beta. [Cadastre-se em plugins.archgate.dev](https://plugins.archgate.dev) para obter acesso antes de seguir os passos abaixo. ::: From 14b4dd1bc510cdd6e3a1fa5857fb293c38c2de87 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 00:54:20 +0100 Subject: [PATCH 07/13] fix(docs): correct VS Code version to 1.110 Co-Authored-By: Claude Opus 4.6 --- docs/src/content/docs/guides/vscode-plugin.mdx | 2 +- docs/src/content/docs/pt-br/guides/vscode-plugin.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx index 6b313d27..3f7ba1db 100644 --- a/docs/src/content/docs/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -14,7 +14,7 @@ The plugin uses the same format as Claude Code plugins (`.claude-plugin/plugin.j ## Installation :::caution[VS Code version requirement] -Agent plugins require **VS Code 1.99 (February 2026 release) or later**. Earlier versions do not support git-based agent plugin marketplaces. Check your version with `code --version`. +Agent plugins require **VS Code 1.110 (February 2026 release) or later**. Earlier versions do not support git-based agent plugin marketplaces. Check your version with `code --version`. ::: :::note[Beta access required] diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx index f792df17..066d3498 100644 --- a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -14,7 +14,7 @@ O plugin usa o mesmo formato dos plugins do Claude Code (`.claude-plugin/plugin. ## Instalação :::caution[Versão do VS Code necessária] -Plugins de agente requerem **VS Code 1.99 (lançamento de fevereiro de 2026) ou posterior**. Versões anteriores não suportam marketplaces de plugins de agente baseados em git. Verifique sua versão com `code --version`. +Plugins de agente requerem **VS Code 1.110 (lançamento de fevereiro de 2026) ou posterior**. Versões anteriores não suportam marketplaces de plugins de agente baseados em git. Verifique sua versão com `code --version`. ::: :::note[Acesso beta necessário] From 9946561306d6a34bdb81c0f9342ea3610d148300 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 01:08:54 +0100 Subject: [PATCH 08/13] fix: use Bun.JSONC.parse for VS Code config files VS Code settings.json and mcp.json use JSONC (JSON with Comments) which includes single-line comments, block comments, and trailing commas. JSON.parse() fails on these files. Use Bun's built-in JSONC parser instead. Co-Authored-By: Claude Opus 4.6 --- src/helpers/vscode-settings.ts | 4 ++-- tests/helpers/vscode-settings.test.ts | 28 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index 818e96e2..e7d9b41a 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -126,7 +126,7 @@ export async function configureVscodeSettings( let existing: VscodeMcpConfig = {}; if (existsSync(mcpConfigPath)) { const content = await Bun.file(mcpConfigPath).text(); - existing = JSON.parse(content) as VscodeMcpConfig; + existing = Bun.JSONC.parse(content) as VscodeMcpConfig; } const merged = mergeVscodeMcpConfig(existing, ARCHGATE_VSCODE_MCP_CONFIG); @@ -161,7 +161,7 @@ export async function addMarketplaceToUserSettings( let existing: VscodeUserSettings = {}; if (existsSync(settingsPath)) { const content = await Bun.file(settingsPath).text(); - existing = JSON.parse(content) as VscodeUserSettings; + existing = Bun.JSONC.parse(content) as VscodeUserSettings; } const merged = mergeMarketplaceUrl(existing, marketplaceUrl); diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index 07de3673..8ec11321 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -179,6 +179,34 @@ describe("configureVscodeSettings", () => { }); }); + 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); + + 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"], + }); + }); + test("returns correct absolute path to mcp.json", async () => { const mcpConfigPath = await configureVscodeSettings(tempDir); From c8a6a77fcb124b17f0d591bc50c78a7984417995 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 01:11:52 +0100 Subject: [PATCH 09/13] fix: properly merge VS Code user settings with Bun.JSONC.parse Use Bun.JSONC.parse to read existing user settings.json (supports comments and trailing commas), merge the marketplace URL additively, and write back. All existing settings data is preserved. Co-Authored-By: Claude Opus 4.6 --- src/helpers/vscode-settings.ts | 7 +-- tests/helpers/vscode-settings.test.ts | 72 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index e7d9b41a..a7deccc8 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -148,9 +148,10 @@ export async function configureVscodeSettings( /** * Add the marketplace URL to VS Code's user-level settings.json. * - * Reads the existing file (if any), merges the URL into - * `chat.plugins.marketplaces`, and writes back. Creates parent - * directories if they don't exist. + * Reads the existing file with `Bun.JSONC.parse` (supports comments and + * trailing commas), merges the marketplace URL, and writes back as standard + * JSON. Comments in the original file are not preserved — VS Code re-reads + * the file without issue. */ export async function addMarketplaceToUserSettings( marketplaceUrl: string diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index 8ec11321..084ce542 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -7,6 +7,7 @@ import { mergeVscodeMcpConfig, mergeMarketplaceUrl, configureVscodeSettings, + addMarketplaceToUserSettings, } from "../../src/helpers/vscode-settings"; describe("mergeVscodeMcpConfig", () => { @@ -213,3 +214,74 @@ describe("configureVscodeSettings", () => { expect(mcpConfigPath).toBe(join(tempDir, ".vscode", "mcp.json")); }); }); + +describe("addMarketplaceToUserSettings", () => { + let tempDir: string; + let originalEnv: string | undefined; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-user-settings-test-")); + originalEnv = process.env.APPDATA; + process.env.APPDATA = tempDir; + }); + + afterEach(() => { + process.env.APPDATA = originalEnv; + rmSync(tempDir, { recursive: true, force: true }); + }); + + const URL = "https://user:token@plugins.archgate.dev/archgate.git"; + + function settingsPath() { + return join(tempDir, "Code", "User", "settings.json"); + } + + test("creates settings file when none exists", async () => { + await addMarketplaceToUserSettings(URL); + + const content = JSON.parse(await Bun.file(settingsPath()).text()); + expect(content["chat.plugins.marketplaces"]).toEqual([URL]); + }); + + test("merges JSONC settings with trailing commas without losing data", async () => { + const dir = join(tempDir, "Code", "User"); + mkdirSync(dir, { recursive: true }); + + const original = `{ + "security.allowedUNCHosts": ["wsl.localhost"], + "git.autofetch": true, + "chat.mcp.gallery.enabled": true, +}`; + await Bun.write(settingsPath(), original); + + await addMarketplaceToUserSettings(URL); + + const content = JSON.parse(await Bun.file(settingsPath()).text()); + expect(content["git.autofetch"]).toBe(true); + expect(content["chat.mcp.gallery.enabled"]).toBe(true); + expect(content["security.allowedUNCHosts"]).toEqual(["wsl.localhost"]); + expect(content["chat.plugins.marketplaces"]).toEqual([URL]); + }); + + test("deduplicates marketplace URLs", async () => { + const dir = join(tempDir, "Code", "User"); + mkdirSync(dir, { recursive: true }); + + await Bun.write( + settingsPath(), + JSON.stringify( + { "chat.plugins.marketplaces": ["https://other.git", URL] }, + null, + 2 + ) + ); + + await addMarketplaceToUserSettings(URL); + + const content = JSON.parse(await Bun.file(settingsPath()).text()); + expect(content["chat.plugins.marketplaces"]).toEqual([ + "https://other.git", + URL, + ]); + }); +}); From c98524c22d9f50b586103fde5c5d9952ce8a6d11 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 01:19:59 +0100 Subject: [PATCH 10/13] fix: preserve VS Code default marketplaces when setting key is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code has built-in defaults for chat.plugins.marketplaces (github/copilot-plugins, github/awesome-copilot) that are implicit — not written to settings.json. When we explicitly set the key, VS Code stops using its defaults. Now we seed the array with these defaults when the key is absent from the file, preventing silent override. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 3 ++ src/helpers/vscode-settings.ts | 27 +++++++++-- tests/helpers/vscode-settings.test.ts | 64 +++++++++++++++------------ 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0f57a031..25abac1b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -78,6 +78,9 @@ Bun 1.3.8, Moon 1.39.4, Node LTS, npm 11.6.0. Minimum user-facing Bun: `>=1.2.21 - `ARCH-004` — No barrel files (direct imports only) - `ARCH-005` — Testing standards (Bun test, fixtures, 80% coverage) - `ARCH-006` — Dependency policy (minimal deps, Bun built-ins) +- `ARCH-007` — Cross-platform subprocess execution (Bun.spawn, no Bun.$) +- `GEN-001` — Documentation site (Astro Starlight) +- `GEN-002` — Documentation internationalization (en + pt-br parity) ## ADR Format diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts index a7deccc8..7bf8a773 100644 --- a/src/helpers/vscode-settings.ts +++ b/src/helpers/vscode-settings.ts @@ -47,6 +47,18 @@ export function mergeVscodeMcpConfig( }; } +/** + * VS Code's built-in default marketplaces for `chat.plugins.marketplaces`. + * + * These are implicit defaults — VS Code uses them when the setting is absent + * from the user's settings.json. Once we explicitly set the key, VS Code stops + * using its defaults, so we must include them to avoid losing them. + */ +const VSCODE_DEFAULT_MARKETPLACES = [ + "github/copilot-plugins", + "github/awesome-copilot", +]; + /** * Deduplicate an array of strings while preserving order. */ @@ -57,6 +69,9 @@ function dedup(arr: string[]): string[] { /** * Add a marketplace URL to the `chat.plugins.marketplaces` array in a VS Code * user settings object. Preserves all other settings. Deduplicates URLs. + * + * When `chat.plugins.marketplaces` is not yet in the file, VS Code's built-in + * defaults are included so they are not lost when we explicitly set the key. */ export function mergeMarketplaceUrl( existing: VscodeUserSettings, @@ -64,16 +79,20 @@ export function mergeMarketplaceUrl( ): VscodeUserSettings { const merged: VscodeUserSettings = { ...existing }; + const hasExplicitMarketplaces = "chat.plugins.marketplaces" in existing; const existingMarketplaces = Array.isArray( merged["chat.plugins.marketplaces"] ) ? (merged["chat.plugins.marketplaces"] as string[]) : []; - merged["chat.plugins.marketplaces"] = dedup([ - ...existingMarketplaces, - marketplaceUrl, - ]); + // When the key is absent, seed with VS Code's built-in defaults so we don't + // silently override them by setting the key explicitly. + const base = hasExplicitMarketplaces + ? existingMarketplaces + : [...VSCODE_DEFAULT_MARKETPLACES, ...existingMarketplaces]; + + merged["chat.plugins.marketplaces"] = dedup([...base, marketplaceUrl]); return merged; } diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index 084ce542..e6f5310b 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -93,12 +93,16 @@ describe("mergeVscodeMcpConfig", () => { describe("mergeMarketplaceUrl", () => { const URL = "https://user:token@plugins.archgate.dev/archgate.git"; - test("adds marketplace URL to empty settings", () => { + test("includes VS Code defaults when key is absent", () => { const result = mergeMarketplaceUrl({}, URL); - expect(result["chat.plugins.marketplaces"]).toEqual([URL]); + expect(result["chat.plugins.marketplaces"]).toEqual([ + "github/copilot-plugins", + "github/awesome-copilot", + URL, + ]); }); - test("appends URL with dedup", () => { + test("appends URL with dedup when key already exists", () => { const result = mergeMarketplaceUrl( { "chat.plugins.marketplaces": ["https://other.git", URL] }, URL @@ -109,21 +113,24 @@ describe("mergeMarketplaceUrl", () => { ]); }); - test("handles non-array marketplaces gracefully", () => { + test("does not re-add defaults when key is explicitly set", () => { const result = mergeMarketplaceUrl( - { "chat.plugins.marketplaces": "not-an-array" }, + { "chat.plugins.marketplaces": ["https://custom.git"] }, URL ); - expect(result["chat.plugins.marketplaces"]).toEqual([URL]); + expect(result["chat.plugins.marketplaces"]).toEqual([ + "https://custom.git", + URL, + ]); }); - test("preserves other settings keys", () => { + test("handles non-array marketplaces gracefully", () => { const result = mergeMarketplaceUrl( - { "editor.fontSize": 14, "workbench.colorTheme": "One Dark" }, + { "chat.plugins.marketplaces": "not-an-array", "editor.fontSize": 14 }, URL ); + expect(result["chat.plugins.marketplaces"]).toEqual([URL]); expect(result["editor.fontSize"]).toBe(14); - expect(result["workbench.colorTheme"]).toBe("One Dark"); }); }); @@ -236,44 +243,45 @@ describe("addMarketplaceToUserSettings", () => { return join(tempDir, "Code", "User", "settings.json"); } - test("creates settings file when none exists", async () => { + test("creates settings file with defaults when none exists", async () => { await addMarketplaceToUserSettings(URL); const content = JSON.parse(await Bun.file(settingsPath()).text()); - expect(content["chat.plugins.marketplaces"]).toEqual([URL]); + expect(content["chat.plugins.marketplaces"]).toEqual([ + "github/copilot-plugins", + "github/awesome-copilot", + URL, + ]); }); - test("merges JSONC settings with trailing commas without losing data", async () => { + test("merges JSONC settings and includes defaults when key absent", async () => { const dir = join(tempDir, "Code", "User"); mkdirSync(dir, { recursive: true }); - - const original = `{ - "security.allowedUNCHosts": ["wsl.localhost"], - "git.autofetch": true, - "chat.mcp.gallery.enabled": true, -}`; - await Bun.write(settingsPath(), original); + await Bun.write( + settingsPath(), + `{ "git.autofetch": true, "chat.mcp.gallery.enabled": true, }` + ); await addMarketplaceToUserSettings(URL); const content = JSON.parse(await Bun.file(settingsPath()).text()); expect(content["git.autofetch"]).toBe(true); expect(content["chat.mcp.gallery.enabled"]).toBe(true); - expect(content["security.allowedUNCHosts"]).toEqual(["wsl.localhost"]); - expect(content["chat.plugins.marketplaces"]).toEqual([URL]); + expect(content["chat.plugins.marketplaces"]).toEqual([ + "github/copilot-plugins", + "github/awesome-copilot", + URL, + ]); }); - test("deduplicates marketplace URLs", async () => { + test("deduplicates when key already exists", async () => { const dir = join(tempDir, "Code", "User"); mkdirSync(dir, { recursive: true }); - await Bun.write( settingsPath(), - JSON.stringify( - { "chat.plugins.marketplaces": ["https://other.git", URL] }, - null, - 2 - ) + JSON.stringify({ + "chat.plugins.marketplaces": ["https://other.git", URL], + }) ); await addMarketplaceToUserSettings(URL); From 11e003a9990cbae2686ff6eefa6c59ffbcb229da Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Thu, 5 Mar 2026 01:23:35 +0100 Subject: [PATCH 11/13] fix: make vscode-settings tests cross-platform for CI Override both APPDATA (Windows) and HOME (macOS/Linux) in tests so getVscodeUserSettingsPath() resolves into the temp directory on all platforms. Use the real path resolver in tests instead of hardcoding Windows-specific paths. Co-Authored-By: Claude Opus 4.6 --- .vscode/mcp.json | 8 ++++++++ tests/helpers/vscode-settings.test.ts | 21 +++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 .vscode/mcp.json diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 00000000..55daa16c --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } +} diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts index e6f5310b..5c04ca90 100644 --- a/tests/helpers/vscode-settings.test.ts +++ b/tests/helpers/vscode-settings.test.ts @@ -8,6 +8,7 @@ import { mergeMarketplaceUrl, configureVscodeSettings, addMarketplaceToUserSettings, + getVscodeUserSettingsPath, } from "../../src/helpers/vscode-settings"; describe("mergeVscodeMcpConfig", () => { @@ -224,23 +225,26 @@ describe("configureVscodeSettings", () => { describe("addMarketplaceToUserSettings", () => { let tempDir: string; - let originalEnv: string | undefined; + let savedEnv: Record; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "archgate-user-settings-test-")); - originalEnv = process.env.APPDATA; - process.env.APPDATA = tempDir; + // Save and override env so getVscodeUserSettingsPath() resolves into tempDir + savedEnv = { APPDATA: process.env.APPDATA, HOME: process.env.HOME }; + process.env.APPDATA = tempDir; // Windows + process.env.HOME = tempDir; // macOS/Linux (homedir()) }); afterEach(() => { - process.env.APPDATA = originalEnv; + Object.assign(process.env, savedEnv); rmSync(tempDir, { recursive: true, force: true }); }); const URL = "https://user:token@plugins.archgate.dev/archgate.git"; + /** Use the real path resolver so the test matches addMarketplaceToUserSettings */ function settingsPath() { - return join(tempDir, "Code", "User", "settings.json"); + return getVscodeUserSettingsPath(); } test("creates settings file with defaults when none exists", async () => { @@ -255,8 +259,7 @@ describe("addMarketplaceToUserSettings", () => { }); test("merges JSONC settings and includes defaults when key absent", async () => { - const dir = join(tempDir, "Code", "User"); - mkdirSync(dir, { recursive: true }); + mkdirSync(join(settingsPath(), ".."), { recursive: true }); await Bun.write( settingsPath(), `{ "git.autofetch": true, "chat.mcp.gallery.enabled": true, }` @@ -266,7 +269,6 @@ describe("addMarketplaceToUserSettings", () => { const content = JSON.parse(await Bun.file(settingsPath()).text()); expect(content["git.autofetch"]).toBe(true); - expect(content["chat.mcp.gallery.enabled"]).toBe(true); expect(content["chat.plugins.marketplaces"]).toEqual([ "github/copilot-plugins", "github/awesome-copilot", @@ -275,8 +277,7 @@ describe("addMarketplaceToUserSettings", () => { }); test("deduplicates when key already exists", async () => { - const dir = join(tempDir, "Code", "User"); - mkdirSync(dir, { recursive: true }); + mkdirSync(join(settingsPath(), ".."), { recursive: true }); await Bun.write( settingsPath(), JSON.stringify({ From 383540f2c140015c0e67fe2b157b60395e118b9e Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 7 Mar 2026 12:19:02 +0100 Subject: [PATCH 12/13] fix: use /archgate-vscode.git URL for VS Code marketplace VS Code Copilot expects .github/plugin/ manifest format, which is served from a separate virtual git repo at /archgate-vscode.git. Claude Code and Copilot CLI continue using /archgate.git with the .claude-plugin/ format. Co-Authored-By: Claude Opus 4.6 --- docs/src/content/docs/guides/vscode-plugin.mdx | 6 +++--- docs/src/content/docs/pt-br/guides/vscode-plugin.mdx | 4 ++-- src/helpers/init-project.ts | 2 +- src/helpers/plugin-install.ts | 11 ++++++++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx index 3f7ba1db..197034af 100644 --- a/docs/src/content/docs/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -7,9 +7,9 @@ The Archgate VS Code plugin gives AI agents working in [VS Code](https://code.vi ## How it works -VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate.git`. When you add this marketplace to your VS Code user settings, VS Code discovers and installs the plugin automatically. +VS Code supports **agent plugins** installed from git-based marketplaces. The Archgate plugin is served from a git repository at `plugins.archgate.dev/archgate-vscode.git`. When you add this marketplace to your VS Code user settings, VS Code discovers and installs the plugin automatically. -The plugin uses the same format as Claude Code plugins (`.claude-plugin/plugin.json`), which VS Code's agent plugin system recognizes natively. +The plugin is served in VS Code Copilot's native `.github/plugin/` manifest format, separate from the Claude Code `.claude-plugin/` format. ## Installation @@ -88,7 +88,7 @@ The user-level marketplace setting (added to your `settings.json`): ```json { "chat.plugins.marketplaces": [ - "https://:@plugins.archgate.dev/archgate.git" + "https://:@plugins.archgate.dev/archgate-vscode.git" ] } ``` diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx index 066d3498..0b0eaa92 100644 --- a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -7,7 +7,7 @@ O plugin Archgate para VS Code oferece aos agentes de IA que trabalham no [VS Co ## Como funciona -O VS Code suporta **plugins de agente** instalados a partir de marketplaces baseados em git. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate.git`. Quando você adiciona esse marketplace às suas configurações de usuário do VS Code, o VS Code descobre e instala o plugin automaticamente. +O VS Code suporta **plugins de agente** instalados a partir de marketplaces baseados em git. O plugin Archgate é servido a partir de um repositório git em `plugins.archgate.dev/archgate-vscode.git`. Quando você adiciona esse marketplace às suas configurações de usuário do VS Code, o VS Code descobre e instala o plugin automaticamente. O plugin usa o mesmo formato dos plugins do Claude Code (`.claude-plugin/plugin.json`), que o sistema de plugins de agente do VS Code reconhece nativamente. @@ -88,7 +88,7 @@ A configuração do marketplace no nível de usuário (adicionada ao seu `settin ```json { "chat.plugins.marketplaces": [ - "https://:@plugins.archgate.dev/archgate.git" + "https://:@plugins.archgate.dev/archgate-vscode.git" ] } ``` diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 878fbfa8..15656d0f 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -120,7 +120,7 @@ async function configureEditorSettings( const { loadCredentials } = await import("./auth"); const creds = await loadCredentials(); const marketplaceUrl = creds - ? (await import("./plugin-install")).buildMarketplaceUrl(creds) + ? (await import("./plugin-install")).buildVscodeMarketplaceUrl(creds) : undefined; return configureVscodeSettings(projectRoot, marketplaceUrl); } diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index e37fa3c9..b8fc1dd4 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -37,12 +37,21 @@ async function run( // --------------------------------------------------------------------------- /** - * Build the authenticated git marketplace URL for Claude Code plugin installation. + * Build the authenticated git marketplace URL for Claude Code & Copilot CLI plugin installation. + * Claude Code and Copilot CLI both use the .claude-plugin/ manifest format. */ export function buildMarketplaceUrl(credentials: StoredCredentials): string { return `https://${credentials.github_user}:${credentials.token}@plugins.archgate.dev/archgate.git`; } +/** + * Build the authenticated git marketplace URL for VS Code plugin installation. + * VS Code Copilot uses the .github/plugin/ manifest format, served from a separate repo. + */ +export function buildVscodeMarketplaceUrl(credentials: StoredCredentials): string { + return `https://${credentials.github_user}:${credentials.token}@plugins.archgate.dev/archgate-vscode.git`; +} + /** * Check whether the `claude` CLI is available on the system PATH. */ From 98147507ff849c524b37e31617ca57e7d7df4d60 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 7 Mar 2026 12:30:45 +0100 Subject: [PATCH 13/13] style: fix Prettier formatting in plugin-install.ts Co-Authored-By: Claude Opus 4.6 --- src/helpers/plugin-install.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index b8fc1dd4..035b9a2c 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -48,7 +48,9 @@ export function buildMarketplaceUrl(credentials: StoredCredentials): string { * Build the authenticated git marketplace URL for VS Code plugin installation. * VS Code Copilot uses the .github/plugin/ manifest format, served from a separate repo. */ -export function buildVscodeMarketplaceUrl(credentials: StoredCredentials): string { +export function buildVscodeMarketplaceUrl( + credentials: StoredCredentials +): string { return `https://${credentials.github_user}:${credentials.token}@plugins.archgate.dev/archgate-vscode.git`; }