From f37800d64097e37d5ab6089fd2aea88b71eb976c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 16 Jul 2026 00:05:55 +0530 Subject: [PATCH] W-23057147: Skills and MCP support for Gemini, Antigravity, and Copilot CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend agent-skill installation and the b2c-dx-mcp MCP server to Gemini and Google Antigravity, and round out the VS Code onboarding walkthrough so it can set up skills + MCP for every supported AI tool (including GitHub Copilot CLI). SDK / CLI - Add gemini-cli and antigravity to the IdeType registry (IDE_CONFIGS, ALL_IDE_TYPES) with their skill install paths: gemini-cli project .gemini/skills global ~/.gemini/skills antigravity project .agents/skills global ~/.gemini/config/skills - b2c setup skills now accepts --ide gemini-cli and --ide antigravity; the flag options derive from ALL_IDE_TYPES so the list stays in sync automatically. Gemini CLI extension - gemini-extension.json + GEMINI.md bundle the b2c-dx-mcp MCP server and B2C project context for `gemini extensions install `. - scripts/sync-plugin-versions.mjs pins the extension to the current @salesforce/b2c-dx-mcp version. VS Code walkthrough — "Set Up Agent Skills & MCP" - Add Gemini CLI and Antigravity targets/icons. - Add a dedicated GitHub Copilot CLI card that installs skills via the marketplace-plugin command (`copilot plugin install`), since copilot-cli is not a valid `b2c setup skills --ide` value. Its "Reinstall" uninstalls the plugins first, then reinstalls (uninstall -> install), and `marketplace add` is separated with `;` (not `&&`) so an "already registered" marketplace does not abort the installs. - Detect Copilot CLI skills via ~/.copilot/installed-plugins// (skillsMarkerDir), where `copilot plugin install` actually drops plugins, so the card correctly flips to "Skills installed". - Detect the VS Code (Copilot Chat) card via the VS Code user-data dir instead of ~/.copilot, so it no longer collides with the Copilot CLI card. - Add per-tool MCP-server detection (detectMcpStatus): parse each IDE's documented MCP config and match a real b2c-dx-mcp entry inside an mcpServers/servers container (a stray mention is not a false positive). Tools with no documented MCP path show "MCP · manual" linking the setup guide. The MCP action reads "Re-configure MCP" once a server is registered. - Replace the single ambiguous "N skills installed" count with four labelled stats (tools supported · detected here · with skills · with MCP) and drop the redundant "Counts are per AI tool" legend; strengthen the ghost-button border. VS Code walkthrough — "Install the B2C CLI" - Render "Update CLI" as a disabled "Up to date" button when the installed CLI is confirmed current (b2cCliOutdated === false); keep it enabled + primary when a newer version exists, and enabled when the latest version can't be resolved (offline) so an unverified update isn't blocked. - Surface the detected b2c-cli version in the green "Quick actions" chip (e.g. "b2c-cli v1.4.4") with a tooltip. - Extract the action logic into a pure, exported buildInstallCliActions(). Docs - Cover Gemini CLI, Gemini Code Assist, and Antigravity (IDE/CLI/SDK) in the agent-skills guide, CLI setup reference, and MCP installation guide, with explicit not-supported notes for the consumer Gemini and Microsoft Copilot desktop apps. Chore - Bump tsx catalog floor to ^4.22.0 to fix the Node >= 22 DEP0205 warning at its source (tsx switched to module.registerHooks); dev-only, dedupes esbuild. Tests - SDK registry/path coverage for the new IDEs; MCP schema-compat regression guard ensuring every array param emits an items definition (GitHub Copilot MCP requirement); VS-extension coverage for the Copilot CLI card invariant, the VS Code detection path change, the MCP detection matrix, the install-cli Update/version states, and the rendered summary counts + badges. Scope note: Antigravity support was folded into this story alongside Gemini and GitHub Copilot. --- .../gemini-copilot-antigravity-skills-mcp.md | 11 + GEMINI.md | 43 ++ README.md | 13 +- docs/cli/setup.md | 12 +- docs/guide/agent-skills.md | 125 +++++- docs/mcp/installation.md | 61 ++- gemini-extension.json | 16 + packages/b2c-cli/src/commands/setup/skills.ts | 5 +- .../test/tools/schema-compat.test.ts | 96 ++++ packages/b2c-tooling-sdk/src/skills/agents.ts | 35 ++ packages/b2c-tooling-sdk/src/skills/types.ts | 2 + .../test/skills/agents.test.ts | 109 +++++ .../src/test/walkthrough-ai-skills.test.ts | 314 +++++++++++++ .../src/test/walkthrough-install-cli.test.ts | 73 +++ .../src/walkthrough/aiSkillsContent.ts | 294 +++++++++++- .../src/walkthrough/onboardingPanel.ts | 52 +-- .../src/walkthrough/toolDetection.ts | 48 ++ pnpm-lock.yaml | 425 +++++++++--------- pnpm-workspace.yaml | 2 +- scripts/sync-plugin-versions.mjs | 27 ++ skills/b2c/skills/b2c-onboarding/SKILL.md | 4 +- 21 files changed, 1476 insertions(+), 291 deletions(-) create mode 100644 .changeset/gemini-copilot-antigravity-skills-mcp.md create mode 100644 GEMINI.md create mode 100644 gemini-extension.json create mode 100644 packages/b2c-dx-mcp/test/tools/schema-compat.test.ts create mode 100644 packages/b2c-tooling-sdk/test/skills/agents.test.ts create mode 100644 packages/b2c-vs-extension/src/test/walkthrough-ai-skills.test.ts create mode 100644 packages/b2c-vs-extension/src/test/walkthrough-install-cli.test.ts diff --git a/.changeset/gemini-copilot-antigravity-skills-mcp.md b/.changeset/gemini-copilot-antigravity-skills-mcp.md new file mode 100644 index 000000000..148cefa28 --- /dev/null +++ b/.changeset/gemini-copilot-antigravity-skills-mcp.md @@ -0,0 +1,11 @@ +--- +'@salesforce/b2c-cli': minor +'@salesforce/b2c-tooling-sdk': minor +'b2c-vs-extension': patch +'@salesforce/b2c-agent-plugins': patch +'@salesforce/b2c-dx-docs': patch +--- + +Add first-class support for installing B2C agent skills and the `b2c-dx-mcp` MCP server in Gemini and Google Antigravity. `b2c setup skills` now accepts `--ide gemini-cli` (writes to `.gemini/skills`) and `--ide antigravity` (writes to `.agents/skills`), a Gemini CLI extension bundles the MCP server plus project context (`gemini extensions install `), and the docs cover Gemini CLI, Gemini Code Assist, and Antigravity (IDE/CLI/SDK), including which surfaces are not supported (the consumer Gemini and Microsoft Copilot desktop apps). Scope note: Antigravity support was folded into this work alongside Gemini and GitHub Copilot. + +The VS Code extension's "Set Up Agent Skills & MCP" walkthrough now includes a dedicated **GitHub Copilot CLI** card (installs skills via the marketplace-plugin command, with a proper uninstall-then-install "Reinstall"), detects and displays per-tool **MCP server** status alongside skills status, and clarifies the summary counts (tools supported · detected here · with skills · with MCP). The MCP action reads "Re-configure MCP" once a server is registered. The "Install the B2C CLI" step now shows the detected `b2c-cli` version and disables "Update CLI" when the CLI is already up to date. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..e0cd1e03e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,43 @@ +# Salesforce B2C Commerce — Project Context + +This context is provided by the **B2C Developer Tooling** Gemini CLI extension. It +helps Gemini assist with Salesforce B2C Commerce development: SFRA storefronts, +cartridge code, SCAPI/OCAPI, jobs, On-Demand Sandboxes, and headless (PWA Kit / MRT) +projects. + +## Available capabilities + +- **`b2c-dx-mcp` MCP server** (bundled by this extension) — exposes B2C tools: + deploy cartridges, run and monitor jobs, manage On-Demand Sandboxes, stream logs, + and search B2C Commerce documentation. Prefer these tools over ad-hoc shell + commands when performing B2C operations. +- **B2C CLI (`b2c`)** — the `@salesforce/b2c-cli` command-line tool. Use it for + `b2c setup` (configuration), `b2c code deploy`, `b2c sandbox`, `b2c job`, + `b2c logs`, and WebDAV operations. Run `b2c --help` to discover commands. +- **Agent skills** — install detailed, task-specific B2C skills with: + + ```bash + b2c setup skills b2c --ide gemini-cli # development patterns + b2c setup skills b2c-cli --ide gemini-cli # CLI operations + ``` + + These install to `~/.gemini/skills/` (global) or `.gemini/skills/` (project), + where Gemini CLI discovers them automatically. + +## Working conventions + +- **Never create or delete an On-Demand Sandbox without explicit user confirmation** — + sandbox creation may be a billable action. +- **Never overwrite an existing `dw.json`** without confirmation — it holds instance + credentials and configuration. +- B2C Commerce has **no self-service signup**; the user must already have Account + Manager access and a target instance (sandbox or PIG) provisioned by their org's + B2C Commerce admin. +- If a `b2c` command is not found, use `npx @salesforce/b2c-cli ` or install + it with `npm install -g @salesforce/b2c-cli`. + +## References + +- CLI reference: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/ +- Agent skills guide: https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/agent-skills +- MCP installation: https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/installation diff --git a/README.md b/README.md index 41d282907..1a007d7a6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,18 @@ Install B2C Commerce agent skills and plugins in your AI-powered editor. Three p - **Codex CLI**: run `/plugins` and add from this repo (GitHub URL). -- **Gemini CLI**: `gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling` +- **Gemini CLI**: install the extension (bundles the MCP server + project context), then add the skills: + + ``` + gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling + npx @salesforce/b2c-cli setup skills b2c --ide gemini-cli + ``` + +- **Google Antigravity** (IDE / CLI / SDK): + + ``` + npx @salesforce/b2c-cli setup skills b2c --ide antigravity + ``` - **Any supported IDE (file-copy install via CLI)**: diff --git a/docs/cli/setup.md b/docs/cli/setup.md index 374a0b156..b67b3bfdd 100644 --- a/docs/cli/setup.md +++ b/docs/cli/setup.md @@ -422,7 +422,7 @@ b2c setup skills [SKILLSET] | --------------------- | ---------------------------------------------------------------------------------------------- | ----------- | | `--list`, `-l` | List available skills without installing | `false` | | `--skill` | Install specific skill(s) (can be repeated) | | -| `--ide` | Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, agentforce-vibes, manual | Auto-detect | +| `--ide` | Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, gemini-cli, antigravity, agentforce-vibes, manual | Auto-detect | | `--directory`, `-d` | Custom installation directory (overrides IDE default path) | | | `--global`, `-g` | Install to user home directory (global scope) | `false` | | `--update`, `-u` | Update existing skills (overwrite) | `false` | @@ -442,11 +442,15 @@ b2c setup skills [SKILLSET] | `vscode` | VS Code / GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | | `codex` | OpenAI Codex CLI | `.codex/skills/` | `~/.codex/skills/` | | `opencode` | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | +| `gemini-cli` | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` | +| `antigravity` | Google Antigravity | `.agents/skills/` | `~/.gemini/config/skills/` | | `agentforce-vibes` | Agentforce Vibes | `.a4drules/skills/` | `~/Library/Application Support/Code/User/globalStorage` (macOS) | | `manual` | Manual | `.agents/skills/` | `~/.agents/skills/` | Use `agentforce-vibes` for Salesforce Agentforce for VS Code. Use `manual` for generic installation with a custom `--directory` path. +For `gemini-cli`, the [Gemini CLI extension](/guide/agent-skills#gemini-cli) (`gemini extensions install …`) also installs the `b2c-dx-mcp` MCP server and B2C project context. For `antigravity`, the same skills serve the Antigravity IDE, CLI, and SDK; configure the MCP server separately via `.agents/mcp_config.json` (see [MCP Installation](/mcp/installation#google-antigravity)). + ### Examples ```bash @@ -470,6 +474,12 @@ b2c setup skills b2c --ide cursor --ide windsurf # Install specific skills only b2c setup skills b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor +# Install to Gemini CLI (.gemini/skills/) +b2c setup skills b2c-cli --ide gemini-cli + +# Install to Google Antigravity (.agents/skills/) +b2c setup skills b2c --ide antigravity + # Install to Agentforce Vibes (.a4drules/skills/) b2c setup skills b2c --ide agentforce-vibes diff --git a/docs/guide/agent-skills.md b/docs/guide/agent-skills.md index 3f38ebe42..86462ed30 100644 --- a/docs/guide/agent-skills.md +++ b/docs/guide/agent-skills.md @@ -1,12 +1,12 @@ --- -description: Agentic B2C Developer Toolkit — AI agent skills and plugins that teach Agentforce Vibes, Claude Code, Codex, Cursor, and GitHub Copilot the full B2C Commerce stack. +description: Agentic B2C Developer Toolkit — AI agent skills and plugins that teach Agentforce Vibes, Claude Code, Codex, Cursor, Gemini, Google Antigravity, and GitHub Copilot the full B2C Commerce stack. --- # Agent Skills & Plugins Turn your coding agent into a B2C Commerce specialist. Skills cover the full platform — storefront and headless development, operational workflows, and everything in between — so your agent knows both how B2C Commerce works and which CLI commands to run. -Skills follow the open [Agent Skills](https://agentskills.io/home) standard and work with Agentforce Vibes, Claude Code, Cursor, GitHub Copilot (VS Code and CLI), Codex, OpenCode, and others. Install from your IDE's plugin marketplace or the B2C CLI (`b2c setup skills`). +Skills follow the open [Agent Skills](https://agentskills.io/home) standard and work with Agentforce Vibes, Claude Code, Cursor, GitHub Copilot (VS Code and CLI), Gemini CLI, Google Antigravity (IDE/CLI/SDK), Codex, OpenCode, and others. Install from your IDE's plugin marketplace or the B2C CLI (`b2c setup skills`). ## Quick Start @@ -61,6 +61,22 @@ copilot plugin install storefront-next@b2c-developer-tooling copilot plugin install storefront-next-figma@b2c-developer-tooling ``` +```bash [Gemini CLI] +# One command installs the MCP server + project context: +gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling + +# Then add the agent skills (discovered from .gemini/skills): +npx @salesforce/b2c-cli setup skills b2c --ide gemini-cli +npx @salesforce/b2c-cli setup skills b2c-cli --ide gemini-cli +``` + +```bash [Antigravity] +# Google Antigravity (IDE/CLI/SDK) reads skills from .agents/skills: +npx @salesforce/b2c-cli setup skills b2c --ide antigravity +npx @salesforce/b2c-cli setup skills b2c-cli --ide antigravity +# For the MCP server on Antigravity, see /mcp/installation +``` + ```bash [Agentforce Vibes] # Marketplace install coming soon. For now, use the B2C CLI: npx @salesforce/b2c-cli setup skills --ide agentforce-vibes @@ -268,6 +284,77 @@ copilot plugin install storefront-next@b2c-developer-tooling copilot plugin install storefront-next-figma@b2c-developer-tooling ``` +::: warning Microsoft Copilot desktop app is not the same product +The **Microsoft 365 / Microsoft Copilot desktop app** for Mac and Windows is a productivity assistant — it does **not** run local MCP servers or read repo Agent Skills (its extensibility is limited to tenant-published agents and connected apps). For B2C Commerce development, use **GitHub Copilot in VS Code** or the **Copilot CLI** above; those are GitHub Copilot's desktop surfaces and both are supported here. +::: + +## Gemini + +Skills and the `b2c-dx-mcp` MCP server work with **Gemini CLI** and **Gemini Code Assist** (VS Code / JetBrains). + +### Gemini CLI + +The fastest path is the Gemini CLI extension, which bundles the MCP server and B2C project context (`GEMINI.md`) in a single install: + +```bash +gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling +``` + +Update or remove it later with: + +```bash +gemini extensions update b2c-developer-tooling +gemini extensions uninstall b2c-developer-tooling +``` + +The extension does **not** bundle the agent skills (the repo's skills are plugin-grouped, not laid out at the extension root). Install them with the B2C CLI — they land in the directories Gemini CLI discovers automatically: + +::: code-group + +```bash [Project Scope] +b2c setup skills b2c --ide gemini-cli +b2c setup skills b2c-cli --ide gemini-cli +``` + +```bash [User Scope] +b2c setup skills b2c --ide gemini-cli --global +b2c setup skills b2c-cli --ide gemini-cli --global +``` + +::: + +This writes to `.gemini/skills/` (project) or `~/.gemini/skills/` (user). Gemini CLI also reads `.agents/skills/`. + +### Gemini Code Assist (VS Code / JetBrains) + +Gemini Code Assist shares configuration with the Gemini CLI. In **VS Code**, once you've installed the extension (or added the MCP server to `~/.gemini/settings.json`), the `b2c-dx-mcp` tools are available in agent mode. In **JetBrains**, configure the MCP server via the IDE's MCP settings and use `GEMINI.md` / `AGENT.md` for context. See [MCP Installation](/mcp/installation) for the exact server configuration. + +::: warning Gemini desktop app (Mac/Windows) is not supported +The standalone **Gemini app** for Mac and Windows is a consumer assistant. It supports only Gems and Google-curated connected apps — it does **not** run local MCP servers or read repo Agent Skills. For B2C Commerce development, use **Gemini CLI**, **Gemini Code Assist**, or **Google Antigravity** instead. +::: + +## Google Antigravity + +[Google Antigravity](https://antigravity.google/) is an agent-first development platform available as an **IDE**, a **CLI**, and a **Python SDK**. It implements the same open [Agent Skills](https://antigravity.google/docs/skills) standard (`SKILL.md` folders) and supports MCP servers over stdio, so the B2C skills and `b2c-dx-mcp` work across all three surfaces. + +Install the skills with the B2C CLI: + +::: code-group + +```bash [Project Scope] +b2c setup skills b2c --ide antigravity +b2c setup skills b2c-cli --ide antigravity +``` + +```bash [User Scope] +b2c setup skills b2c --ide antigravity --global +b2c setup skills b2c-cli --ide antigravity --global +``` + +::: + +This writes to `.agents/skills/` (workspace) or `~/.gemini/config/skills/` (global) — the locations Antigravity discovers. For the `b2c-dx-mcp` MCP server, add it to `.agents/mcp_config.json` (workspace) or `~/.gemini/config/mcp_config.json` (global); see [MCP Installation](/mcp/installation). + ## B2C CLI Interactive — select skillsets and IDEs: @@ -338,13 +425,15 @@ b2c setup skills b2c --ide agentforce-vibes --global Use [`b2c setup skills`](/cli/setup) for any supported IDE. ::: -| IDE | Flag | -| ------------------------------------------------------------------------------------------ | ---------------- | -| [Cursor](https://cursor.com/docs/skills) | `--ide cursor` | -| [Windsurf](https://docs.windsurf.com/) | `--ide windsurf` | -| [VS Code / Copilot](https://code.visualstudio.com/docs/copilot/customization/agent-skills) | `--ide vscode` | -| [Codex CLI](https://github.com/openai/codex) | `--ide codex` | -| [OpenCode](https://opencode.ai/) | `--ide opencode` | +| IDE | Flag | +| ------------------------------------------------------------------------------------------ | ------------------- | +| [Cursor](https://cursor.com/docs/skills) | `--ide cursor` | +| [Windsurf](https://docs.windsurf.com/) | `--ide windsurf` | +| [VS Code / Copilot](https://code.visualstudio.com/docs/copilot/customization/agent-skills) | `--ide vscode` | +| [Codex CLI](https://github.com/openai/codex) | `--ide codex` | +| [OpenCode](https://opencode.ai/) | `--ide opencode` | +| [Gemini CLI](https://google-gemini.github.io/gemini-cli/) | `--ide gemini-cli` | +| [Google Antigravity](https://antigravity.google/docs/skills) | `--ide antigravity` | ### Manual Installation @@ -357,14 +446,16 @@ b2c setup skills b2c --ide manual --directory ./my-skills For reference, the install locations each `--ide` flag writes to: -| IDE | Project | User | -| ----------------- | ------------------- | ----------------------------- | -| Cursor | `.cursor/skills/` | `~/.cursor/skills/` | -| Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | -| VS Code / Copilot | `.github/skills/` | `~/.copilot/skills/` | -| Codex CLI | `.codex/skills/` | `~/.codex/skills/` | -| OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | -| Agentforce Vibes | `.a4drules/skills/` | IDE's global storage | +| IDE | Project | User | +| ------------------ | ------------------- | ----------------------------- | +| Cursor | `.cursor/skills/` | `~/.cursor/skills/` | +| Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | +| VS Code / Copilot | `.github/skills/` | `~/.copilot/skills/` | +| Codex CLI | `.codex/skills/` | `~/.codex/skills/` | +| OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | +| Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` | +| Google Antigravity | `.agents/skills/` | `~/.gemini/config/skills/` | +| Agentforce Vibes | `.a4drules/skills/` | IDE's global storage | ## Usage Examples diff --git a/docs/mcp/installation.md b/docs/mcp/installation.md index 033f37147..2546fa693 100644 --- a/docs/mcp/installation.md +++ b/docs/mcp/installation.md @@ -1,5 +1,5 @@ --- -description: Install and configure the B2C DX MCP Server for Claude Code, Cursor, GitHub Copilot, and other MCP clients. +description: Install and configure the B2C DX MCP Server for Claude Code, Cursor, GitHub Copilot, Gemini, Google Antigravity, and other MCP clients. --- # Installation @@ -128,6 +128,65 @@ Copilot supports project-level configuration. Create the MCP config file in your With project-level configuration, the server automatically detects your project location. +> **Note:** This applies to **GitHub Copilot in VS Code** and the **Copilot CLI** — GitHub Copilot's developer surfaces. The **Microsoft 365 / Microsoft Copilot desktop app** for Mac/Windows is a separate productivity assistant that does not run local MCP servers. + +## Gemini + +The B2C DX MCP server works with **Gemini CLI** and **Gemini Code Assist** (VS Code / JetBrains). + +### Gemini CLI (extension — recommended) + +The Gemini CLI extension bundles the MCP server (and B2C project context) in a single install: + +```bash +gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling +``` + +This registers `b2c-dx-mcp` automatically — no manual config editing. Update it later with `gemini extensions update b2c-developer-tooling`. + +### Gemini CLI (manual config) + +Alternatively, add the server to `~/.gemini/settings.json` under `mcpServers` (the same shape as Cursor). Gemini CLI uses `"mcpServers"` (not `"servers"`): + +```json +{ + "mcpServers": { + "b2c-dx-mcp": { + "command": "npx", + "args": ["-y", "@salesforce/b2c-dx-mcp@latest", "--allow-non-ga-tools"] + } + } +} +``` + +> **Note:** `~/.gemini/settings.json` holds broad Gemini CLI settings, not just MCP servers — edit it by hand (merge into any existing `mcpServers` object) rather than overwriting the file. The extension install above avoids this entirely. + +### Gemini Code Assist + +Gemini Code Assist shares configuration with the Gemini CLI. In **VS Code**, the server configured in `~/.gemini/settings.json` (or via the extension) is available in agent mode. In **JetBrains**, add the server through the IDE's MCP settings. See the [Gemini CLI configuration docs](https://google-gemini.github.io/gemini-cli/) for details. + +> **Note:** The standalone **Gemini desktop app** for Mac/Windows is a consumer assistant and does **not** run local MCP servers. Use Gemini CLI, Gemini Code Assist, or Google Antigravity for B2C development. + +## Google Antigravity + +[Google Antigravity](https://antigravity.google/docs/mcp) (IDE, CLI, and Python SDK) supports MCP servers over stdio using the standard `"mcpServers"` shape — identical to the config below. Add the server at either scope: + +- **Workspace:** `.agents/mcp_config.json` in your project root +- **Global:** `~/.gemini/config/mcp_config.json` + +```json +{ + "mcpServers": { + "b2c-dx-mcp": { + "command": "npx", + "args": ["-y", "@salesforce/b2c-dx-mcp@latest", "--allow-non-ga-tools"] + } + } +} +``` + +The Antigravity **SDK** auto-discovers servers from the workspace `.agents/mcp_config.json`, so the same file serves the IDE, CLI, and SDK. In the Antigravity IDE you can also add the server through the built-in MCP Store (**···** → **MCP Servers** → **Manage MCP Servers** → **View raw config**). + ## After Installation With the default `mcp.json`, the server auto-detects your project type and loads credentials from your project's `dw.json` or `.env` file. No additional changes to `mcp.json` are needed. diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 000000000..a793a7201 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,16 @@ +{ + "name": "b2c-developer-tooling", + "version": "1.8.0", + "description": "Salesforce B2C Commerce developer tooling for Gemini CLI: the b2c-dx-mcp MCP server (deploy cartridges, run jobs, manage On-Demand Sandboxes, stream logs, query docs) plus B2C project context. Install agent skills with: b2c setup skills --ide gemini-cli", + "contextFileName": "GEMINI.md", + "mcpServers": { + "b2c-dx-mcp": { + "command": "npx", + "args": [ + "-y", + "@salesforce/b2c-dx-mcp@1.8.0", + "--allow-non-ga-tools" + ] + } + } +} diff --git a/packages/b2c-cli/src/commands/setup/skills.ts b/packages/b2c-cli/src/commands/setup/skills.ts index 54691ef1c..bc59139fb 100644 --- a/packages/b2c-cli/src/commands/setup/skills.ts +++ b/packages/b2c-cli/src/commands/setup/skills.ts @@ -91,6 +91,8 @@ export default class SetupSkills extends BaseCommand { '<%= config.bin %> <%= command.id %> b2c --list', '<%= config.bin %> <%= command.id %> b2c-cli --skill b2c-code --skill b2c-webdav --ide cursor', '<%= config.bin %> <%= command.id %> b2c --global --update --force', + '<%= config.bin %> <%= command.id %> b2c-cli --ide gemini-cli', + '<%= config.bin %> <%= command.id %> b2c --ide antigravity', '<%= config.bin %> <%= command.id %> b2c --ide agentforce-vibes', '<%= config.bin %> <%= command.id %> b2c --ide manual --directory ./my-skills', ]; @@ -107,7 +109,8 @@ export default class SetupSkills extends BaseCommand { multiple: true, }), ide: Flags.string({ - description: 'Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, agentforce-vibes, manual', + description: + 'Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, gemini-cli, antigravity, agentforce-vibes, manual', options: ALL_IDE_TYPES, multiple: true, }), diff --git a/packages/b2c-dx-mcp/test/tools/schema-compat.test.ts b/packages/b2c-dx-mcp/test/tools/schema-compat.test.ts new file mode 100644 index 000000000..7a660030e --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/schema-compat.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import {expect} from 'chai'; +import {describe, it} from 'mocha'; +import {z} from 'zod'; +import {normalizeObjectSchema} from '@modelcontextprotocol/sdk/server/zod-compat.js'; +import {toJsonSchemaCompat} from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js'; +import type {McpTool} from '../../src/utils/index.js'; +import type {Services} from '../../src/services.js'; +import {createCartridgesTools} from '../../src/tools/cartridges/index.js'; +import {createDiagnosticsTools} from '../../src/tools/diagnostics/index.js'; +import {createDocsTools} from '../../src/tools/docs/index.js'; +import {createMrtTools} from '../../src/tools/mrt/index.js'; +import {createPwav3Tools} from '../../src/tools/pwav3/index.js'; +import {createScapiTools} from '../../src/tools/scapi/index.js'; +import {createStorefrontNextTools} from '../../src/tools/storefrontnext/index.js'; + +/** + * loadServices is only invoked inside tool handlers at call time — schema + * generation never touches it, so a throwing stub is safe here. + */ +const loadServices = (() => { + throw new Error('loadServices must not be called during schema generation'); +}) as unknown as () => Services; + +function allTools(): McpTool[] { + return [ + ...createCartridgesTools(loadServices), + ...createDiagnosticsTools(loadServices), + ...createDocsTools(loadServices), + ...createMrtTools(loadServices), + ...createPwav3Tools(loadServices), + ...createScapiTools(loadServices), + ...createStorefrontNextTools(loadServices), + ]; +} + +/** + * Convert a tool's Zod input schema exactly the way the MCP SDK does when it + * answers a `tools/list` request, so the JSON Schema we assert on is the one + * clients actually receive. + */ +function toClientJsonSchema(tool: McpTool): Record { + const obj = normalizeObjectSchema(z.object(tool.inputSchema) as never); + return obj ? (toJsonSchemaCompat(obj, {strictUnions: true, pipeStrategy: 'input'}) as Record) : {}; +} + +/** + * Walk a JSON Schema node, collecting JSON-path strings of every `type: "array"` + * node that lacks an `items` (or `prefixItems`) definition. + */ +function findArraysWithoutItems(node: unknown, path: string, out: string[]): void { + if (node === null || typeof node !== 'object') return; + if (Array.isArray(node)) { + for (const [i, n] of node.entries()) { + findArraysWithoutItems(n, `${path}[${i}]`, out); + } + return; + } + const record = node as Record; + const type = record.type; + const isArrayType = type === 'array' || (Array.isArray(type) && type.includes('array')); + if (isArrayType && !('items' in record) && !('prefixItems' in record)) { + out.push(path); + } + for (const [key, value] of Object.entries(record)) { + if (key === 'type') continue; + findArraysWithoutItems(value, `${path}.${key}`, out); + } +} + +describe('tools/schema client compatibility', () => { + // GitHub Copilot's MCP client rejects any tool whose JSON Schema contains an + // array-typed parameter without an `items` definition. This guards against a + // regression that would silently break every Copilot user. + it('emits an items definition for every array-typed parameter', () => { + const offenders: string[] = []; + for (const tool of allTools()) { + findArraysWithoutItems(toClientJsonSchema(tool), tool.name, offenders); + } + expect(offenders, `arrays missing items: ${offenders.join(', ')}`).to.deep.equal([]); + }); + + it('generates a valid object schema for every registered tool', () => { + const tools = allTools(); + expect(tools.length).to.be.greaterThan(0); + for (const tool of tools) { + const schema = toClientJsonSchema(tool); + expect(schema, `${tool.name} schema`).to.be.an('object'); + } + }); +}); diff --git a/packages/b2c-tooling-sdk/src/skills/agents.ts b/packages/b2c-tooling-sdk/src/skills/agents.ts index 5b43f9542..36bb81b88 100644 --- a/packages/b2c-tooling-sdk/src/skills/agents.ts +++ b/packages/b2c-tooling-sdk/src/skills/agents.ts @@ -101,6 +101,39 @@ export const IDE_CONFIGS: Record = { }, docsUrl: 'https://opencode.ai/', }, + // NOTE: gemini-cli, antigravity, and manual all live under ~/.gemini or + // .agents/skills. They are intentionally distinct entries with different + // subpaths — do not collapse them: + // - gemini-cli skills: ~/.gemini/skills, .gemini/skills + // - antigravity skills: ~/.gemini/config/skills, .agents/skills + // - manual skills: ~/.agents/skills, .agents/skills + // gemini-cli and antigravity both probe ~/.gemini for detection (a slightly + // loose signal, since both products share that root), but they install to + // different locations. See docs/guide/agent-skills.md for the surface matrix. + 'gemini-cli': { + id: 'gemini-cli', + displayName: 'Gemini CLI', + paths: { + projectDir: '.gemini/skills', + globalDir: path.join(home, '.gemini/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.gemini')); + }, + docsUrl: 'https://google-gemini.github.io/gemini-cli/', + }, + antigravity: { + id: 'antigravity', + displayName: 'Google Antigravity', + paths: { + projectDir: '.agents/skills', + globalDir: path.join(home, '.gemini/config/skills'), + }, + detectInstalled: async () => { + return fs.existsSync(path.join(home, '.gemini')); + }, + docsUrl: 'https://antigravity.google/docs/skills', + }, 'agentforce-vibes': { id: 'agentforce-vibes', displayName: 'Agentforce Vibes', @@ -139,6 +172,8 @@ export const ALL_IDE_TYPES: IdeType[] = [ 'vscode', 'codex', 'opencode', + 'gemini-cli', + 'antigravity', 'agentforce-vibes', 'manual', ]; diff --git a/packages/b2c-tooling-sdk/src/skills/types.ts b/packages/b2c-tooling-sdk/src/skills/types.ts index 2cd227873..75c671fa2 100644 --- a/packages/b2c-tooling-sdk/src/skills/types.ts +++ b/packages/b2c-tooling-sdk/src/skills/types.ts @@ -14,6 +14,8 @@ export type IdeType = | 'vscode' | 'codex' | 'opencode' + | 'gemini-cli' + | 'antigravity' | 'agentforce-vibes' | 'manual'; diff --git a/packages/b2c-tooling-sdk/test/skills/agents.test.ts b/packages/b2c-tooling-sdk/test/skills/agents.test.ts new file mode 100644 index 000000000..88cb5a756 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/skills/agents.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as os from 'node:os'; +import * as path from 'node:path'; +import {expect} from 'chai'; +import { + IDE_CONFIGS, + ALL_IDE_TYPES, + getSkillInstallPath, + getIdeDisplayName, + getIdeDocsUrl, +} from '../../src/skills/agents.js'; +import type {IdeType} from '../../src/skills/types.js'; + +const home = os.homedir(); + +describe('skills/agents', () => { + describe('IDE_CONFIGS and ALL_IDE_TYPES', () => { + it('has a config entry for every type in ALL_IDE_TYPES', () => { + for (const ide of ALL_IDE_TYPES) { + expect(IDE_CONFIGS[ide], `config for ${ide}`).to.exist; + expect(IDE_CONFIGS[ide].id).to.equal(ide); + } + }); + + it('lists exactly the keys of IDE_CONFIGS (no orphans in either direction)', () => { + expect([...ALL_IDE_TYPES].sort()).to.deep.equal((Object.keys(IDE_CONFIGS) as IdeType[]).sort()); + }); + + it('gives every IDE a non-empty displayName and both install paths', () => { + for (const ide of ALL_IDE_TYPES) { + const config = IDE_CONFIGS[ide]; + expect(config.displayName, `${ide} displayName`).to.be.a('string').and.not.be.empty; + expect(config.paths.projectDir, `${ide} projectDir`).to.be.a('string').and.not.be.empty; + expect(config.paths.globalDir, `${ide} globalDir`).to.be.a('string').and.not.be.empty; + } + }); + }); + + describe('gemini-cli entry', () => { + it('is registered', () => { + expect(ALL_IDE_TYPES).to.include('gemini-cli'); + }); + + it('installs to .gemini/skills (project) and ~/.gemini/skills (global)', () => { + const config = IDE_CONFIGS['gemini-cli']; + expect(config.displayName).to.equal('Gemini CLI'); + expect(config.paths.projectDir).to.equal('.gemini/skills'); + expect(config.paths.globalDir).to.equal(path.join(home, '.gemini/skills')); + expect(config.docsUrl).to.equal('https://google-gemini.github.io/gemini-cli/'); + }); + }); + + describe('antigravity entry', () => { + it('is registered', () => { + expect(ALL_IDE_TYPES).to.include('antigravity'); + }); + + it('installs to .agents/skills (project) and ~/.gemini/config/skills (global)', () => { + const config = IDE_CONFIGS['antigravity']; + expect(config.displayName).to.equal('Google Antigravity'); + expect(config.paths.projectDir).to.equal('.agents/skills'); + expect(config.paths.globalDir).to.equal(path.join(home, '.gemini/config/skills')); + expect(config.docsUrl).to.equal('https://antigravity.google/docs/skills'); + }); + + it('uses a different global path than gemini-cli despite sharing the ~/.gemini root', () => { + expect(IDE_CONFIGS['antigravity'].paths.globalDir).to.not.equal(IDE_CONFIGS['gemini-cli'].paths.globalDir); + }); + }); + + describe('getSkillInstallPath', () => { + it('resolves project-scope paths for gemini-cli', () => { + const p = getSkillInstallPath('gemini-cli', 'b2c-onboarding', {global: false, projectRoot: '/proj'}); + expect(p).to.equal(path.join('/proj', '.gemini/skills', 'b2c-onboarding')); + }); + + it('resolves global-scope paths for gemini-cli', () => { + const p = getSkillInstallPath('gemini-cli', 'b2c-onboarding', {global: true}); + expect(p).to.equal(path.join(home, '.gemini/skills', 'b2c-onboarding')); + }); + + it('resolves project-scope paths for antigravity', () => { + const p = getSkillInstallPath('antigravity', 'b2c-onboarding', {global: false, projectRoot: '/proj'}); + expect(p).to.equal(path.join('/proj', '.agents/skills', 'b2c-onboarding')); + }); + + it('resolves global-scope paths for antigravity', () => { + const p = getSkillInstallPath('antigravity', 'b2c-onboarding', {global: true}); + expect(p).to.equal(path.join(home, '.gemini/config/skills', 'b2c-onboarding')); + }); + }); + + describe('getIdeDisplayName / getIdeDocsUrl', () => { + it('returns display names for the new IDEs', () => { + expect(getIdeDisplayName('gemini-cli')).to.equal('Gemini CLI'); + expect(getIdeDisplayName('antigravity')).to.equal('Google Antigravity'); + }); + + it('returns docs URLs for the new IDEs', () => { + expect(getIdeDocsUrl('gemini-cli')).to.equal('https://google-gemini.github.io/gemini-cli/'); + expect(getIdeDocsUrl('antigravity')).to.equal('https://antigravity.google/docs/skills'); + }); + }); +}); diff --git a/packages/b2c-vs-extension/src/test/walkthrough-ai-skills.test.ts b/packages/b2c-vs-extension/src/test/walkthrough-ai-skills.test.ts new file mode 100644 index 000000000..28f30d170 --- /dev/null +++ b/packages/b2c-vs-extension/src/test/walkthrough-ai-skills.test.ts @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as assert from 'assert'; +import * as fs from 'fs/promises'; +import * as os from 'os'; +import * as path from 'path'; +import { + AI_SKILL_TARGETS, + detectIdeStatus, + detectMcpStatus, + generateAiSkillsHtml, + type AiSkillsTarget, + type DetectedTarget, +} from '../walkthrough/aiSkillsContent.js'; + +/** + * Build a DetectedTarget for render-only assertions without touching disk. + * Uses a clean minimal base (NOT a real registry entry) so tests control + * exactly which optional fields — mcpCommand, marketplaceCommand — are present. + */ +function detected(over: Partial): DetectedTarget { + return { + id: 'test', + label: 'Test IDE', + description: 'desc', + detectPath: '/nope', + globalSkillsDir: '/nope/skills', + projectSkillsDir: 'skills', + status: 'ide-present', + mcpStatus: 'not-configured', + ...over, + }; +} + +suite('walkthrough AI skills — targets', () => { + test('includes a GitHub Copilot CLI card distinct from VS Code Copilot', () => { + const ids = AI_SKILL_TARGETS.map((t) => t.id); + assert.ok(ids.includes('copilot-cli'), 'copilot-cli target is registered'); + assert.ok(ids.includes('vscode'), 'vscode (Copilot Chat) target still present'); + assert.notEqual(ids.indexOf('copilot-cli'), ids.indexOf('vscode'), 'they are separate cards'); + }); + + test('Copilot CLI installs skills via the marketplace command, not --ide', () => { + const copilot = AI_SKILL_TARGETS.find((t) => t.id === 'copilot-cli')!; + assert.equal(copilot.skillsMode, 'marketplace'); + assert.ok(copilot.marketplaceCommand?.includes('copilot plugin install'), 'runs `copilot plugin install`'); + // Guard the invariant that broke the UI: copilot-cli is NOT a valid `b2c + // setup skills --ide` value, so it must never rely on the file-copy path. + assert.notEqual(copilot.skillsMode, 'cli'); + }); + + test('Copilot CLI marketplace-add is separated with ; so a re-add does not abort installs', () => { + // `copilot plugin marketplace add` errors if the marketplace is already + // registered; joining it with `&&` would abort the installs (real bug). + const copilot = AI_SKILL_TARGETS.find((t) => t.id === 'copilot-cli')!; + assert.match(copilot.marketplaceCommand!, /marketplace add [^&]*;[^&]*plugin install/); + assert.doesNotMatch(copilot.marketplaceCommand!, /marketplace add\s+\S+\s+&&/); + }); + + test('VS Code card no longer probes ~/.copilot (that is the Copilot CLI dir)', () => { + const vscode = AI_SKILL_TARGETS.find((t) => t.id === 'vscode')!; + const copilot = AI_SKILL_TARGETS.find((t) => t.id === 'copilot-cli')!; + // The old collision was both cards detecting at ~/.copilot. Assert the VS + // Code card resolves somewhere else AND to a non-empty, VS-Code-shaped path + // (a broken empty string would also "not end with .copilot"). + assert.ok(vscode.detectPath.length > 0, 'vscode detectPath is set'); + assert.equal(path.basename(vscode.detectPath), 'Code', 'vscode detectPath points at a VS Code user dir'); + assert.notEqual(path.basename(vscode.detectPath), '.copilot', 'vscode detectPath is not ~/.copilot'); + assert.notEqual(vscode.detectPath, copilot.detectPath, 'VS Code and Copilot CLI detect at different paths'); + assert.equal(path.basename(copilot.detectPath), '.copilot', 'Copilot CLI keeps ~/.copilot'); + }); +}); + +suite('walkthrough AI skills — MCP detection', () => { + let tmp: string; + + suiteSetup(async () => { + tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'b2c-mcp-test-')); + }); + + suiteTeardown(async () => { + await fs.rm(tmp, {recursive: true, force: true}); + }); + + test('returns "unknown" when the target has no documented MCP location', async () => { + const target: AiSkillsTarget = { + id: 'x', + label: 'X', + description: '', + detectPath: tmp, + globalSkillsDir: tmp, + projectSkillsDir: 'skills', + }; + assert.equal(await detectMcpStatus(target, [tmp]), 'unknown'); + }); + + test('returns "not-configured" when a known config file lacks the server', async () => { + const root = path.join(tmp, 'proj-empty'); + await fs.mkdir(path.join(root, '.cursor'), {recursive: true}); + await fs.writeFile(path.join(root, '.cursor', 'mcp.json'), JSON.stringify({mcpServers: {}})); + const target: AiSkillsTarget = { + id: 'cursor', + label: 'Cursor', + description: '', + detectPath: tmp, + globalSkillsDir: path.join(tmp, 'nope'), + projectSkillsDir: '.cursor/skills', + mcpProjectConfig: path.join('.cursor', 'mcp.json'), + mcpGlobalConfig: path.join(tmp, 'nope', 'mcp.json'), + }; + assert.equal(await detectMcpStatus(target, [root]), 'not-configured'); + }); + + test('returns "configured" when a project config registers b2c-dx-mcp', async () => { + const root = path.join(tmp, 'proj-ok'); + await fs.mkdir(path.join(root, '.cursor'), {recursive: true}); + await fs.writeFile( + path.join(root, '.cursor', 'mcp.json'), + JSON.stringify({mcpServers: {'b2c-dx-mcp': {command: 'npx'}}}), + ); + const target: AiSkillsTarget = { + id: 'cursor', + label: 'Cursor', + description: '', + detectPath: tmp, + globalSkillsDir: path.join(tmp, 'nope'), + projectSkillsDir: '.cursor/skills', + mcpProjectConfig: path.join('.cursor', 'mcp.json'), + }; + assert.equal(await detectMcpStatus(target, [root]), 'configured'); + }); + + test('detects the server inside a shared settings file (Gemini-style)', async () => { + const settings = path.join(tmp, 'settings.json'); + await fs.writeFile(settings, JSON.stringify({theme: 'dark', mcpServers: {'b2c-dx-mcp': {command: 'npx'}}})); + const target: AiSkillsTarget = { + id: 'gemini-cli', + label: 'Gemini CLI', + description: '', + detectPath: tmp, + globalSkillsDir: tmp, + projectSkillsDir: 'skills', + mcpGlobalConfig: settings, + }; + assert.equal(await detectMcpStatus(target, []), 'configured'); + }); + + test('a stray "b2c-dx-mcp" string outside a server container is NOT configured', async () => { + // Regression guard: a broad settings file that merely mentions the server + // (e.g. in a disabled list or a comment-like string) must not read as + // configured — only a real entry inside mcpServers/servers counts. + const settings = path.join(tmp, 'stray.json'); + await fs.writeFile( + settings, + JSON.stringify({note: 'considered adding b2c-dx-mcp', disabledMcpServers: ['b2c-dx-mcp'], mcpServers: {}}), + ); + const target: AiSkillsTarget = { + id: 'gemini-cli', + label: 'Gemini CLI', + description: '', + detectPath: tmp, + globalSkillsDir: tmp, + projectSkillsDir: 'skills', + mcpGlobalConfig: settings, + }; + assert.equal(await detectMcpStatus(target, []), 'not-configured'); + }); + + test('a present marker dir counts as configured (extension install)', async () => { + const marker = path.join(tmp, 'ext', 'b2c-developer-tooling'); + await fs.mkdir(marker, {recursive: true}); + const target: AiSkillsTarget = { + id: 'gemini-cli', + label: 'Gemini CLI', + description: '', + detectPath: tmp, + globalSkillsDir: tmp, + projectSkillsDir: 'skills', + mcpMarkerDir: marker, + }; + assert.equal(await detectMcpStatus(target, []), 'configured'); + }); +}); + +suite('walkthrough AI skills — IDE / skills detection', () => { + let tmp: string; + + suiteSetup(async () => { + tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'b2c-ide-test-')); + }); + + suiteTeardown(async () => { + await fs.rm(tmp, {recursive: true, force: true}); + }); + + test('marketplace target flips to skills-installed via its skillsMarkerDir', async () => { + // Copilot CLI installs plugins under ~/.copilot/installed-plugins//, + // NOT in a skills dir. A b2c* entry there must count as installed. + const detect = path.join(tmp, '.copilot'); + const marker = path.join(detect, 'installed-plugins', 'b2c-developer-tooling'); + await fs.mkdir(path.join(marker, 'b2c-cli'), {recursive: true}); + const target: AiSkillsTarget = { + id: 'copilot-cli', + label: 'GitHub Copilot CLI', + description: '', + detectPath: detect, + globalSkillsDir: path.join(detect, 'skills'), // empty — must NOT be the deciding signal + projectSkillsDir: '.copilot/skills', + skillsMarkerDir: marker, + }; + assert.equal(await detectIdeStatus(target, []), 'skills-installed'); + }); + + test('marketplace target with an empty marker dir stays ide-present', async () => { + const detect = path.join(tmp, '.copilot-empty'); + const marker = path.join(detect, 'installed-plugins', 'b2c-developer-tooling'); + await fs.mkdir(marker, {recursive: true}); // exists but has no b2c* plugin + const target: AiSkillsTarget = { + id: 'copilot-cli', + label: 'GitHub Copilot CLI', + description: '', + detectPath: detect, + globalSkillsDir: path.join(detect, 'skills'), + projectSkillsDir: '.copilot/skills', + skillsMarkerDir: marker, + }; + assert.equal(await detectIdeStatus(target, []), 'ide-present'); + }); + + test('the registered Copilot CLI target has a marketplace skillsMarkerDir', () => { + const copilot = AI_SKILL_TARGETS.find((t) => t.id === 'copilot-cli')!; + assert.ok(copilot.skillsMarkerDir, 'copilot-cli defines skillsMarkerDir'); + assert.match(copilot.skillsMarkerDir!, /installed-plugins/); + }); +}); + +suite('walkthrough AI skills — rendered summary + badges', () => { + test('summary counts distinguish detected / with-skills / with-MCP', () => { + const targets: DetectedTarget[] = [ + detected({id: 'cursor', status: 'skills-installed', mcpStatus: 'configured'}), + detected({id: 'vscode', status: 'ide-present', mcpStatus: 'not-configured'}), + detected({id: 'windsurf', status: 'not-installed', mcpStatus: 'unknown'}), + ]; + const html = generateAiSkillsHtml(targets); + // 3 supported, 2 detected (not "not-installed"), 1 with skills, 1 with MCP. + assert.match(html, /3<\/span>tools supported/); + assert.match(html, /2<\/span>detected here/); + assert.match(html, /1<\/span>with skills/); + assert.match(html, /1<\/span>with MCP/); + // The old ambiguous "skills installed" phrasing must be gone. + assert.doesNotMatch(html, />skills installed { + const html = generateAiSkillsHtml([ + detected({id: 'cursor', status: 'ide-present', mcpStatus: 'configured'}), + detected({id: 'windsurf', status: 'not-installed', mcpStatus: 'unknown'}), + ]); + assert.match(html, /MCP configured/, 'configured present card shows the badge'); + // The not-installed card should not assert any MCP state. Anchor on the + //
tag (the CSS block also contains data-status="not-installed") + // and bound the slice at the card's own
so it can't bleed into + // the next card or truncate before the pills. + const cardStart = html.indexOf('
-1, 'not-installed card is rendered'); + const cardEnd = html.indexOf('
', cardStart); + assert.ok(cardEnd > cardStart, 'not-installed card is closed'); + const notInstalledCard = html.slice(cardStart, cardEnd); + assert.doesNotMatch(notInstalledCard, /MCP configured|MCP not set up|MCP · manual/); + }); + + test('offers an MCP setup-guide link when no one-click MCP command exists', () => { + const html = generateAiSkillsHtml([detected({id: 'codex', status: 'ide-present', mcpStatus: 'unknown'})]); + assert.match(html, /MCP setup guide/); + assert.match(html, /mcp\/installation/); + }); + + test('MCP action reads "Re-configure MCP" when already configured, "Install MCP" otherwise', () => { + const mcpCommand = 'mkdir -p .vscode && echo b2c-dx-mcp > .vscode/mcp.json'; + const notYet = generateAiSkillsHtml([ + detected({id: 'vscode', status: 'ide-present', mcpStatus: 'not-configured', mcpCommand}), + ]); + assert.match(notYet, />Install MCPRe-configure MCPRe-configure MCPInstall MCP { + const base = { + skillsMode: 'marketplace' as const, + marketplaceCommand: 'copilot plugin install b2c@mkt', + marketplaceUninstallCommand: 'copilot plugin uninstall b2c@mkt', + }; + // Skills already installed → primary is "Reinstall" and chains uninstall → install. + const reinstall = generateAiSkillsHtml([detected({id: 'copilot-cli', status: 'skills-installed', ...base})]); + assert.match(reinstall, /Reinstall<\/span>/); + // The button's data-cmd carries `uninstall ... ; ... install`. + assert.match(reinstall, /copilot plugin uninstall b2c@mkt\s*;\s*copilot plugin install b2c@mkt/); + + // Fresh (IDE present, no skills) → "Install Skills", plain install only. + const fresh = generateAiSkillsHtml([detected({id: 'copilot-cli', status: 'ide-present', ...base})]); + assert.match(fresh, /Install Skills<\/span>/); + assert.doesNotMatch(fresh, /plugin uninstall/); + }); +}); diff --git a/packages/b2c-vs-extension/src/test/walkthrough-install-cli.test.ts b/packages/b2c-vs-extension/src/test/walkthrough-install-cli.test.ts new file mode 100644 index 000000000..9da235fb7 --- /dev/null +++ b/packages/b2c-vs-extension/src/test/walkthrough-install-cli.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ + +import * as assert from 'assert'; +import {buildInstallCliActions, type ToolDetectionResult} from '../walkthrough/toolDetection.js'; + +/** Minimal ToolDetectionResult builder for the CLI-step action logic. */ +function result(over: Partial): ToolDetectionResult { + const tool = (name: string, installed: boolean, version?: string) => ({name, installed, version, label: name}); + return { + node: tool('node', true, '20.0.0'), + npm: tool('npm', true, '10.0.0'), + homebrew: tool('homebrew', false), + npx: tool('npx', true, '10.0.0'), + b2cCli: tool('b2c-cli', true, '1.2.3'), + ...over, + }; +} + +suite('walkthrough install-cli — Update CLI action state', () => { + test('CLI up to date: Update is present but DISABLED with a tooltip', () => { + const actions = buildInstallCliActions(result({b2cCliOutdated: false, b2cCliLatest: '1.2.3'})); + const update = actions.find((a) => a.command === 'b2c-dx.cli.update')!; + assert.ok(update, 'update action still rendered'); + assert.equal(update.disabled, true, 'update is disabled when up to date'); + assert.equal(update.label, 'Up to date'); + assert.match(update.tooltip ?? '', /latest version|no update/i); + assert.notEqual(update.primary, true, 'up-to-date update is not the primary CTA'); + // Verify CLI is the primary action in the up-to-date case. + const verify = actions.find((a) => a.command === 'b2c-dx.cli.verify')!; + assert.equal(verify.primary, true); + }); + + test('update available: Update CLI is enabled and primary', () => { + const actions = buildInstallCliActions(result({b2cCliOutdated: true, b2cCliLatest: '2.0.0'})); + const update = actions.find((a) => a.command === 'b2c-dx.cli.update')!; + assert.equal(update.label, 'Update CLI'); + assert.notEqual(update.disabled, true, 'update is actionable when a newer version exists'); + assert.equal(update.primary, true, 'update is the primary CTA when outdated'); + }); + + test('latest version unknown: Update stays ENABLED (do not block an unverified update)', () => { + // b2cCliOutdated undefined => we could not resolve the latest version. + const actions = buildInstallCliActions(result({b2cCliOutdated: undefined, b2cCliLatest: undefined})); + const update = actions.find((a) => a.command === 'b2c-dx.cli.update')!; + assert.equal(update.label, 'Update CLI'); + assert.notEqual(update.disabled, true, 'unknown-latest keeps update enabled'); + }); + + test('CLI not installed: no Update action, install path offered', () => { + const actions = buildInstallCliActions(result({b2cCli: {name: 'b2c-cli', installed: false, label: 'b2c-cli'}})); + assert.ok(!actions.some((a) => a.command === 'b2c-dx.cli.update'), 'no Update CLI when nothing is installed'); + assert.ok( + actions.some((a) => a.command === 'b2c-dx.cli.installNpm' && a.primary), + 'npm install is the primary CTA', + ); + assert.ok(actions.some((a) => a.command === 'b2c-dx.cli.recheck')); + }); + + test('not installed, no npm but Homebrew present: brew install is primary', () => { + const actions = buildInstallCliActions( + result({ + b2cCli: {name: 'b2c-cli', installed: false, label: 'b2c-cli'}, + npm: {name: 'npm', installed: false, label: 'npm'}, + homebrew: {name: 'homebrew', installed: true, version: '4.0.0', label: 'homebrew'}, + }), + ); + assert.ok(actions.some((a) => a.command === 'b2c-dx.cli.installBrew' && a.primary)); + }); +}); diff --git a/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts b/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts index e4b629571..8217bd4e4 100644 --- a/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts +++ b/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts @@ -13,13 +13,27 @@ import * as vscode from 'vscode'; * Renders the AI Skills & MCP step body as styled HTML with one-click install * actions for each IDE the B2C CLI's `setup skills` command supports. * - * IDE list and detection paths come directly from - * `@salesforce/b2c-tooling-sdk/skills` IDE_CONFIGS (DRY by mirroring the - * paths since the SDK runs in the extension host too). + * IDE list and detection paths mirror `@salesforce/b2c-tooling-sdk/skills` + * IDE_CONFIGS (DRY by mirroring the paths since the SDK runs in the extension + * host too). Two intentional divergences from the SDK: + * - `vscode` detects via the VS Code user-data dir, NOT `~/.copilot`, so it + * doesn't collide with the separate GitHub Copilot CLI card (whose home IS + * `~/.copilot`). The SDK still uses `~/.copilot` for `vscode`. + * - `copilot-cli` is walkthrough-only — the SDK's IdeType union has no such + * value, so it installs skills via a marketplace command, not `--ide`. */ export type IdeStatus = 'not-installed' | 'ide-present' | 'skills-installed'; +/** + * MCP-server configuration state for a target. + * - `configured` — a `b2c-dx-mcp` entry was found in the IDE's MCP config. + * - `not-configured` — the IDE has a known MCP config location, but no entry yet. + * - `unknown` — this IDE's MCP config path isn't documented, so we don't probe it + * (shown as a manual-setup hint rather than a false "not configured"). + */ +export type McpStatus = 'configured' | 'not-configured' | 'unknown'; + export interface AiSkillsTarget { /** CLI flag value for `b2c setup skills --ide `. */ id: string; @@ -31,14 +45,43 @@ export interface AiSkillsTarget { globalSkillsDir: string; /** Per-IDE skills install dir for project-scoped installs (relative to workspace root). */ projectSkillsDir: string; + /** + * Absolute dir that holds installed skills for marketplace-mode targets whose + * skills DON'T land in `globalSkillsDir` (e.g. GitHub Copilot CLI installs + * plugins under `~/.copilot/installed-plugins//`). A `b2c*` entry + * here counts as skills-installed. + */ + skillsMarkerDir?: string; + /** + * How skills install for this target. `'cli'` (default) uses the file-copy + * installer `b2c setup skills --ide `. `'marketplace'` runs + * `marketplaceCommand` as the primary action — used by GitHub Copilot CLI, + * which is a marketplace-plugin target, NOT a valid `--ide` value. + */ + skillsMode?: 'cli' | 'marketplace'; /** Marketplace plugin command, if applicable. */ marketplaceCommand?: string; + /** + * Command that uninstalls the marketplace plugins. Prepended to + * `marketplaceCommand` for a clean "Reinstall" (uninstall → install) once the + * plugins are already present. Only meaningful for marketplace-mode targets. + */ + marketplaceUninstallCommand?: string; /** MCP install one-liner / snippet. */ mcpCommand?: string; + /** Project-relative MCP config file that would hold a `b2c-dx-mcp` entry. */ + mcpProjectConfig?: string; + /** Absolute global MCP config file that would hold a `b2c-dx-mcp` entry. */ + mcpGlobalConfig?: string; + /** Absolute marker dir whose existence implies MCP is registered (e.g. an installed extension). */ + mcpMarkerDir?: string; } const home = os.homedir(); +/** Fallback MCP setup docs, linked when a target has no one-click MCP path. */ +const MCP_DOCS_URL = 'https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/installation.html'; + /** Only IDEs the b2c CLI's `setup skills` command actually supports. */ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ { @@ -52,6 +95,9 @@ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ 'claude plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling && claude plugin install b2c-cli', mcpCommand: 'claude mcp add --transport stdio --scope project b2c-dx-mcp -- npx -y @salesforce/b2c-dx-mcp@latest --allow-non-ga-tools', + // `claude mcp add --scope project` writes .mcp.json; user scope writes ~/.claude.json. + mcpProjectConfig: '.mcp.json', + mcpGlobalConfig: path.join(home, '.claude.json'), }, { id: 'cursor', @@ -62,6 +108,8 @@ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ projectSkillsDir: path.join('.cursor', 'skills'), mcpCommand: 'mkdir -p .cursor && printf \'%s\' \'{"mcpServers":{"b2c-dx-mcp":{"command":"npx","args":["-y","@salesforce/b2c-dx-mcp@latest","--allow-non-ga-tools"]}}}\' > .cursor/mcp.json', + mcpProjectConfig: path.join('.cursor', 'mcp.json'), + mcpGlobalConfig: path.join(home, '.cursor', 'mcp.json'), }, { id: 'windsurf', @@ -70,16 +118,21 @@ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ detectPath: path.join(home, '.codeium', 'windsurf'), globalSkillsDir: path.join(home, '.codeium', 'windsurf', 'skills'), projectSkillsDir: path.join('.windsurf', 'skills'), + // MCP config path for Windsurf is not documented in our install guide — leave + // it unprobed (McpStatus 'unknown') rather than assert a false negative. }, { id: 'vscode', label: 'VS Code / GitHub Copilot', description: 'Copilot Chat in VS Code. MCP via .vscode/mcp.json.', - detectPath: path.join(home, '.copilot'), + // Detect via the VS Code user dir, NOT ~/.copilot (that's the Copilot *CLI*, + // which now has its own card below). + detectPath: getVsCodeUserDir(), globalSkillsDir: path.join(home, '.copilot', 'skills'), projectSkillsDir: path.join('.github', 'skills'), mcpCommand: 'mkdir -p .vscode && printf \'%s\' \'{"servers":{"b2c-dx-mcp":{"type":"stdio","command":"npx","args":["-y","@salesforce/b2c-dx-mcp@latest","--allow-non-ga-tools"]}}}\' > .vscode/mcp.json', + mcpProjectConfig: path.join('.vscode', 'mcp.json'), }, { id: 'codex', @@ -98,6 +151,62 @@ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ globalSkillsDir: path.join(home, '.config', 'opencode', 'skills'), projectSkillsDir: path.join('.opencode', 'skills'), }, + { + id: 'gemini-cli', + label: 'Gemini CLI', + description: 'Google Gemini CLI. Extension install bundles skills, MCP, and context.', + detectPath: path.join(home, '.gemini'), + globalSkillsDir: path.join(home, '.gemini', 'skills'), + projectSkillsDir: path.join('.gemini', 'skills'), + // The extension bundles skills + MCP + GEMINI.md context in one install. + // No raw mcpCommand: Gemini's ~/.gemini/settings.json holds broad settings + // (not a dedicated mcp.json), so overwriting it would clobber user config. + marketplaceCommand: 'gemini extensions install https://github.com/SalesforceCommerceCloud/b2c-developer-tooling', + // The extension install drops the server under ~/.gemini/extensions/. + mcpMarkerDir: path.join(home, '.gemini', 'extensions', 'b2c-developer-tooling'), + mcpGlobalConfig: path.join(home, '.gemini', 'settings.json'), + }, + { + id: 'antigravity', + label: 'Google Antigravity', + description: 'Google Antigravity (IDE/CLI/SDK). Skills via .agents/skills; MCP via .agents/mcp_config.json.', + detectPath: path.join(home, '.gemini'), + globalSkillsDir: path.join(home, '.gemini', 'config', 'skills'), + projectSkillsDir: path.join('.agents', 'skills'), + // .agents/mcp_config.json is a dedicated MCP file using the standard + // mcpServers/stdio shape, so writing it directly is safe (like Cursor/VS Code). + mcpCommand: + 'mkdir -p .agents && printf \'%s\' \'{"mcpServers":{"b2c-dx-mcp":{"command":"npx","args":["-y","@salesforce/b2c-dx-mcp@latest","--allow-non-ga-tools"]}}}\' > .agents/mcp_config.json', + mcpProjectConfig: path.join('.agents', 'mcp_config.json'), + mcpGlobalConfig: path.join(home, '.gemini', 'config', 'mcp_config.json'), + }, + { + // GitHub Copilot CLI is a MARKETPLACE-plugin target, not a `b2c setup skills + // --ide` value — the CLI's IdeType union has no `copilot-cli`. Skills install + // via `copilot plugin install`; MCP has no documented one-click path, so it + // links to the setup docs. + id: 'copilot-cli', + label: 'GitHub Copilot CLI', + description: 'GitHub Copilot in the terminal. Skills install as marketplace plugins.', + detectPath: path.join(home, '.copilot'), + globalSkillsDir: path.join(home, '.copilot', 'skills'), + projectSkillsDir: path.join('.copilot', 'skills'), + // `copilot plugin install` drops plugins under + // ~/.copilot/installed-plugins//, NOT in a skills dir. + // Our marketplace is `b2c-developer-tooling`, so its presence (containing + // the b2c/b2c-cli plugins) means the skills are installed. + skillsMarkerDir: path.join(home, '.copilot', 'installed-plugins', 'b2c-developer-tooling'), + skillsMode: 'marketplace', + // `marketplace add` errors ("already registered") if the marketplace is + // already present, so separate it with `;` — otherwise `&&` would abort the + // installs. The installs themselves chain with `&&` (b2c-cli after b2c). + marketplaceCommand: + 'copilot plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling ; copilot plugin install b2c@b2c-developer-tooling && copilot plugin install b2c-cli@b2c-developer-tooling', + // Reinstall = uninstall both plugins first, then re-run the install command. + marketplaceUninstallCommand: + 'copilot plugin uninstall b2c@b2c-developer-tooling ; copilot plugin uninstall b2c-cli@b2c-developer-tooling', + // MCP config location for Copilot CLI is not documented — surfaced as a docs link. + }, { id: 'agentforce-vibes', label: 'Agentforce Vibes', @@ -108,6 +217,16 @@ export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ }, ]; +/** VS Code user-data dir, used to distinguish VS Code from the Copilot CLI (~/.copilot). */ +function getVsCodeUserDir(): string { + if (process.platform === 'darwin') { + return path.join(home, 'Library', 'Application Support', 'Code'); + } else if (process.platform === 'win32') { + return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code'); + } + return path.join(home, '.config', 'Code'); +} + function getAgentforceVibesGlobalDir(): string { if (process.platform === 'darwin') { return path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage'); @@ -139,6 +258,60 @@ async function dirHasB2cSkills(dir: string): Promise { } } +/** Server-container keys used across the MCP config formats we support. */ +const MCP_CONTAINER_KEYS = ['mcpServers', 'servers'] as const; + +/** + * Returns true if the given MCP config file registers a `b2c-dx-mcp` server. + * Prefers a structural check — parse the JSON and look for a `b2c-dx-mcp` key + * inside a `mcpServers` (Cursor/Gemini/Antigravity) or `servers` (VS Code + * Copilot) container — so a disabled/commented mention or an unrelated string + * in a broad settings file (`~/.gemini/settings.json`, `~/.claude.json`) + * doesn't count as "configured". Falls back to a substring match only when the + * file isn't parseable JSON. Missing/unreadable files → false. + */ +async function fileRegistersB2cMcp(file: string): Promise { + let text: string; + try { + text = await fs.readFile(file, 'utf8'); + } catch { + return false; + } + if (!text.includes('b2c-dx-mcp')) return false; + try { + const parsed = JSON.parse(text) as Record; + return MCP_CONTAINER_KEYS.some((key) => { + const container = parsed[key]; + return !!container && typeof container === 'object' && 'b2c-dx-mcp' in container; + }); + } catch { + // Not valid JSON (e.g. JSONC with comments) — the substring is our best signal. + return true; + } +} + +/** + * Resolve the MCP-server state for a target across its documented config + * locations. Targets with no known MCP path resolve to `'unknown'` so the UI + * can offer a manual-setup hint instead of a misleading "not configured". + */ +export async function detectMcpStatus(target: AiSkillsTarget, workspaceRoots: string[] = []): Promise { + const hasKnownLocation = !!(target.mcpProjectConfig || target.mcpGlobalConfig || target.mcpMarkerDir); + if (!hasKnownLocation) return 'unknown'; + + if (target.mcpMarkerDir && (await pathExists(target.mcpMarkerDir))) return 'configured'; + + if (target.mcpProjectConfig) { + for (const root of workspaceRoots) { + if (await fileRegistersB2cMcp(path.join(root, target.mcpProjectConfig))) return 'configured'; + } + } + + if (target.mcpGlobalConfig && (await fileRegistersB2cMcp(target.mcpGlobalConfig))) return 'configured'; + + return 'not-configured'; +} + /** * Returns the install state for a single target. * - `not-installed` — no IDE installed @@ -153,6 +326,10 @@ export async function detectIdeStatus(target: AiSkillsTarget, workspaceRoots: st const idePresent = await pathExists(target.detectPath); if (!idePresent) return 'not-installed'; + // Marketplace-plugin targets (e.g. Copilot CLI) install skills outside the + // per-IDE skills dir — check their dedicated marker dir for a b2c* entry. + if (target.skillsMarkerDir && (await dirHasB2cSkills(target.skillsMarkerDir))) return 'skills-installed'; + // Project-scoped checks (one per workspace folder). for (const root of workspaceRoots) { const dir = path.join(root, target.projectSkillsDir); @@ -167,15 +344,21 @@ export async function detectIdeStatus(target: AiSkillsTarget, workspaceRoots: st export interface DetectedTarget extends AiSkillsTarget { status: IdeStatus; + mcpStatus: McpStatus; } /** * Detect status for every target in parallel. Pulls workspace roots from - * VS Code so project-scoped skill installs are picked up. + * VS Code so project-scoped skill and MCP installs are picked up. */ export async function detectAllTargets(): Promise { const roots = (vscode.workspace.workspaceFolders ?? []).map((f) => f.uri.fsPath); - return Promise.all(AI_SKILL_TARGETS.map(async (t) => ({...t, status: await detectIdeStatus(t, roots)}))); + return Promise.all( + AI_SKILL_TARGETS.map(async (t) => { + const [status, mcpStatus] = await Promise.all([detectIdeStatus(t, roots), detectMcpStatus(t, roots)]); + return {...t, status, mcpStatus}; + }), + ); } const escape = (s: string): string => @@ -183,10 +366,12 @@ const escape = (s: string): string => c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''', ); +const CHECK_SVG = ``; + function statusPill(status: IdeStatus): string { switch (status) { case 'skills-installed': - return `Ready · Skills installed`; + return `${CHECK_SVG}Ready · Skills installed`; case 'ide-present': return `IDE detected`; case 'not-installed': @@ -194,6 +379,22 @@ function statusPill(status: IdeStatus): string { } } +/** + * Small secondary badge showing MCP-server state. Only rendered when the IDE + * is present (an "MCP configured" claim is meaningless for an absent IDE). + * `unknown` targets (no documented MCP path) get a neutral "manual" hint. + */ +function mcpPill(status: McpStatus): string { + switch (status) { + case 'configured': + return `${CHECK_SVG}MCP configured`; + case 'not-configured': + return `MCP not set up`; + case 'unknown': + return `MCP · manual`; + } +} + /** Per-IDE icon SVG. Stylised glyphs only — no logos to avoid trademark issues. */ function ideIcon(id: string): string { switch (id) { @@ -215,6 +416,15 @@ function ideIcon(id: string): string { case 'opencode': // Open hexagon return ``; + case 'gemini-cli': + // Four-point sparkle (Gemini-ish accent) + return ``; + case 'antigravity': + // Upward orbit (anti-gravity) + return ``; + case 'copilot-cli': + // Terminal prompt (CLI surface of Copilot) + return ``; case 'agentforce-vibes': // Spark with cloud return ``; @@ -230,43 +440,71 @@ export function generateAiSkillsHtml(targets: DetectedTarget[]): string { return order[a.status] - order[b.status]; }); + const downloadSvg = ``; + const cards = sorted .map((t) => { const isInstalled = t.status === 'skills-installed'; const isReady = t.status === 'ide-present'; const isMissing = t.status === 'not-installed'; + // Marketplace-mode targets (GitHub Copilot CLI) install skills via a + // `copilot plugin install` command, not `b2c setup skills --ide` — that + // IDE value doesn't exist, so wiring the file-copy button would just fail. + const marketplaceMode = t.skillsMode === 'marketplace'; - const skillsLabel = isInstalled ? 'Reinstall' : isReady ? 'Install Skills' : 'Install Skills'; + const skillsLabel = isInstalled ? 'Reinstall' : 'Install Skills'; const skillsDisabled = isMissing ? 'disabled' : ''; const skillsClass = isReady ? 'ai-btn ai-btn--primary' : isInstalled ? 'ai-btn ai-btn--ghost' : 'ai-btn'; + const skillsIcon = isReady || isInstalled ? downloadSvg : ''; + + // Primary skills action: file-copy installer, or the marketplace command. + // For a marketplace Reinstall, uninstall the plugins first so the install + // is clean (uninstall → install); a fresh install just runs install. + const marketplaceCmd = + isInstalled && t.marketplaceUninstallCommand + ? `${t.marketplaceUninstallCommand} ; ${t.marketplaceCommand ?? ''}` + : (t.marketplaceCommand ?? ''); + const primaryAction = marketplaceMode + ? `` + : ``; const secondaryActions: string[] = []; - if (t.marketplaceCommand) { + // In marketplace mode the plugin command IS the primary action — don't repeat it. + if (t.marketplaceCommand && !marketplaceMode) { secondaryActions.push( ``, ); } if (t.mcpCommand) { + // Re-running the command overwrites the config, so label it as such + // once the server is already registered. + const mcpLabel = t.mcpStatus === 'configured' ? 'Re-configure MCP' : 'Install MCP'; secondaryActions.push( - ``, + ``, + ); + } else if (!isMissing) { + // No one-click MCP path documented for this IDE — link to setup docs + // instead of leaving the user with no MCP action at all. + secondaryActions.push( + `MCP setup guide`, ); } + // Show the MCP badge next to the skills pill once the IDE is present. + const mcpBadge = isMissing ? '' : mcpPill(t.mcpStatus); + return ` -
+
${ideIcon(t.id)}

${escape(t.label)}

- ${statusPill(t.status)} +
${statusPill(t.status)}${mcpBadge}

${escape(t.description)}

- + ${primaryAction} ${secondaryActions.length ? `
${secondaryActions.join('')}
` : ''}
`; @@ -275,6 +513,7 @@ export function generateAiSkillsHtml(targets: DetectedTarget[]): string { const installedCount = targets.filter((t) => t.status !== 'not-installed').length; const skillsInstalledCount = targets.filter((t) => t.status === 'skills-installed').length; + const mcpConfiguredCount = targets.filter((t) => t.mcpStatus === 'configured').length; return `