From d694d31e7d23a50d665b572dd392fad1c00d85e0 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 18 Sep 2026 12:25:44 -0400 Subject: [PATCH 1/3] feat: connect IDE context to VS Code and Cursor agents --- .changeset/retire-preview-walkthrough.md | 5 + .changeset/vscode-agent-context.md | 7 + docs/mcp/configuration.md | 19 + docs/mcp/toolsets.md | 7 +- docs/vscode-extension/configuration.md | 36 +- docs/vscode-extension/index.md | 4 + guidance/mcp/b2c-config/SKILL.md | 18 + packages/b2c-dx-mcp/package.json | 3 +- packages/b2c-dx-mcp/src/commands/mcp.ts | 11 + packages/b2c-dx-mcp/src/registry.ts | 5 +- packages/b2c-dx-mcp/src/tools/ide-context.ts | 105 + packages/b2c-dx-mcp/src/utils/types.ts | 2 + .../b2c-dx-mcp/test/tools/ide-context.test.ts | 148 + .../b2c-tooling-sdk/data/tooling/index.json | 8 +- packages/b2c-vs-extension/README.md | 8 +- .../.vscode-walkthrough-reference.md | 320 --- .../media/walkthrough/README.md | 110 - .../media/walkthrough/ai-skills.md | 21 - .../media/walkthrough/cartridge-structure.md | 101 - .../media/walkthrough/code-sync.md | 131 - .../media/walkthrough/deploy-cartridge.md | 88 - .../media/walkthrough/dw-json-setup.md | 73 - .../media/walkthrough/install-cli.md | 41 - .../media/walkthrough/next-steps.md | 33 - .../media/walkthrough/oauth-setup.md | 51 - .../media/walkthrough/sandbox-explorer.md | 117 - .../media/walkthrough/webdav-browser.md | 66 - .../media/walkthrough/welcome-hero-dark.svg | 241 -- .../media/walkthrough/welcome-hero-light.svg | 245 -- .../media/walkthrough/welcome.md | 21 - packages/b2c-vs-extension/package.json | 355 +-- .../scripts/esbuild-bundle.mjs | 2 + .../b2c-vs-extension/src/ai/cursor-mcp.ts | 85 + .../src/ai/ide-context-bridge.ts | 70 + .../b2c-vs-extension/src/ai/ide-context.ts | 67 + packages/b2c-vs-extension/src/ai/index.ts | 61 + .../b2c-vs-extension/src/ai/mcp-provider.ts | 53 + .../b2c-vs-extension/src/build-constants.d.ts | 1 + .../src/code-sync/code-sync-manager.ts | 6 + .../b2c-vs-extension/src/code-sync/index.ts | 3 +- .../b2c-vs-extension/src/config-provider.ts | 18 +- packages/b2c-vs-extension/src/extension.ts | 355 +-- .../src/{walkthrough => setup}/commands.ts | 70 +- .../src/test/ai-context.test.ts | 241 ++ .../src/test/ide-context-bridge.test.ts | 46 + .../src/test/integration/activation.test.ts | 39 +- .../src/walkthrough/accessibility.ts | 287 -- .../src/walkthrough/aiSkillsContent.ts | 448 --- .../b2c-vs-extension/src/walkthrough/index.ts | 26 - .../src/walkthrough/markdown.ts | 199 -- .../src/walkthrough/onboardingPanel.ts | 2503 ----------------- .../src/walkthrough/personas.ts | 234 -- .../b2c-vs-extension/src/walkthrough/state.ts | 113 - .../src/walkthrough/stepDetection.ts | 171 -- .../src/walkthrough/telemetry.ts | 192 -- .../src/walkthrough/test/commands.test.ts | 200 -- .../src/walkthrough/toolDetection.ts | 282 -- .../src/walkthrough/validator.ts | 348 --- pnpm-lock.yaml | 3 + 59 files changed, 1322 insertions(+), 7201 deletions(-) create mode 100644 .changeset/retire-preview-walkthrough.md create mode 100644 .changeset/vscode-agent-context.md create mode 100644 packages/b2c-dx-mcp/src/tools/ide-context.ts create mode 100644 packages/b2c-dx-mcp/test/tools/ide-context.test.ts delete mode 100644 packages/b2c-vs-extension/media/walkthrough/.vscode-walkthrough-reference.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/README.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/ai-skills.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/cartridge-structure.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/code-sync.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/deploy-cartridge.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/dw-json-setup.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/install-cli.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/next-steps.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/oauth-setup.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/sandbox-explorer.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/webdav-browser.md delete mode 100644 packages/b2c-vs-extension/media/walkthrough/welcome-hero-dark.svg delete mode 100644 packages/b2c-vs-extension/media/walkthrough/welcome-hero-light.svg delete mode 100644 packages/b2c-vs-extension/media/walkthrough/welcome.md create mode 100644 packages/b2c-vs-extension/src/ai/cursor-mcp.ts create mode 100644 packages/b2c-vs-extension/src/ai/ide-context-bridge.ts create mode 100644 packages/b2c-vs-extension/src/ai/ide-context.ts create mode 100644 packages/b2c-vs-extension/src/ai/index.ts create mode 100644 packages/b2c-vs-extension/src/ai/mcp-provider.ts rename packages/b2c-vs-extension/src/{walkthrough => setup}/commands.ts (96%) create mode 100644 packages/b2c-vs-extension/src/test/ai-context.test.ts create mode 100644 packages/b2c-vs-extension/src/test/ide-context-bridge.test.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/accessibility.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/index.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/markdown.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/onboardingPanel.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/personas.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/state.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/stepDetection.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/telemetry.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/test/commands.test.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/toolDetection.ts delete mode 100644 packages/b2c-vs-extension/src/walkthrough/validator.ts diff --git a/.changeset/retire-preview-walkthrough.md b/.changeset/retire-preview-walkthrough.md new file mode 100644 index 000000000..08b2b277e --- /dev/null +++ b/.changeset/retire-preview-walkthrough.md @@ -0,0 +1,5 @@ +--- +'b2c-vs-extension': patch +--- + +Remove the preview walkthrough, role-based onboarding panel, and agent-install instructions. Standalone configuration and CLI setup commands remain available as beta features, disabled by default; enable `b2c-dx.features.setup` and reload the editor to use them. diff --git a/.changeset/vscode-agent-context.md b/.changeset/vscode-agent-context.md new file mode 100644 index 000000000..a1dc37ac9 --- /dev/null +++ b/.changeset/vscode-agent-context.md @@ -0,0 +1,7 @@ +--- +'b2c-vs-extension': minor +'@salesforce/b2c-dx-mcp': minor +'@salesforce/b2c-dx-docs': patch +--- + +Connect VS Code and Cursor chat to the B2C Commerce MCP server and expose the IDE's selected instance, project, and live code-sync status. Assistants can use that selection unless you specify another target; Cursor receives live context through an optional connection to the running extension. diff --git a/docs/mcp/configuration.md b/docs/mcp/configuration.md index c872e566a..56a441b7e 100644 --- a/docs/mcp/configuration.md +++ b/docs/mcp/configuration.md @@ -33,6 +33,25 @@ startup default, use the shared `--project-directory`, `--config`, or `--instanc options. See [Configuration](../guide/configuration) for their values and file formats. These defaults do not restrict which projects the assistant can access. +### IDE selection + +The [IDE Extension](../vscode-extension/configuration#ai-chat) registers the +Commerce MCP server in VS Code and Cursor. In Cursor, it also supplies a private +connection for reading the selected instance and live code-sync status. VS Code +provides the same context through the extension's native chat tool. + +The optional `--ide-context-url` launch flag and `SFCC_IDE_CONTEXT_TOKEN` +environment variable are supplied together by the extension. They identify one +running editor window and are not project configuration to save or share. The +MCP process must run on the same host as the extension. Without that connection, +the MCP server does not expose IDE context. Restarting the editor requires a new +connection; a failed connection never substitutes the shared default instance. + +The assistant can use this context unless you explicitly select another target. +This does not automatically override every MCP operation. Configuration +inspection still reports what the MCP process resolves, which can differ from +the editor's credentials or environment. + ## Tools and toolsets {#toolset-selection} All toolsets are enabled by default. Use names from [MCP Tools](./toolsets) to diff --git a/docs/mcp/toolsets.md b/docs/mcp/toolsets.md index 7583a8742..af904c3c1 100644 --- a/docs/mcp/toolsets.md +++ b/docs/mcp/toolsets.md @@ -252,9 +252,10 @@ See [analytics access](./security#cip). ## Configuration inspection -| Tool | Capability | Toolsets | -| ---------------- | ------------------------------------------------------------------------ | ---------------- | -| `config_inspect` | Check resolved configuration and targets; secrets are masked by default. | DIAGNOSTICS, CIP | +| Tool | Capability | Toolsets | +| --------------------- | ---------------------------------------------------------------------------------------------- | ------------------- | +| `config_inspect` | Check resolved configuration and targets; secrets are masked by default. | DIAGNOSTICS, CIP | +| `b2c_get_ide_context` | Read the selected IDE instance and live code-sync status when launched with an IDE connection. | All, when connected | ## Toolsets for customization diff --git a/docs/vscode-extension/configuration.md b/docs/vscode-extension/configuration.md index 3e9c60b72..0cc6d03a0 100644 --- a/docs/vscode-extension/configuration.md +++ b/docs/vscode-extension/configuration.md @@ -11,6 +11,7 @@ This page covers: - [Connecting to a B2C Instance](#connecting-to-a-b2c-instance) — credentials per feature. - [How the Extension Chooses a Project](#how-the-extension-chooses-a-project) — parent folders and multi-root workspaces. - [Selecting an Instance](#selecting-an-instance) — workspace-specific and shared defaults. +- [AI Chat](#ai-chat) — MCP setup and the current IDE context. - [Safety Mode](#safety-mode) — restrict changes and require confirmation for selected actions. - [Settings Reference](#settings-reference) — the `b2c-dx.*` toggles and verbosity controls. @@ -29,7 +30,7 @@ For the selected project, the extension loads all variables from its `.env` and The global default is the same fallback used by the CLI and MCP server. The extension automatically refreshes when that shared setting changes. -The extension's instance picker combines instances from the primary and global files. Same-name primary entries shadow global entries, and each instance remains a complete entry rather than having fields merged across files. Switching an instance updates the file that owns it and clears the previous active selection across the catalog. +The extension's instance picker combines instances from the primary and global files. Same-name primary entries shadow global entries, and each instance remains a complete entry rather than having fields merged across files. Setting the shared default updates the file that owns it and clears the previous active selection across the catalog. Selecting an instance only for this workspace leaves those files unchanged. ### Per-feature requirements @@ -100,12 +101,38 @@ To keep a particular project directory selected, right-click that folder in Expl ## Selecting an Instance -When your configuration defines multiple named instances (the recommended pattern for working across dev / staging / sandbox), click the cloud icon in the status bar to open a quick pick. Selecting an instance applies it only to the current VS Code workspace and refreshes every extension view. Other VS Code workspaces, the CLI, and MCP continue using their own selection or the shared default. +When your configuration defines multiple named instances (the recommended pattern for working across dev / staging / sandbox), click the cloud icon in the status bar to open a quick pick. Selecting an instance applies it only to the current VS Code workspace and refreshes every extension view. Other editor workspaces and the CLI continue using their own selection or the shared default. See [AI Chat](#ai-chat) for how assistants use the IDE selection. The picker distinguishes the instance **selected for this workspace** with a check mark and the shared **default instance** with a star. Use the star action on a row—or run **B2C DX: Set Default Instance**—to intentionally change the default used by other consumers. Run **B2C DX: Follow Default Instance** to remove the workspace-specific selection. For named entries, setting the default writes `active: true`; a root configuration without an explicit `active` value remains an implicit default. This is equivalent to running `b2c setup instance set-active ` and is separate from selecting an instance only for VS Code. +## AI Chat + +The extension makes the **B2C Commerce MCP server** available in VS Code and Cursor without creating an MCP configuration file. In a trusted workspace, enable the server and its tools in your editor's chat settings. The default launcher requires Node.js 22 or later and `npx` on the extension host's PATH; it downloads the MCP version matched to the extension. Remote workspaces need these prerequisites on the remote host. + +In VS Code, attach **#b2cContext** to ask about the selected instance or code-sync status. In Cursor, the registered **salesforce-b2c-commerce** server exposes the same context through an optional connection to the extension. Context is read live from the editor window and includes connection metadata, never credentials. + + + +> Check the logs on my selected B2C instance. Is code sync currently active for that instance? + + + +Assistants can use the current IDE selection unless you specify another target. The context includes the project root, configuration file, instance name, hostname, configured code version, and whether code sync is actually running. When active, code sync reports its upload hostname and code version separately. + +This is guidance for the assistant, not an enforced binding of every operation to the status bar. Server launch defaults reflect the IDE selection at discovery/startup. After switching instances, the context tool reports the new selection; refresh/restart the Commerce MCP server if you need its launch defaults refreshed. Updating registration in Cursor can restart its server. Existing debug and log sessions do not move to the newly selected instance; a server restart ends those sessions. Environment overrides and credentials available only in the editor can also cause MCP resolution to differ. + +The native context tool is specific to VS Code chat integrations that consume extension tools. Cursor uses its own MCP registration API. Other assistants sharing the directory do not automatically inherit editor context. In remote workspaces, the MCP process and extension host must run on the same host. + +| Setting | Default | Purpose | +| -------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `b2c-dx.mcp.enabled` | `true` | Register MCP servers with the editor. Disable when managing the Commerce server yourself to avoid duplicate registrations. In Cursor this also closes the IDE-context connection. | +| `b2c-dx.mcp.command` | `npx` | MCP executable on the extension host. | +| `b2c-dx.mcp.args` | `null` | Optional replacement launcher arguments. By default, uses the MCP package version matched to the extension. The extension appends project, configuration, and instance flags. | + +The command and argument settings are machine-scoped. For local MCP development, set the command to `node` and the arguments to the absolute path of the built MCP package's `bin/run.js`. + ## Safety Mode The extension honors the selected instance's `safety` settings in `dw.json`. @@ -119,11 +146,13 @@ There is no separate safety-level toggle in VS Code Settings. ## Settings Reference +Standalone configuration and CLI setup commands are beta features, disabled by default. Enable `b2c-dx.features.setup` and reload the window to use them from the Command Palette. The retired guided walkthrough and role-based onboarding panel are no longer included. + These VS Code settings live under the `b2c-dx.*` namespace. **You usually don't need to change any of them** — they exist for niche cases like disabling a feature you don't use, or quieting the log channel for a bug report. To browse: **Settings** (Cmd+,) → search for `b2c-dx`. ### Feature toggles -Each feature is enabled by default. Set to `false` to skip its activation entirely (no tree views, no commands, no context-menu entries). Useful for trimming the UI, isolating activation issues, or running in a project where a feature isn't applicable. +Most features are enabled by default; beta setup commands are off by default. Set a feature to `false` to skip its activation entirely (no tree views, no commands, no context-menu entries). Useful for trimming the UI, isolating activation issues, or running in a project where a feature isn't applicable. | Setting | Default | | ---------------------------------- | ------- | @@ -135,6 +164,7 @@ Each feature is enabled by default. Set to `false` to skip its activation entire | `b2c-dx.features.scaffold` | `true` | | `b2c-dx.features.apiBrowser` | `true` | | `b2c-dx.features.cap` | `true` | +| `b2c-dx.features.setup` | `false` | The B2C Script Debugger registers regardless of these toggles — it activates only when a `b2c-script` launch configuration is used. diff --git a/docs/vscode-extension/index.md b/docs/vscode-extension/index.md index 383b966e9..7444dd3de 100644 --- a/docs/vscode-extension/index.md +++ b/docs/vscode-extension/index.md @@ -24,6 +24,10 @@ Available on the [Visual Studio Marketplace](https://marketplace.visualstudio.co ## Highlights +### AI Chat Context + +Use B2C Commerce tools in VS Code and Cursor chat with MCP registration supplied by the extension. Assistants can read the selected instance, project root, and live code-sync status. In VS Code, reference **#b2cContext** explicitly; Cursor receives the same context through the Commerce MCP server. See [AI Chat configuration](./configuration#ai-chat) for setup and targeting behavior. + ### ISML and Script API Editor Support Write storefront code with ISML syntax highlighting, snippets, formatting, tag completion, diagnostics, and Emmet support. Cartridge JavaScript files automatically provide autocomplete and hover documentation for `dw/*` modules without writing a `jsconfig.json` into your project. See the [Script API IntelliSense guide](../guide/ide-integration#script-api-intellisense) for more detail. diff --git a/guidance/mcp/b2c-config/SKILL.md b/guidance/mcp/b2c-config/SKILL.md index fb39475f7..42942c263 100644 --- a/guidance/mcp/b2c-config/SKILL.md +++ b/guidance/mcp/b2c-config/SKILL.md @@ -9,6 +9,24 @@ For MCP installation or tool selection, see [server setup](skill://mcp/server/SK ## Inspect resolved values +When `b2c_get_ide_context` is available from the IDE extension and the user has +not specified a target, read it before configuration-dependent calls. Pass its +`projectDirectory`, `configPath`, and `instanceName` to the MCP tools. The IDE +selection can differ from the active entry on disk; names alone are not unique +across configuration files. Explicit user targets take precedence: resolve them +independently rather than combining another instance name with the IDE's file. +If IDE context is unavailable or unconfigured, report that instead of guessing +or falling back silently. Without the IDE tool, use normal MCP configuration. + +Refresh IDE context after a selection change or when beginning another task; +it is a snapshot, not a persistent binding. Compare returned `resolution` with +the intended target. Environment/plugin overrides and different credential +stores can make IDE and MCP resolution differ. Stop on a target mismatch before +mutating data. Existing debugger/log sessions retain their original targets. +`codeSync.active` reports the actual watcher state, not the auto-upload setting; +its hostname/code version can differ while an old upload is draining. Do not +assume active sync means every file has finished uploading. + Call `config_inspect` directly with the task's absolute `projectDirectory`. No prior skill read is required. Secrets are masked by default (`unmask: false`). The result includes effective values, contributing sources, warnings, and diff --git a/packages/b2c-dx-mcp/package.json b/packages/b2c-dx-mcp/package.json index 40130bbcc..899b88597 100644 --- a/packages/b2c-dx-mcp/package.json +++ b/packages/b2c-dx-mcp/package.json @@ -137,7 +137,8 @@ "sinon": "catalog:", "tsx": "catalog:", "typescript": "catalog:", - "typescript-eslint": "catalog:" + "typescript-eslint": "catalog:", + "msw": "catalog:" }, "engines": { "node": ">=22.16.0" diff --git a/packages/b2c-dx-mcp/src/commands/mcp.ts b/packages/b2c-dx-mcp/src/commands/mcp.ts index 75cc6c879..465695195 100644 --- a/packages/b2c-dx-mcp/src/commands/mcp.ts +++ b/packages/b2c-dx-mcp/src/commands/mcp.ts @@ -153,6 +153,7 @@ import {registerToolsets} from '../registry.js'; import {TOOLSETS, type StartupFlags} from '../utils/index.js'; import type {ProjectContextInput} from '../tools/project-context.js'; import type {ServicesLoader} from '../tools/adapter.js'; +import {validateIdeContextConnection} from '../tools/ide-context.js'; /** * oclif Command that starts the B2C DX MCP server. @@ -193,6 +194,9 @@ export default class McpServerCommand extends BaseCommand s.trim()) : undefined, tools: this.flags.tools ? this.flags.tools.split(',').map((s) => s.trim()) : undefined, configPath: this.flags.config, @@ -407,6 +414,7 @@ export default class McpServerCommand extends BaseCommand"}) when enabled; not native skill commands. ' + 'SCAPI: first read skill://mcp/scapi/SKILL.md. ' + 'Analytics: cip_discover/cip_query; skill://mcp/cip/SKILL.md. ' + diff --git a/packages/b2c-dx-mcp/src/registry.ts b/packages/b2c-dx-mcp/src/registry.ts index ed3da3102..c8a7c3796 100644 --- a/packages/b2c-dx-mcp/src/registry.ts +++ b/packages/b2c-dx-mcp/src/registry.ts @@ -20,6 +20,7 @@ import {createScapiTools} from './tools/scapi/index.js'; import {createWebDavTools} from './tools/webdav/index.js'; import {createCipTools} from './tools/cip/index.js'; import {createGuidanceTool, registerGuidanceResources} from './guidance.js'; +import {createIdeContextTool, type IdeContextConnection} from './tools/ide-context.js'; /** * Registry of tools organized by toolset. @@ -40,6 +41,7 @@ export function createToolRegistry( serverContext?: ServerContext, detectedWorkspaces: readonly ProjectType[] = [], enabledDocCategories?: readonly DocCategory[], + ideContext?: IdeContextConnection, ): ToolRegistry { const registry: ToolRegistry = { CARTRIDGES: [], @@ -53,6 +55,7 @@ export function createToolRegistry( // Collect all tools from all factories const allTools: McpTool[] = [ + ...(ideContext ? [createIdeContextTool(ideContext)] : []), ...createCartridgesTools(loadServices), ...createWebDavTools(loadServices), ...createCipTools(loadServices), @@ -108,7 +111,7 @@ export async function registerToolsets( } // Tool availability is independent of the workspace. Explicit selection customizes the default catalog. - const toolRegistry = createToolRegistry(loadServices, serverContext, [], enabledDocCategories); + const toolRegistry = createToolRegistry(loadServices, serverContext, [], enabledDocCategories, flags.ideContext); const existingToolNames = new Set( Object.values(toolRegistry) .flat() diff --git a/packages/b2c-dx-mcp/src/tools/ide-context.ts b/packages/b2c-dx-mcp/src/tools/ide-context.ts new file mode 100644 index 000000000..ad0c5333e --- /dev/null +++ b/packages/b2c-dx-mcp/src/tools/ide-context.ts @@ -0,0 +1,105 @@ +/* + * 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 {z} from 'zod'; +import type {McpTool} from '../utils/index.js'; +import {TOOLSETS} from '../utils/constants.js'; +import {errorResult, jsonResult} from './adapter.js'; + +export interface IdeContextConnection { + url: string; + token: string; +} + +const contextSchema = z.object({ + status: z.enum(['ready', 'unconfigured', 'unavailable']), + selectionMode: z.enum(['workspace', 'default']), + projectDirectory: z.string().optional(), + projectRootPinned: z.boolean(), + configPath: z.string().optional(), + instanceName: z.string().optional(), + hostname: z.string().optional(), + codeVersion: z.string().optional(), + codeSync: z.object({ + available: z.boolean(), + active: z.boolean(), + hostname: z.string().optional(), + codeVersion: z.string().optional(), + }), +}); + +export function validateIdeContextConnection(connection: IdeContextConnection): void { + const url = new URL(connection.url); + if ( + url.protocol !== 'http:' || + url.hostname !== '127.0.0.1' || + !url.port || + url.pathname !== '/context' || + url.username || + url.password || + url.search || + url.hash + ) { + throw new Error('IDE context must use the extension-provided http://127.0.0.1:/context endpoint.'); + } + if (!connection.token) throw new Error('SFCC_IDE_CONTEXT_TOKEN is required with --ide-context-url.'); +} + +/** Reads the editor's state independently of Commerce configuration resolution. */ +export function createIdeContextTool(connection: IdeContextConnection): McpTool { + validateIdeContextConnection(connection); + return { + name: 'b2c_get_ide_context', + description: + 'Read the live B2C IDE selection and code-sync status. Unless the user specifies another target, ' + + 'pass its projectDirectory, configPath, and instanceName to configuration-dependent B2C tools. ' + + 'Refresh after selection changes; do not guess unavailable selections or retarget existing sessions. ' + + 'Does not expose credentials, inspect MCP-resolved configuration, or change the selected instance.', + effect: 'read', + idempotent: true, + openWorld: false, + toolsets: [...TOOLSETS], + inputSchema: {}, + outputSchema: contextSchema.shape, + async handler(_args, context) { + const signal = AbortSignal.any([AbortSignal.timeout(5000), ...(context?.signal ? [context.signal] : [])]); + try { + const response = await fetch(connection.url, { + headers: {Authorization: `Bearer ${connection.token}`}, + redirect: 'error', + signal, + }); + if (!response.ok) { + response.body?.cancel().catch(() => {}); + throw new Error('IDE bridge unavailable'); + } + const reader = response.body?.getReader(); + if (!reader) throw new Error('Empty IDE context'); + const chunks: Uint8Array[] = []; + let length = 0; + try { + for (;;) { + // A bounded streaming read must consume chunks in order. + // eslint-disable-next-line no-await-in-loop + const {done, value} = await reader.read(); + if (done) break; + length += value.byteLength; + if (length > 64 * 1024) throw new Error('IDE context too large'); + chunks.push(value); + } + } finally { + reader.cancel().catch(() => {}); + } + // Strip unexpected fields, including any accidental credential fields. + const selected = contextSchema.parse(JSON.parse(Buffer.concat(chunks).toString('utf8'))); + return {...jsonResult(selected), structuredContent: selected}; + } catch { + return errorResult( + 'Could not read live IDE context. Reconnect the MCP server from the editor or ask for an explicit target; do not assume the default instance matches the IDE.', + ); + } + }, + }; +} diff --git a/packages/b2c-dx-mcp/src/utils/types.ts b/packages/b2c-dx-mcp/src/utils/types.ts index b5d901210..1c6c14594 100644 --- a/packages/b2c-dx-mcp/src/utils/types.ts +++ b/packages/b2c-dx-mcp/src/utils/types.ts @@ -51,6 +51,8 @@ export interface McpTool extends McpToolCon * Startup flags passed to the MCP server. */ export interface StartupFlags { + /** Optional private connection to the editor that launched this server. */ + ideContext?: {url: string; token: string}; /** Comma-separated list of toolsets to enable */ toolsets?: string[]; /** Specific individual tools to enable */ diff --git a/packages/b2c-dx-mcp/test/tools/ide-context.test.ts b/packages/b2c-dx-mcp/test/tools/ide-context.test.ts new file mode 100644 index 000000000..b3fa4aba8 --- /dev/null +++ b/packages/b2c-dx-mcp/test/tools/ide-context.test.ts @@ -0,0 +1,148 @@ +/* + * 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 {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {createIdeContextTool, validateIdeContextConnection} from '../../src/tools/ide-context.js'; +import {createToolRegistry, registerToolsets} from '../../src/registry.js'; +import {Client, InMemoryTransport} from '@modelcontextprotocol/client'; +import {B2CDxMcpServer} from '../../src/server.js'; +import type {ToolResult} from '../../src/utils/index.js'; + +const connection = {url: 'http://127.0.0.1:43210/context', token: 'test-bridge-token'}; +const server = setupServer(); +const snapshot = (instanceName = 'development') => ({ + status: 'ready', + selectionMode: 'workspace', + instanceName, + projectDirectory: '/project', + configPath: '/global/dw.json', + projectRootPinned: true, + hostname: `${instanceName}.invalid`, + codeSync: {available: true, active: true}, +}); +const json = (result: ToolResult) => JSON.parse(result.content[0].type === 'text' ? result.content[0].text : '{}'); + +describe('IDE context bridge tool', () => { + before(() => server.listen({onUnhandledRequest: 'error'})); + + afterEach(() => server.resetHandlers()); + + after(() => server.close()); + + it('is present only with an explicit bridge connection and never resolves Commerce config', async () => { + const loadServices = () => { + throw new Error('must not resolve configuration'); + }; + const ordinary = createToolRegistry(loadServices); + expect( + Object.values(ordinary) + .flat() + .some((tool) => tool.name === 'b2c_get_ide_context'), + ).to.equal(false); + const connected = createToolRegistry(loadServices, undefined, [], undefined, connection); + const tool = connected.DIAGNOSTICS.find((entry) => entry.name === 'b2c_get_ide_context')!; + server.use(http.get(connection.url, () => HttpResponse.json(snapshot()))); + expect(json(await tool.handler({}))).to.deep.equal(snapshot()); + expect(tool.effect).to.equal('read'); + expect(tool.openWorld).to.equal(false); + }); + + it('authenticates each call, reads fresh state, and strips unexpected secret fields', async () => { + let selected = 'development'; + server.use( + http.get(connection.url, ({request}) => { + expect(request.headers.get('authorization')).to.equal(`Bearer ${connection.token}`); + return HttpResponse.json({ + ...snapshot(selected), + password: 'secret', + codeSync: {available: true, active: false, token: 'secret'}, + }); + }), + ); + const tool = createIdeContextTool(connection); + expect(json(await tool.handler({})).instanceName).to.equal('development'); + selected = 'staging'; + const result = await tool.handler({}); + expect(json(result).instanceName).to.equal('staging'); + expect(JSON.stringify(result)).not.to.include('secret'); + expect(json(result)).not.to.have.property('resolution'); + }); + + it('publishes the connected tool with read-only annotations through MCP', async () => { + const mcp = new B2CDxMcpServer({name: 'ide-test', version: '1.0.0'}); + const client = new Client({name: 'ide-test', version: '1.0.0'}); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await registerToolsets({ideContext: connection, tools: ['b2c_get_ide_context']}, mcp, () => { + throw new Error('must not resolve Commerce config'); + }); + server.use(http.get(connection.url, () => HttpResponse.json(snapshot('staging')))); + try { + await mcp.connect(serverTransport); + await client.connect(clientTransport); + const tools = await client.listTools(); + expect(tools.tools.map((tool) => tool.name)).to.deep.equal(['b2c_get_ide_context']); + expect(tools.tools[0].annotations).to.deep.equal({ + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }); + const result = await client.callTool({name: 'b2c_get_ide_context', arguments: {}}); + expect(result.isError, JSON.stringify(result)).not.to.equal(true); + expect(JSON.stringify(result)).to.include('staging.invalid'); + expect(JSON.stringify(result)).not.to.include(connection.token); + } finally { + await client.close(); + await mcp.close(); + } + }); + + it('preserves an unavailable IDE selection instead of substituting a default', async () => { + server.use( + http.get(connection.url, () => + HttpResponse.json({...snapshot('missing'), status: 'unavailable', hostname: undefined}), + ), + ); + const result = json(await createIdeContextTool(connection).handler({})); + expect(result.status).to.equal('unavailable'); + expect(result.instanceName).to.equal('missing'); + expect(result).not.to.have.property('hostname'); + }); + + for (const [label, response] of [ + ['disconnected', new HttpResponse(null, {status: 503})], + ['malformed', HttpResponse.json({password: 'do not echo'})], + ['oversized', new HttpResponse('x'.repeat(65_537))], + ['redirected', new HttpResponse(null, {status: 302, headers: {Location: 'https://example.com/context'}})], + ] as const) { + it(`fails closed on ${label} responses`, async () => { + server.use(http.get(connection.url, () => response)); + const result = await createIdeContextTool(connection).handler({}); + expect(result.isError).to.equal(true); + expect(JSON.stringify(result)).not.to.include('do not echo'); + expect(JSON.stringify(result)).not.to.include(connection.token); + }); + } + + it('cancels bridge reads with the tool invocation', async () => { + const controller = new AbortController(); + controller.abort(); + const result = await createIdeContextTool(connection).handler({}, {signal: controller.signal}); + expect(result.isError).to.equal(true); + }); + + it('rejects remote endpoints and missing bridge tokens before registering', () => { + for (const url of [ + 'https://example.com/context', + 'http://localhost:43210/context', + 'http://127.0.0.1:43210/context?token=x', + ]) { + expect(() => validateIdeContextConnection({...connection, url})).to.throw(/extension-provided/); + } + expect(() => validateIdeContextConnection({...connection, token: ''})).to.throw(/SFCC_IDE_CONTEXT_TOKEN/); + }); +}); diff --git a/packages/b2c-tooling-sdk/data/tooling/index.json b/packages/b2c-tooling-sdk/data/tooling/index.json index a66c2e2ee..1b86e1000 100644 --- a/packages/b2c-tooling-sdk/data/tooling/index.json +++ b/packages/b2c-tooling-sdk/data/tooling/index.json @@ -1,6 +1,6 @@ { "version": "2.0.0", - "generatedAt": "2026-09-15T13:55:15.255Z", + "generatedAt": "2026-09-18T15:58:03.133Z", "entries": [ { "id": "cli-account-manager", @@ -485,7 +485,7 @@ "category": "tooling", "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/configuration.html", "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/configuration.md", - "headings": "Startup configuration {#advanced-manual-configuration} • Project defaults {#project-directory} • Tools and toolsets {#toolset-selection} • Documentation topics {#documentation-tools-restriction} • Saved workflows {#saved-workflows} • Logging", + "headings": "Startup configuration {#advanced-manual-configuration} • Project defaults {#project-directory} • IDE selection • Tools and toolsets {#toolset-selection} • Documentation topics {#documentation-tools-restriction} • Saved workflows {#saved-workflows} • Logging", "preview": "Customize B2C MCP tools, documentation topics, startup defaults, and saved workflows." }, { @@ -521,7 +521,7 @@ "category": "tooling", "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/vscode-extension/configuration.html", "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/vscode-extension/configuration.md", - "headings": "Connecting to a B2C Instance • Per-feature requirements • Example `dw.json` • API Browser Setup • How the Extension Chooses a Project • Selecting an Instance • Safety Mode • Settings Reference • Feature toggles • Verbosity, polling, telemetry • XML schema validation • Complete defaults (copy-paste) • Next Steps", + "headings": "Connecting to a B2C Instance • Per-feature requirements • Example `dw.json` • API Browser Setup • How the Extension Chooses a Project • Selecting an Instance • AI Chat • Safety Mode • Settings Reference • Feature toggles • Verbosity, polling, telemetry • XML schema validation • Complete defaults (copy-paste) • Next Steps", "preview": "Connect the Salesforce B2C Commerce IDE Extension to a B2C Commerce instance — credentials, OAuth, telemetry, and the b2c-dx.* settings reference." }, { @@ -530,7 +530,7 @@ "category": "tooling", "url": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/vscode-extension/index.html", "sourceUrl": "https://salesforcecommercecloud.github.io/b2c-developer-tooling/vscode-extension/index.md", - "headings": "Highlights • ISML and Script API Editor Support • B2C Script Debugger • Scaffolding • Sandbox Realm Explorer • Library Explorer • Cartridge Management and Code Watch/Upload • SCAPI API Explorer • WebDAV Browser • Log Tailing • Active Instance Status Bar • B2C CLI Plugin Support • Next Steps", + "headings": "Highlights • AI Chat Context • ISML and Script API Editor Support • B2C Script Debugger • Scaffolding • Sandbox Realm Explorer • Library Explorer • Cartridge Management and Code Watch/Upload • SCAPI API Explorer • WebDAV Browser • Log Tailing • Active Instance Status Bar • B2C CLI Plugin Support • Next Steps", "preview": "The official Salesforce B2C Commerce IDE Extension for VS Code, Cursor, and other VS Code-compatible editors — code sync, sandbox management, API exploration, and debugging." }, { diff --git a/packages/b2c-vs-extension/README.md b/packages/b2c-vs-extension/README.md index 1da3e7cfc..4f4d765e4 100644 --- a/packages/b2c-vs-extension/README.md +++ b/packages/b2c-vs-extension/README.md @@ -51,7 +51,7 @@ Use **Setup Help** in the API Browser toolbar or any API tab for connection exam ### Stay in the development flow -Tail sandbox logs into a VS Code output channel, install Commerce App Packages, manage jobs, and keep the selected B2C instance visible in the status bar. Preview features such as Job History, site export, analytics, and guided onboarding can be enabled from the `b2c-dx.features.*` settings. +Tail sandbox logs into a VS Code output channel, install Commerce App Packages, manage jobs, and keep the selected B2C instance visible in the status bar. Preview features such as Job History, site export, and analytics can be enabled from the `b2c-dx.features.*` settings. ## Get started @@ -89,3 +89,9 @@ Development, testing, and packaging instructions are available in [DEVELOPMENT.m ## License Copyright (c) 2026, Salesforce, Inc. Licensed under the [Apache License 2.0](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/blob/main/license.txt). + +## AI Chat + +The extension registers the B2C Commerce MCP server with VS Code and Cursor. The default launcher requires Node.js 22+ and npx. VS Code chat can read the selected instance and live code-sync status through `#b2cContext`; Cursor gets the same information through the Commerce MCP server connected to the editor. + +Explicit user targets take precedence over IDE-context guidance. Context is read on demand; switching instances does not retarget existing debugger/log sessions. Set `b2c-dx.mcp.enabled` to `false` if you configure MCP separately. See [AI Chat configuration](https://salesforcecommercecloud.github.io/b2c-developer-tooling/vscode-extension/configuration#ai-chat). diff --git a/packages/b2c-vs-extension/media/walkthrough/.vscode-walkthrough-reference.md b/packages/b2c-vs-extension/media/walkthrough/.vscode-walkthrough-reference.md deleted file mode 100644 index 04888fe86..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/.vscode-walkthrough-reference.md +++ /dev/null @@ -1,320 +0,0 @@ -# VS Code Walkthrough Quick Reference - -This document provides technical reference for maintaining and extending the B2C DX walkthrough. - -## VS Code Walkthrough API - -### Package.json Structure - -```json -{ - "contributes": { - "walkthroughs": [ - { - "id": "unique.walkthrough.id", - "title": "Display Title", - "description": "Short description shown in welcome page", - "when": "context-condition", - "steps": [...] - } - ] - } -} -``` - -### Step Structure - -```json -{ - "id": "unique.step.id", - "title": "Step Title", - "description": "Markdown content with [links](command:commandId)", - "media": { - "markdown": "path/to/file.md" - // OR - "image": "path/to/image.png", - "altText": "Accessible description" - }, - "completionEvents": [ - "onCommand:commandId", - "onView:viewId", - "onContext:contextKey" - ] -} -``` - -## Completion Events - -### Available Event Types - -| Event Type | Syntax | Example | When It Fires | -|------------|--------|---------|---------------| -| Command | `onCommand:id` | `onCommand:b2c-dx.codeSync.deploy` | When command executes | -| View Open | `onView:id` | `onView:b2cWebdavExplorer` | When view becomes visible | -| Context | `onContext:key` | `onContext:workspaceContains:dw.json` | When context becomes true | -| Link | `onLink:uri` | `onLink:https://example.com` | When link is clicked | - -### Common Context Keys - -- `workspaceContains:pattern` - File matching pattern exists -- `editorLangId == language` - Active editor language -- `resourceExtname == .ext` - File extension matches -- Custom contexts set via `vscode.commands.executeCommand('setContext', key, value)` - -## Command Links in Markdown - -### Basic Command Link -```markdown -[Link Text](command:commandId) -``` - -### Command with Arguments -Arguments must be URL-encoded JSON: -```markdown -[Open File](command:workbench.action.quickOpen?%22filename.json%22) -``` - -To encode arguments: -```javascript -const args = ["filename.json"]; -const encoded = encodeURIComponent(JSON.stringify(args)); -// Use: command:commandId?${encoded} -``` - -### Common Built-in Commands - -| Command | Description | -|---------|-------------| -| `workbench.action.quickOpen?args` | Open Quick Open with pre-filled text | -| `workbench.action.openWalkthrough` | Open a specific walkthrough | -| `workbench.view.extension.id` | Open extension view container | -| `workbench.action.openSettings` | Open settings | -| `workbench.action.files.openFile` | Open file picker | - -## Media Types - -### Markdown Media -Best for text-heavy steps with formatting needs: -```json -{ - "media": { - "markdown": "path/to/content.md" - } -} -``` - -**Pros:** -- Rich formatting (headings, lists, code blocks) -- Easy to maintain and update -- No image assets needed -- Command links work naturally - -**Cons:** -- Less visual than images -- Can be text-heavy - -### Image Media -Best for visual demonstrations: -```json -{ - "media": { - "image": "path/to/image.png", - "altText": "Descriptive text for screen readers" - } -} -``` - -**Pros:** -- Highly visual -- Quick to understand -- Professional appearance - -**Cons:** -- Requires image creation/maintenance -- Can become outdated -- Larger file sizes - -### SVG Media -Best for diagrams and scalable graphics: -```json -{ - "media": { - "svg": "path/to/diagram.svg", - "altText": "Descriptive text" - } -} -``` - -**Pros:** -- Scalable (no pixelation) -- Small file size -- Can be version-controlled easily -- Can include text - -### Video Media (Experimental) -```json -{ - "media": { - "video": "path/to/video.mp4" - } -} -``` - -## Conditional Display - -### When Clause -Controls when walkthrough appears: - -```json -{ - "when": "workspaceFolderCount > 0" -} -``` - -Common conditions: -- `workspaceFolderCount > 0` - Workspace is open -- `!isWeb` - Not running in browser -- `extensionId:installed` - Another extension is installed - -### Step Visibility -Currently, steps cannot be conditionally hidden. All steps show in sequence. - -**Workaround:** Use description text to explain prerequisites: -```markdown -**Note:** This step requires OAuth credentials. If you skipped Step 3, come back later. -``` - -## Best Practices - -### Content Guidelines - -1. **Keep steps focused** - One main action per step -2. **Use active voice** - "Deploy your cartridge" not "Cartridges can be deployed" -3. **Provide context** - Explain *why* not just *how* -4. **Include examples** - Code snippets, sample configs -5. **Add troubleshooting** - Common errors and solutions - -### Writing Descriptions - -```markdown -# Good -Configure your instance by creating a `dw.json` file. - -[Create dw.json](command:b2c-dx.walkthrough.createDwJson) - -**Tip:** Add dw.json to .gitignore to avoid committing credentials. - -# Less Good -You need to configure your instance. Click the button to create a file. -``` - -### Completion Events - -**Do:** -- Use clear, specific events -- Test completion triggers -- Provide multiple completion paths if possible - -**Don't:** -- Rely on events that might not trigger -- Use too many completion events (makes step too easy to complete by accident) -- Use events from commands that might fail - -### Accessibility - -1. **Always provide altText** for images -2. **Use semantic markdown** (headings, lists) -3. **Don't rely only on color** to convey information -4. **Test with screen readers** if possible -5. **Keep command links descriptive** (not "click here") - -## Testing Checklist - -- [ ] Build extension without errors -- [ ] Walkthrough appears in Welcome screen -- [ ] All steps load without errors -- [ ] Markdown renders correctly -- [ ] Command links work -- [ ] Completion events trigger correctly -- [ ] Images load (if using images) -- [ ] AltText is descriptive -- [ ] Content is accurate and up-to-date -- [ ] Links to external resources work -- [ ] Tested in Extension Development Host - -## Debugging - -### Extension Host Console -View walkthrough errors: -1. Help → Toggle Developer Tools -2. Console tab -3. Look for walkthrough-related errors - -### Common Issues - -**"Walkthrough not found"** -- Check walkthrough ID matches exactly -- Verify package.json is valid JSON -- Rebuild extension - -**"Step media not loading"** -- Check file path is relative to extension root -- Verify markdown file exists -- Check for typos in path - -**"Completion events not firing"** -- Verify command ID exists in package.json -- Check context key syntax -- Test event manually - -**"Command link does nothing"** -- Verify command is registered -- Check URL encoding of arguments -- Look for errors in Extension Host console - -## Internationalization (i18n) - -Currently not implemented, but can be added: - -```json -{ - "title": "%walkthrough.title%", - "description": "%walkthrough.description%" -} -``` - -With corresponding `package.nls.json`: -```json -{ - "walkthrough.title": "Get Started with B2C Commerce", - "walkthrough.description": "Learn the basics in 30 minutes" -} -``` - -## Performance Considerations - -- Markdown files are loaded lazily (only when step is viewed) -- Images are cached by VS Code -- Large images (>500KB) may slow initial load -- Keep GIFs under 2MB if possible -- Consider using SVG for diagrams - -## Version Compatibility - -- **Walkthroughs API**: VS Code 1.56.0+ (May 2021) -- **Completion events**: VS Code 1.56.0+ -- **Link events**: VS Code 1.58.0+ -- **Video support**: Experimental, not recommended - -Current engine requirement: `^1.105.1` - -## Resources - -- [VS Code Walkthrough API Docs](https://code.visualstudio.com/api/references/contribution-points#contributes.walkthroughs) -- [VS Code Extension Samples](https://github.com/microsoft/vscode-extension-samples/tree/main/getting-started-sample) -- [GitHub Flavored Markdown Spec](https://github.github.com/gfm/) -- [VS Code Built-in Commands](https://code.visualstudio.com/api/references/commands) - ---- - -**Last Updated:** 2026-04-28 diff --git a/packages/b2c-vs-extension/media/walkthrough/README.md b/packages/b2c-vs-extension/media/walkthrough/README.md deleted file mode 100644 index abaad0bf0..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# B2C DX Walkthrough Media Assets - -This directory contains markdown content and media assets for the **B2C Commerce Development** getting started walkthrough. - -## Files - -### Markdown Content - -Step IDs come from `src/walkthrough/personas.ts` (`STEP_CATALOG`). Each persona in `PERSONAS` picks a subset and ordering of these. - -| File | Step ID | Purpose | -|------|---------|---------| -| `welcome.md` | `welcome` | Intro and the universal five-step path | -| `install-cli.md` | `install-cli` | Optional B2C CLI install (npm / brew / npx) | -| `dw-json-setup.md` | `configure-dw-json` | dw.json layout, credential grouping, resolution precedence | -| `oauth-setup.md` | `setup-oauth` | client-id / client-secret in the active config | -| `webdav-browser.md` | `explore-webdav` | username / password and the WebDAV view | -| `cartridge-structure.md` | `setup-cartridges` | Cartridge layout, `.project` detection, SCAPI fields | -| `deploy-cartridge.md` | `deploy-code` | First deploy via Cartridges view / `b2c-dx.codeSync.deploy` | -| `sandbox-explorer.md` | `manage-sandboxes` | Realm + sandbox lifecycle (DevOps persona) | -| `code-sync.md` | `enable-code-sync` | Auto-deploy on save | -| `ai-skills.md` | `ai-skills` | Agent Skills + MCP install (AI-augmented persona) | -| `next-steps.md` | `next-steps` | Where to go after onboarding | - -### Image Assets (To Be Added) - -The following image assets should be created for enhanced walkthrough experience: - -| File | Description | Dimensions | -|------|-------------|------------| -| `welcome.png` | Extension overview banner | 800x400 | -| `dw-json-example.png` | Screenshot of configured dw.json | 600x400 | -| `oauth-credentials.png` | Account Manager OAuth setup | 800x500 | -| `webdav-tree.png` | WebDAV browser showing cartridges | 400x600 | -| `cartridge-explorer.png` | Cartridges view with detected cartridges | 400x500 | -| `deploy-success.png` | Deployment success notification | 600x200 | -| `sandbox-explorer.png` | Sandbox Explorer with realms | 400x600 | -| `code-sync-active.png` | Status bar with Code Sync enabled | 800x100 | -| `api-browser.png` | API Browser with Swagger UI | 800x600 | - -### GIF Assets (Optional Enhancement) - -For more engaging walkthrough experience: - -| File | Description | Duration | -|------|-------------|----------| -| `webdav-navigation.gif` | Animated demo of browsing WebDAV | 5-10s | -| `deploy-cartridge.gif` | Animated deployment process | 5-10s | -| `code-sync-demo.gif` | Live demo of file save → auto-upload | 5-10s | - -## Adding Images - -To add image assets: - -1. Create or capture screenshots/images -2. Save them in this directory with the names above -3. Update package.json walkthrough steps to reference images: - -```json -"media": { - "image": "media/walkthrough/welcome.png", - "altText": "B2C DX Extension welcome screen" -} -``` - -Or keep using markdown files: - -```json -"media": { - "markdown": "media/walkthrough/welcome.md" -} -``` - -## Guidelines - -### Screenshot Guidelines -- Use light theme for consistency -- Crop to relevant UI elements -- Highlight important elements (arrows, boxes) -- Use high-resolution images (2x for Retina displays) - -### Markdown Guidelines -- Keep content concise and scannable -- Use headings, bullets, and code blocks -- Include emoji sparingly for visual interest -- Link to relevant commands using `command:` URIs - -### Accessibility -- Always provide `altText` for images -- Ensure markdown is readable without images -- Use semantic headings in markdown files -- Test with screen readers if possible - -## Testing - -To test the walkthrough: - -1. Build the extension: `pnpm run build` -2. Press F5 to launch Extension Development Host -3. Open Command Palette: `Cmd+Shift+P` -4. Run: **Welcome: Open Walkthrough...** -5. Select: **Get Started with B2C Commerce Development** - -## Maintenance - -When updating walkthrough content: -- Update this README if adding/removing files -- Keep markdown files synced with actual extension features -- Test all command links to ensure they work -- Update completion events if command IDs change diff --git a/packages/b2c-vs-extension/media/walkthrough/ai-skills.md b/packages/b2c-vs-extension/media/walkthrough/ai-skills.md deleted file mode 100644 index 9076e5bc3..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/ai-skills.md +++ /dev/null @@ -1,21 +0,0 @@ -# Set up Agent Skills & MCP - -The B2C developer toolkit ships an **MCP server** and a set of **Agent Skills** that let Claude Code, Cursor, GitHub Copilot, and similar tools share context with your B2C project. Configure them once and your AI tools learn the same instance, dw.json, and cartridge layout this extension already understands. - -## MCP server - -The MCP server exposes B2C-specific tools (deploy, log queries, sandbox info) to any MCP-aware client. Configuration is project-scoped — drop a JSON snippet into your client's MCP config and you're done. - -[MCP server setup guide](https://salesforcecommercecloud.github.io/b2c-developer-tooling/mcp/) — includes copy-paste snippets for Claude Code, Cursor, and Copilot. - -## Agent Skills - -Agent Skills bundle B2C-specific instructions, prompts, and conventions that your IDE's AI features can reference. They keep code generation grounded in B2C patterns rather than generic JavaScript. - -[Agent Skills installation](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/agent-skills.html) - -## Pairing with this extension - -- The **Prompt Agent** command (Cursor only) opens a Cursor chat with whatever prompt you type — useful for round-tripping context out of VS Code. -- Keep `dw.json` at the project root; both the MCP server and this extension look there first. -- Set secrets via environment variables (see the dw.json step) so AI tools don't accidentally surface them in context windows. diff --git a/packages/b2c-vs-extension/media/walkthrough/cartridge-structure.md b/packages/b2c-vs-extension/media/walkthrough/cartridge-structure.md deleted file mode 100644 index fc69c5540..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/cartridge-structure.md +++ /dev/null @@ -1,101 +0,0 @@ -# Cartridge Development - -Cartridges are the building blocks of B2C Commerce applications. The extension automatically detects cartridges in your workspace. - -## What is a Cartridge? - -A cartridge is a modular unit of code that contains: -- **Scripts**: Server-side JavaScript (ISML templates, controllers) -- **Static assets**: CSS, JavaScript, images -- **Templates**: ISML templates for rendering pages -- **Forms and metadata**: XML configuration files - -## Cartridge Structure - -A typical cartridge looks like this: - -``` -my_cartridge/ -├── .project ← Required for detection! -├── cartridge/ -│ ├── scripts/ ← Server-side scripts -│ ├── templates/ ← ISML templates -│ ├── static/ ← CSS, JS, images -│ │ ├── default/ -│ │ │ ├── css/ -│ │ │ ├── js/ -│ │ │ └── images/ -│ ├── controllers/ ← Page controllers -│ ├── models/ ← Business logic -│ └── forms/ ← Form definitions -└── package.json ← Node.js dependencies (optional) -``` - -## How Cartridges are Detected - -The extension looks for folders containing a **`.project`** file. This is the Eclipse project file that identifies a cartridge. - -### Sample `.project` File - -```xml - - - my_cartridge - - - - - com.demandware.studio.core.beehiveElementBuilder - - - - - com.demandware.studio.core.beehiveNature - - -``` - -## Creating a New Cartridge - -### Option 1: Use the Scaffold Generator -Click **Create New Cartridge** above to use the built-in scaffold generator. - -### Option 2: Manual Creation -1. Create a new folder in your workspace -2. Add a `.project` file (see example above) -3. Create the `cartridge/` directory structure -4. Click **Refresh Cartridge List** - -## Viewing Your Cartridges - -Open the **Cartridges** view in the B2C-DX activity bar to see all detected cartridges. - -### Cartridge Actions - -Right-click a cartridge to: -- 📤 **Upload Cartridge**: Deploy to your instance -- 📥 **Download from Instance**: Sync remote version to local -- ↔️ **Compare with Instance**: See differences -- ➕ **Add to Site Cartridge Path**: Add to site's cartridge path -- ➖ **Remove from Site Cartridge Path**: Remove from cartridge path - -## Multiple Cartridges - -Your workspace can contain multiple cartridges. The extension will detect all of them automatically. - -``` -workspace/ -├── cartridge_1/ -│ ├── .project -│ └── cartridge/ -├── cartridge_2/ -│ ├── .project -│ └── cartridge/ -└── cartridge_3/ - ├── .project - └── cartridge/ -``` - ---- - -Once cartridges are detected, the **Cartridges** view will open automatically! diff --git a/packages/b2c-vs-extension/media/walkthrough/code-sync.md b/packages/b2c-vs-extension/media/walkthrough/code-sync.md deleted file mode 100644 index d1262dc8d..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/code-sync.md +++ /dev/null @@ -1,131 +0,0 @@ -# Code Sync (Automatic Deployment) - -**Code Sync** watches your cartridge files and automatically uploads changes as you save. Perfect for rapid development! - -## What is Code Sync? - -Code Sync is a **file watcher** that: -- Monitors your cartridge files for changes -- Automatically uploads modified files to your B2C instance -- Shows upload status in the status bar -- Supports multiple cartridges simultaneously - -## Starting Code Sync - -Click **Start Code Sync** above, or: -1. Open Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) -2. Run **B2C DX - Code Sync: Start Code Sync** - -You'll see a status bar item: **$(sync~spin) Code Sync: Active** - -## Stopping Code Sync - -Click **Stop Code Sync** above, or: -- Click the **$(sync~spin)** status bar item -- Run **B2C DX - Code Sync: Stop Code Sync** from Command Palette - -## What Gets Synced? - -Code Sync monitors these file types in your cartridges: -- `.js` - Scripts and controllers -- `.ds` - Scripts (DemandWare Script) -- `.isml` - Templates -- `.xml` - Forms and metadata -- `.properties` - Configuration files -- `.css` - Stylesheets -- `.scss` / `.sass` - Sass files -- `.json` - JSON configuration -- Images (`.png`, `.jpg`, `.svg`, etc.) - -### Excluded Files -Code Sync ignores: -- `node_modules/` -- `.git/` -- Build artifacts -- `.project` files - -## Status Bar Indicators - -### $(sync~spin) Code Sync: Active -Watching for file changes. - -### $(cloud-upload) Uploading: filename.js -Currently uploading a file. - -### $(check) Upload Complete -File uploaded successfully. - -### $(error) Upload Failed -Upload encountered an error. Check Output panel. - -## Configuration - -Control Code Sync behavior in Settings (`Cmd+,` / `Ctrl+,`): - -### Enable/Disable Code Sync -```json -"b2c-dx.features.codeSync": true -``` - -## How It Works - -1. **Save a file** in a cartridge -2. Code Sync **detects the change** -3. File is **uploaded** to WebDAV (`/Cartridges//...`) -4. B2C instance **updates** the active code version -5. Changes are **immediately available** (no restart needed for most files) - -## Best Practices - -### ✅ When to Use Code Sync - -- **Rapid development**: Making frequent small changes -- **Template editing**: ISML template development -- **CSS/JS tweaks**: Front-end styling adjustments -- **Debugging**: Quick fixes to test theories - -### ❌ When NOT to Use Code Sync - -- **Large refactoring**: Many file changes at once (use manual deploy) -- **Production deployments**: Always use manual deploy for production -- **Multiple cartridges**: Deploy all at once with manual deploy -- **First deployment**: Use manual deploy to ensure everything uploads - -## Performance Tips - -💡 **Watch the Output panel**: Monitor upload progress and errors in **B2C DX** output. - -💡 **Stop when not developing**: Disable Code Sync when not actively coding to save resources. - -💡 **Use .gitignore patterns**: Code Sync respects `.gitignore` files. - -💡 **Exclude large files**: Don't upload huge images or videos via Code Sync (use WebDAV browser for bulk uploads). - -## Troubleshooting - -### Files Not Uploading? -- Check Output panel for errors -- Verify `dw.json` credentials -- Ensure cartridge is detected (Cartridges view) -- Confirm Code Sync is active (status bar) - -### Upload Delays? -- Network latency can slow uploads -- Large files take longer -- Check your internet connection - -### Changes Not Visible on Storefront? -- Some changes require **cache clearing** -- Templates update immediately -- Controllers may need instance restart -- CSS/JS may be cached in browser (hard refresh) - -## Toggle Code Sync - -You can also **toggle** Code Sync on/off: -- Run **B2C DX - Code Sync: Toggle Code Sync** from Command Palette -- Quickly enable/disable without separate commands - ---- - -Click **Start Code Sync** to enable automatic deployment! diff --git a/packages/b2c-vs-extension/media/walkthrough/deploy-cartridge.md b/packages/b2c-vs-extension/media/walkthrough/deploy-cartridge.md deleted file mode 100644 index 009a86e17..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/deploy-cartridge.md +++ /dev/null @@ -1,88 +0,0 @@ -# Deploy Your First Cartridge - -Deploying cartridges is the core workflow for B2C Commerce development. Upload your local code to your B2C instance with a single command! - -## Deployment Methods - -### Method 1: Deploy All Cartridges -Click **Deploy All Cartridges** above to upload all cartridges in your workspace. - -This creates a ZIP archive of each cartridge and uploads them to your instance's active code version. - -### Method 2: Deploy Individual Cartridge -1. Open the **Cartridges** view (B2C-DX sidebar) -2. Right-click a cartridge -3. Select **Upload Cartridge** - -### Method 3: Command Palette -1. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) -2. Type "B2C: Deploy" -3. Select **B2C DX - Code Sync: Deploy Cartridges** - -## What Happens During Deployment - -1. **Packaging**: Extension creates a ZIP archive of the cartridge -2. **Upload**: ZIP is uploaded via WebDAV to `/Cartridges/` -3. **Extraction**: B2C instance automatically extracts the cartridge -4. **Activation**: Cartridge is available in the active code version - -## Monitoring Deployment - -Watch the **Output** panel for deployment progress: -1. View → Output -2. Select **B2C DX** from the dropdown - -You'll see logs like: -``` -[INFO] Starting cartridge upload: my_cartridge -[INFO] Creating archive... -[INFO] Uploading to /Cartridges/my_cartridge... -[INFO] Upload complete! (2.3 MB in 1.2s) -``` - -## Code Versions - -Cartridges are deployed to the **active code version** on your instance. - -### Viewing Code Versions -Click the **Code Versions** icon in the Cartridges view to see all code versions on your instance. - -### Creating a New Code Version -1. Click **Code Versions** in Cartridges view -2. Click **Create Code Version** -3. Enter a version name -4. Optionally activate it - -### Activating a Code Version -1. Click **Code Versions** -2. Select a version -3. Click **Activate Code Version** - -## Troubleshooting - -### ❌ "Upload failed: Authentication required" -- Check your `dw.json` credentials -- Verify hostname, username, and password are correct - -### ❌ "Upload failed: Permission denied" -- Ensure your user has WebDAV upload permissions -- Check Business Manager user roles - -### ❌ "Cartridge not found on instance" -- After uploading, the cartridge appears in the cartridge path -- Verify upload succeeded in Output panel - -### ❌ "Code version is read-only" -- You cannot upload to a locked code version -- Create a new code version or unlock the current one - -## Next Steps - -After deploying: -- **Test your changes**: Visit your storefront to see updates -- **Check logs**: Use the **Start Tailing Logs** command to view instance logs -- **Debug**: Use the B2C Script Debugger to debug server-side code - ---- - -Click **Deploy All Cartridges** to upload your code now! diff --git a/packages/b2c-vs-extension/media/walkthrough/dw-json-setup.md b/packages/b2c-vs-extension/media/walkthrough/dw-json-setup.md deleted file mode 100644 index cc665f690..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/dw-json-setup.md +++ /dev/null @@ -1,73 +0,0 @@ -# Configure your instance - -The B2C tooling reads config from layered sources. **Different fields belong in different places.** The walkthrough's *Run setup wizard* asks you, pair by pair, where each value should live. - -## What goes where - -| Field | Sensitive? | Recommended home | -|---|---|---| -| `hostname` | No | `dw.json` | -| `code-version` | No | `dw.json` | -| `short-code`, `tenant-id`, `oauth-scopes` | No | `dw.json` (when SCAPI is enabled) | -| `mrtProject`, `mrtEnvironment` | No | `dw.json` | -| `client-id` | Identifier (not sensitive) | Same source as `client-secret` (Credential Grouping) | -| `client-secret` | **Yes** | Keychain / Password Store / `SFCC_CLIENT_SECRET` | -| `username` | Identifier | Same source as `password` | -| `password` | **Yes** (WebDAV access key) | Keychain / Password Store / `SFCC_PASSWORD` | -| MRT API key | **Yes** | `b2c mrt save-credentials` (writes `~/.mobify`) or `MRT_API_KEY` env var | -| `certificate`, `certificate-passphrase` | **Yes** (mTLS) | Env vars only | - -> **Credential Grouping.** If one half of an OAuth or Basic-auth pair comes from a higher-priority source, the matching half from a lower source is **ignored**. The wizard enforces this by asking pair-by-pair. - -## Auth flows you can mix and match - -Different jobs need different fields. The wizard lets you enable any combination: - -- **OAuth** — `client-id` + `client-secret`. Required for Sandbox Explorer, OCAPI / SCAPI, and CI jobs. -- **Basic** — `username` + `password`. Required for WebDAV and cartridge deploys. -- **SCAPI extras** — `short-code` + `tenant-id` + optional `oauth-scopes`. Required by the API Browser. -- **MRT (Managed Runtime)** — `mrtProject` + `mrtEnvironment` (in `dw.json`) + `MRT_API_KEY` (in `~/.mobify` or env var). - -## Resolution precedence - -Highest first — the first source that supplies a value wins for that field: - -1. CLI flags & environment variables -2. Plugin sources at high priority (Keychain, Password Store, IntelliJ Config) -3. `dw.json` -4. `~/.mobify` -5. Plugin sources at low priority -6. `package.json` - -So **environment variables always override `dw.json`** for the same field — useful for CI overrides without touching the file. - -## Inspecting what actually resolved - -Run **B2C DX - Getting Started: Inspect Resolved Config (b2c setup inspect)** any time. It prints every resolved field with its source: `dw.json`, `env (SFCC_CLIENT_SECRET)`, `keychain (b2c-cli/dev)`, etc. Add `--unmask` to show secret values too. - -## Single vs. multi-instance dw.json - -The wizard always writes a `configs[]` array, even for a single instance — that lets you add a second entry later without reshaping the file: - -```json -{ - "configs": [ - { - "name": "dev", - "active": true, - "hostname": "abcd-123.dx.commercecloud.salesforce.com", - "code-version": "version1", - "short-code": "kv7kzm78", - "tenant-id": "zzrf_001" - } - ] -} -``` - -Switch the active instance from the status bar (click the `$(cloud)` item) or via `b2c setup instance set-active `. - -## .gitignore - -The wizard appends `dw.json` to your workspace `.gitignore` automatically. Even if every secret lives outside the file, `dw.json` can still expose hostnames and tenant IDs. - -[Full configuration reference](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/configuration.html) · [Third-party plugins](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/third-party-plugins.html) · [`b2c setup inspect`](https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/setup.html) diff --git a/packages/b2c-vs-extension/media/walkthrough/install-cli.md b/packages/b2c-vs-extension/media/walkthrough/install-cli.md deleted file mode 100644 index ebf0b877a..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/install-cli.md +++ /dev/null @@ -1,41 +0,0 @@ -# Install the B2C CLI - -The B2C CLI (`b2c`) drives deploys, log tailing, sandbox management, and more from the terminal. The VS Code extension uses it under the hood for some commands. - -> **Optional.** You can use the extension's Cartridges, WebDAV, and Sandbox views without the CLI. Install it when you want to script the same operations from the terminal or CI. - -## Prerequisites - -- **Node.js** — v22.0.0 or newer required -- **npm** — included with Node.js (used for global install) -- **npx** — included with Node.js (used for one-off runs) -- **Homebrew** — optional, alternative install method on macOS/Linux - -## Install - -Pick whichever fits your toolchain. The published docs list these three: - -- **npm** — `npm install -g @salesforce/b2c-cli` -- **Homebrew** — `brew install salesforcecommercecloud/tools/b2c-cli` -- **npx** — `npx @salesforce/b2c-cli --help` - -## Verify - -After install, confirm the CLI is on your PATH with `b2c --version`. - -Or click **Verify CLI** above — the extension detects the installed version and checks for updates automatically. - -## What it unlocks - -- `b2c code:deploy` — same flow the Cartridges view uses, scriptable from CI. -- `b2c sandbox:*` — create/start/stop/delete sandboxes from the terminal. -- `b2c log:tail` — stream instance logs. -- `b2c auth:*` — non-interactive OAuth client login for pipelines. - -## Troubleshooting - -- **Command not found after `npm install -g`** — your global npm prefix isn't on PATH. Run `npm config get prefix` and add `/bin` to PATH. -- **EACCES on install** — use a Node version manager (`nvm`, `fnm`, `volta`) instead of `sudo npm`. Avoid `sudo`. -- **Old version behaves oddly** — run **Update CLI** (or `npm install -g @salesforce/b2c-cli@latest`) to pin to the latest published release. - -[Full installation guide on the docs site](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/installation.html) diff --git a/packages/b2c-vs-extension/media/walkthrough/next-steps.md b/packages/b2c-vs-extension/media/walkthrough/next-steps.md deleted file mode 100644 index 5165dc2e0..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/next-steps.md +++ /dev/null @@ -1,33 +0,0 @@ -# You're set up — what's next - -You've got a connected instance, a deployed cartridge, and a scaffold workflow. Here's where to go from here. - -## Day-to-day commands - -| Goal | Command | -|---|---| -| Browse remote files | **B2C DX: List WebDAV** or open the WebDAV view | -| Watch & auto-deploy | **B2C DX - Code Sync: Toggle Code Sync** | -| Tail instance logs | **B2C DX - Logs: Start Tailing Logs** | -| Switch active instance | Click the `$(cloud)` item in the status bar | -| Inspect resolved config | **B2C DX: B2C Instance Config** | - -## Features worth exploring - -- **API Browser** — interactive Swagger for your instance's SCAPI specs. Needs OAuth. -- **Sandbox Explorer** — start, stop, restart, extend, and create sandboxes. Needs OAuth. -- **B2C Script Debugger** — set breakpoints in server-side `.js`/`.ds` files; F5 to attach. -- **Commerce App Packages (CAP)** — install B2C apps from a `commerce-app.json`. -- **Page Designer Assistant** — generate Page Designer page files from a guided UI. - -## Going further - -- [Documentation site](https://salesforcecommercecloud.github.io/b2c-developer-tooling/) — CLI reference, SDK, MCP, Agent Skills. -- [Issues](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling/issues) — bug reports & feature requests. -- [SFRA reference](https://github.com/SalesforceCommerceCloud/storefront-reference-architecture) — cartridge patterns to learn from. - -## Re-open this guide - -Run **B2C DX: Open Getting Started Guide** from the Command Palette (`Cmd/Ctrl+Shift+P`). - -Open the role-based deep-dive any time with **B2C DX: Open Onboarding Panel**. diff --git a/packages/b2c-vs-extension/media/walkthrough/oauth-setup.md b/packages/b2c-vs-extension/media/walkthrough/oauth-setup.md deleted file mode 100644 index 28a7ef233..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/oauth-setup.md +++ /dev/null @@ -1,51 +0,0 @@ -# Set Up OAuth Credentials - -OAuth credentials unlock features that talk to Account Manager and SCAPI: **Sandbox Explorer**, **API Browser**, **Code Versions**, and most CLI/CI flows. WebDAV and basic cartridge deploys do *not* need OAuth. - -> **Optional.** Skip this step if you only need WebDAV browsing or cartridge deploys via username/password. - -## What you need in `dw.json` - -The wizard adds these fields to your active config — names use **kebab-case**, the same as the SDK reads: - -```json -{ - "configs": [ - { - "name": "dev", - "active": true, - "hostname": "your-sandbox.dx.commercecloud.salesforce.com", - "code-version": "version1", - "client-id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "client-secret": "", - "short-code": "kv7kzm78", - "tenant-id": "zzrf_001" - } - ] -} -``` - -`client-id` is an identifier (not sensitive). `client-secret` is sensitive — pick where it should live when the wizard prompts you (Keychain, `pass`, `SFCC_CLIENT_SECRET` env var, or `dw.json`). Same source as `client-id` per the **Credential Grouping** rule. - -## Getting the credentials - -1. Open [Account Manager](https://account.demandware.com/) and go to **API Client**. -2. Click **Add API Client** and grant the scopes you need: - - `sfcc.sandboxes.rw` — Sandbox Explorer - - `sfcc.code-versions` (rw) — code-version management - - `sfcc.shopper-*` — only if the same client is also used for SCAPI calls. Pinning these scopes will break Sandbox Explorer if the AM client is **not** registered for them, so leave `oauth-scopes` blank in the SCAPI step unless you know your client supports them. -3. Save and copy the **Client ID** and **Client Secret** immediately — the secret is shown only once. -4. Run the **Set up OAuth** action above (or **B2C DX - Getting Started: Setup · OAuth Credentials** from the Command Palette). The wizard writes `client-id` and the secret to your chosen sources, targeting the **active** config in `dw.json`. - -## Verify - -- Run **Inspect Resolved Config** above — it shows whether `client-id` and `client-secret` resolved, and from which source. -- Open the **Realm Explorer** (B2C-DX Sandboxes activity bar). If OAuth is wired up correctly the realm loads and lists your sandboxes. - -## What OAuth unlocks - -- **Sandbox Explorer** — create, start, stop, restart, extend, and delete sandboxes; open Business Manager. -- **API Browser** — browse SCAPI OpenAPI specs (also needs `short-code` + `tenant-id` from the SCAPI step). -- **Code Versions** — list, create, and activate code versions from the Cartridges view. - -[Full OAuth + scopes reference](https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/configuration.html#oauth) · [`b2c setup inspect`](https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/setup.html) diff --git a/packages/b2c-vs-extension/media/walkthrough/sandbox-explorer.md b/packages/b2c-vs-extension/media/walkthrough/sandbox-explorer.md deleted file mode 100644 index 63a3633d7..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/sandbox-explorer.md +++ /dev/null @@ -1,117 +0,0 @@ -# Sandbox Explorer - -Manage your B2C Commerce development sandboxes directly from VS Code! - -## Prerequisites - -⚠️ **Requires OAuth credentials** configured in `dw.json`. If you haven't set up OAuth yet, go back to the "Set Up OAuth Credentials" step. - -## What is a Sandbox? - -A sandbox is an isolated B2C Commerce development environment where you can: -- Develop and test code changes -- Import/export data -- Configure site settings -- Test storefront functionality - -Each sandbox has its own: -- Database -- Code versions -- Configuration -- Users and permissions - -## Opening the Sandbox Explorer - -Click **Open Sandbox Explorer** above, or: -- Open the **B2C-DX Sandboxes** activity bar icon (left sidebar) -- View the **Realm Explorer** - -## Key Concepts - -### Realm -Your **realm** is your organization's collection of sandboxes. One realm can have multiple sandboxes. - -### Sandbox States -- 🟢 **Started**: Sandbox is running and accessible -- 🔴 **Stopped**: Sandbox is paused (saves resources) -- 🟡 **Starting**: Sandbox is booting up -- 🟠 **Stopping**: Sandbox is shutting down - -## Common Operations - -### Add a Realm -1. Click the **+** icon in Realm Explorer -2. Enter your realm name (short code) -3. Credentials will be used from `dw.json` - -### Create a Sandbox -1. Right-click your realm -2. Select **Create Sandbox** -3. Enter a sandbox name -4. Wait for provisioning (2-5 minutes) - -### Start a Sandbox -1. Right-click a stopped sandbox -2. Select **Start Sandbox** -3. Wait for startup (~1-2 minutes) - -### Stop a Sandbox -1. Right-click a started sandbox -2. Select **Stop Sandbox** -3. Confirm the action - -### Restart a Sandbox -1. Right-click a started sandbox -2. Select **Restart Sandbox** -3. Useful for clearing cache or applying changes - -### View Sandbox Details -1. Right-click any sandbox -2. Select **View Details** -3. See status, expiration, hostname, etc. - -### Open Business Manager -1. Right-click a started sandbox -2. Select **Open Business Manager** -3. BM opens in your default browser - -### Extend Sandbox Expiration -1. Right-click any sandbox -2. Select **Extend Expiration** -3. Choose extension period -4. Prevents automatic deletion - -### Delete a Sandbox -1. Right-click any sandbox -2. Select **Delete Sandbox** -3. Confirm deletion -4. ⚠️ **Warning**: This is permanent! - -## Status Bar Integration - -When connected to a sandbox, the status bar shows: -- **☁️ Instance name**: Click to switch instances -- **$(pinned)**: Indicates pinned project root - -## Tips - -💡 **Stop when not in use**: Save resources by stopping sandboxes you're not actively using. - -💡 **Watch expiration dates**: Sandboxes auto-delete after expiration. Extend them regularly! - -💡 **One realm per team**: Share a realm with your team for easier collaboration. - -💡 **Test on multiple sandboxes**: Use different sandboxes for feature branches or testing. - -## Sandbox Lifecycle Best Practices - -1. **Start** a sandbox when you begin working -2. **Develop** and deploy code changes -3. **Test** on the sandbox storefront -4. **Stop** the sandbox when done for the day -5. **Extend** expiration if working on long-term features -6. **Delete** when completely done with a feature - ---- - -Click **Open Sandbox Explorer** to start managing your sandboxes! diff --git a/packages/b2c-vs-extension/media/walkthrough/webdav-browser.md b/packages/b2c-vs-extension/media/walkthrough/webdav-browser.md deleted file mode 100644 index 9709889c6..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/webdav-browser.md +++ /dev/null @@ -1,66 +0,0 @@ -# WebDAV Browser - -The **WebDAV Browser** lets you explore and edit files directly on your B2C Commerce instance. - -## What is WebDAV? - -WebDAV (Web Distributed Authoring and Versioning) is a protocol that allows you to access files on your B2C instance as if they were local files. - -## Opening the WebDAV Browser - -Click **Open WebDAV Browser** above, or: -- Open the **B2C-DX** activity bar icon (left sidebar) -- Find the **WebDAV Browser** view - -## What You Can Browse - -### 📁 Cartridges -View and edit cartridge code deployed to your instance. - -### 📚 Libraries -Browse content libraries (Page Designer content, images, etc.). - -### 🛍️ Catalogs -Access product catalog data and imports. - -## Common Actions - -### Open a Remote File -- Click any file in the WebDAV tree -- Edit directly in VS Code -- Save to upload changes to the instance - -### Upload a File -- Right-click a folder -- Select **Upload File** -- Choose a local file to upload - -### Create New Files/Folders -- Right-click a folder -- Select **New File** or **New Folder** - -### Download Files -- Right-click a file -- Select **Download** -- Choose local save location - -### Mount as Workspace -- Right-click a folder -- Select **Open as Workspace Folder** -- Browse the remote folder as if it were local! - -## Tips - -💡 **Browse before deploying**: Check what's currently on your instance before uploading new code. - -💡 **Quick edits**: Make small fixes directly on the instance without a full deployment. - -💡 **Compare versions**: Download a cartridge from the instance to compare with your local version. - -## Performance Note - -The WebDAV browser loads folders on-demand. Large directories may take a moment to load. - ---- - -Click **Open WebDAV Browser** to explore your instance! diff --git a/packages/b2c-vs-extension/media/walkthrough/welcome-hero-dark.svg b/packages/b2c-vs-extension/media/walkthrough/welcome-hero-dark.svg deleted file mode 100644 index ecc973298..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/welcome-hero-dark.svg +++ /dev/null @@ -1,241 +0,0 @@ - - - Pick your starting role - Welcome to B2C DX. Choose one of four roles — Storefront, API/Integration, DevOps/Release, or AI-augmented — to tailor your onboarding path. Each role lists its step count and average completion time. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PHASE 0 / 5 - - - - - WELCOME · TAILOR YOUR PATH - - - - - Pick your starting role. - - - - - Different roles need different things first. Choose one for a tailored deep-dive, - - - or follow the universal five-phase flow shown on the left. - - - - - - - 4 - ROLES - - - - 5 - PHASES - - - - ~10 MIN - AVG TIME - - - - - DOC-BACKED - - - - - - - - - - - - - - - - - - Storefront developer - SFRA · PWA Kit · ISML - Cartridge authoring, fast iteration with Code Sync, - and the WebDAV browser. - 8 PHASES · ~8 MIN - - - - - - - - - - - - - - - - - API / integration developer - SCAPI · OCAPI · jobs · hooks - OAuth setup and the API Browser front-and-center. - Code Sync optional. - 8 PHASES · ~10 MIN - - - - - - - - - - - - - - - - - - - DevOps / release engineer - sandboxes · code versions · CAPs - OAuth + Sandbox Explorer first; less time on - cartridge authoring. - 7 PHASES · ~6 MIN - - - - - - - - - - - - - - - - - - - - NEW - - AI-augmented developer - Cursor · Claude Code · Copilot - Storefront setup plus the documented MCP server - and Agent Skills. - 8 PHASES · ~12 MIN - - - - - - - - - - - Ready to begin? - - - Click Open role-based guide below to launch the deep-dive panel. - - - - Already set up? Mark all done → - - - - - - - - diff --git a/packages/b2c-vs-extension/media/walkthrough/welcome-hero-light.svg b/packages/b2c-vs-extension/media/walkthrough/welcome-hero-light.svg deleted file mode 100644 index 6b0768d92..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/welcome-hero-light.svg +++ /dev/null @@ -1,245 +0,0 @@ - - - Pick your starting role - Welcome to B2C DX. Choose one of four roles — Storefront, API/Integration, DevOps/Release, or AI-augmented — to tailor your onboarding path. Each role lists its step count and average completion time. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PHASE 0 / 5 - - - - - WELCOME · TAILOR YOUR PATH - - - - - Pick your starting role. - - - - - Different roles need different things first. Choose one for a tailored deep-dive, - - - or follow the universal five-phase flow shown on the left. - - - - - - - 4 - ROLES - - - - 5 - PHASES - - - - ~10 MIN - AVG TIME - - - - - DOC-BACKED - - - - - - - - - - - - - - - - - - Storefront developer - SFRA · PWA Kit · ISML - Cartridge authoring, fast iteration with Code Sync, - and the WebDAV browser. - 8 PHASES · ~8 MIN - - - - - - - - - - - - - - - - - - API / integration developer - SCAPI · OCAPI · jobs · hooks - OAuth setup and the API Browser front-and-center. - Code Sync optional. - 8 PHASES · ~10 MIN - - - - - - - - - - - - - - - - - - - DevOps / release engineer - sandboxes · code versions · CAPs - OAuth + Sandbox Explorer first; less time on - cartridge authoring. - 7 PHASES · ~6 MIN - - - - - - - - - - - - - - - - - - - - - NEW - - AI-augmented developer - Cursor · Claude Code · Copilot - Storefront setup plus the documented MCP server - and Agent Skills. - 8 PHASES · ~12 MIN - - - - - - - - - - - Ready to begin? - - - Click Open role-based guide below to launch the deep-dive panel. - - - - - Already set up? Mark all done → - - - - - - - - - diff --git a/packages/b2c-vs-extension/media/walkthrough/welcome.md b/packages/b2c-vs-extension/media/walkthrough/welcome.md deleted file mode 100644 index 270c2420b..000000000 --- a/packages/b2c-vs-extension/media/walkthrough/welcome.md +++ /dev/null @@ -1,21 +0,0 @@ -# Welcome to B2C Commerce on VS Code - -Five focused steps and you're deployed. - -## What you'll set up - -1. **Install the B2C CLI** *(optional — skip if you'll only use the views)* -2. **Configure your instance** — drop a `dw.json` at your workspace root -3. **Connect & authenticate** — verify the extension can talk to your sandbox -4. **Deploy your first cartridge** — push code to your active code version -5. **Generate from a scaffold** — boilerplate for new cartridges, controllers, pages - -## Pick a starting point - -Different roles need different things first. Open the **role-based deep-dive guide** for a tailored walkthrough — Storefront, API/Integration, DevOps/Release, or AI-augmented developer. - -The five-step path is the same for everyone. The deep-dive layers in the role-specific bits. - -## Already set up? - -Use **Mark all steps as done** in the last step (or the Command Palette command **B2C DX - Getting Started: Mark Getting Started as Done**) to tick everything at once. diff --git a/packages/b2c-vs-extension/package.json b/packages/b2c-vs-extension/package.json index 3901c3d7d..c6fa2c17d 100644 --- a/packages/b2c-vs-extension/package.json +++ b/packages/b2c-vs-extension/package.json @@ -163,7 +163,7 @@ "b2c-dx.features.jobsExplorer": { "type": "boolean", "default": false, - "description": "(Preview) Enable the Job History view for monitoring Business Manager job execution history. In development — off by default." + "description": "(Preview) Enable the Job History view for monitoring Business Manager job execution history. In development \u2014 off by default." }, "b2c-dx.features.scaffold": { "type": "boolean", @@ -178,7 +178,7 @@ "b2c-dx.features.exportExplorer": { "type": "boolean", "default": false, - "description": "(Preview) Enable the Export view for interactively exporting site impex data units. In development — off by default." + "description": "(Preview) Enable the Export view for interactively exporting site impex data units. In development \u2014 off by default." }, "b2c-dx.features.cap": { "type": "boolean", @@ -248,7 +248,7 @@ "b2c-dx.jobs.autoRefresh": { "type": "boolean", "default": false, - "description": "Auto-refresh Job History on a schedule (set by b2c-dx.jobs.refreshInterval). Off by default — load manually from the title bar." + "description": "Auto-refresh Job History on a schedule (set by b2c-dx.jobs.refreshInterval). Off by default \u2014 load manually from the title bar." }, "b2c-dx.jobs.knownJobIds": { "type": "array", @@ -271,17 +271,12 @@ "b2c-dx.telemetry.enabled": { "type": "boolean", "default": true, - "description": "Send anonymous usage telemetry (extension lifecycle and broad feature-category events). Honors VS Code's telemetry.telemetryLevel — disabling that disables this regardless of this setting." + "description": "Send anonymous usage telemetry (extension lifecycle and broad feature-category events). Honors VS Code's telemetry.telemetryLevel \u2014 disabling that disables this regardless of this setting." }, "b2c-dx.features.cipAnalytics": { "type": "boolean", "default": false, - "description": "(Preview) Enable the B2C-DX Analytics (CIP) sidebar for browsing tables and running curated reports. In development — off by default." - }, - "b2c-dx.features.onboarding": { - "type": "boolean", - "default": false, - "description": "(Preview) Enable the guided developer onboarding walkthrough and role-based deep-dive panel. In development — off by default." + "description": "(Preview) Enable the B2C-DX Analytics (CIP) sidebar for browsing tables and running curated reports. In development \u2014 off by default." }, "b2c-dx.features.xmlValidation": { "type": "boolean", @@ -347,6 +342,34 @@ }, "default": [], "markdownDescription": "ISML diagnostic rule codes to disable globally. Empty by default (all rules on, including the `encoding-off` output-escaping security warning). Add a code to silence that rule; individual lines can also be suppressed inline with ` b2c-dx-disable-next-line `." + }, + "b2c-dx.mcp.enabled": { + "type": "boolean", + "default": true, + "description": "Make the B2C Commerce MCP server available to VS Code and Cursor chat. Disable if you manage this server separately." + }, + "b2c-dx.mcp.command": { + "type": "string", + "default": "npx", + "scope": "machine", + "description": "Executable used to launch the B2C Commerce MCP server. Requires Node.js 22 or later with the default npx command." + }, + "b2c-dx.mcp.args": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + }, + "default": null, + "scope": "machine", + "description": "Arguments for the MCP executable. When unset, runs the MCP package version matched to this extension. Project, configuration, and instance arguments are supplied by the extension." + }, + "b2c-dx.features.setup": { + "type": "boolean", + "default": false, + "description": "(Beta) Enable standalone instance, credential, and CLI setup commands. Reload the window after changing this setting." } } }, @@ -481,7 +504,7 @@ }, { "view": "b2cCipAnalytics", - "contents": "Explore Commerce Intelligence Platform (CIP) data — build SQL queries visually, browse tables, and run curated reports.\n\n[Open Query Builder](command:b2c-dx.cipAnalytics.queryBuilder)\n[Browse Tables](command:b2c-dx.cipAnalytics.browseTables)\n\nRequires OAuth credentials (clientId, clientSecret) in dw.json.\n\n[Refresh](command:b2c-dx.cipAnalytics.refresh)" + "contents": "Explore Commerce Intelligence Platform (CIP) data \u2014 build SQL queries visually, browse tables, and run curated reports.\n\n[Open Query Builder](command:b2c-dx.cipAnalytics.queryBuilder)\n[Browse Tables](command:b2c-dx.cipAnalytics.browseTables)\n\nRequires OAuth credentials (clientId, clientSecret) in dw.json.\n\n[Refresh](command:b2c-dx.cipAnalytics.refresh)" }, { "view": "b2cExportExplorer", @@ -595,7 +618,7 @@ }, { "command": "b2c-dx.cipAnalytics.resetFromDwJson", - "title": "Reset from dw.json…", + "title": "Reset from dw.json\u2026", "icon": "$(discard)", "category": "B2C-DX Analytics" }, @@ -618,7 +641,7 @@ }, { "command": "b2c-dx.cipAnalytics.configureConnection", - "title": "Configure Connection…", + "title": "Configure Connection\u2026", "icon": "$(gear)", "category": "B2C-DX Analytics" }, @@ -630,25 +653,25 @@ }, { "command": "b2c-dx.cipAnalytics.switchRealm", - "title": "Switch Realm…", + "title": "Switch Realm\u2026", "icon": "$(server)", "category": "B2C-DX Analytics" }, { "command": "b2c-dx.cipAnalytics.switchConnection", - "title": "Switch Connection…", + "title": "Switch Connection\u2026", "icon": "$(plug)", "category": "B2C-DX Analytics" }, { "command": "b2c-dx.cipAnalytics.addRealm", - "title": "Add Realm…", + "title": "Add Realm\u2026", "icon": "$(add)", "category": "B2C-DX Analytics" }, { "command": "b2c-dx.cipAnalytics.removeRealm", - "title": "Remove Realm…", + "title": "Remove Realm\u2026", "icon": "$(trash)", "category": "B2C-DX Analytics" }, @@ -659,13 +682,13 @@ }, { "command": "b2c-dx.cipAnalytics.renameSavedQuery", - "title": "Edit Saved Query…", + "title": "Edit Saved Query\u2026", "icon": "$(edit)", "category": "B2C-DX Analytics" }, { "command": "b2c-dx.cipAnalytics.deleteSavedQuery", - "title": "Delete Saved Query…", + "title": "Delete Saved Query\u2026", "icon": "$(trash)", "category": "B2C-DX Analytics" }, @@ -1203,210 +1226,72 @@ { "command": "b2c-dx.walkthrough.createDwJson", "title": "Create dw.json Configuration", - "category": "B2C DX - Getting Started" - }, - { - "command": "b2c-dx.walkthrough.open", - "title": "Open Getting Started Guide", - "category": "B2C DX" - }, - { - "command": "b2c-dx.walkthrough.markAllDone", - "title": "Mark Getting Started as Done", - "category": "B2C DX - Getting Started" - }, - { - "command": "b2c-dx.walkthrough.resetProgress", - "title": "Reset Getting Started Progress", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.cli.verify", "title": "Verify B2C CLI Installation", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.cli.update", "title": "Update B2C CLI to Latest", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.cli.installNpm", "title": "Install B2C CLI via npm", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.cli.installBrew", "title": "Install B2C CLI via Homebrew", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.cli.recheck", "title": "Re-check B2C CLI Installation", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.walkthrough.chooseCredentialStorage", "title": "Configure Instance (Wizard)", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.walkthrough.inspectSetup", "title": "Inspect Resolved Config (b2c setup inspect)", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.setup.connection", - "title": "Setup · Connection (instance + hostname)", - "category": "B2C DX - Getting Started" + "title": "Setup \u00b7 Connection (instance + hostname)", + "category": "B2C DX - Setup" }, { "command": "b2c-dx.setup.oauth", - "title": "Setup · OAuth Credentials", - "category": "B2C DX - Getting Started" + "title": "Setup \u00b7 OAuth Credentials", + "category": "B2C DX - Setup" }, { "command": "b2c-dx.setup.webdav", - "title": "Setup · WebDAV Credentials", - "category": "B2C DX - Getting Started" + "title": "Setup \u00b7 WebDAV Credentials", + "category": "B2C DX - Setup" }, { "command": "b2c-dx.setup.scapi", - "title": "Setup · SCAPI Configuration", - "category": "B2C DX - Getting Started" + "title": "Setup \u00b7 SCAPI Configuration", + "category": "B2C DX - Setup" }, { "command": "b2c-dx.setup.resetSession", "title": "Reset Setup Session (Start over from connection step)", - "category": "B2C DX - Getting Started" + "category": "B2C DX - Setup" }, { "command": "b2c-dx.theme.toggle", "title": "Toggle Light / Dark Theme", "category": "B2C DX" - }, - { - "command": "b2c-dx.onboarding.open", - "title": "Open Onboarding Panel", - "category": "B2C DX" - }, - { - "command": "b2c-dx.onboarding.reset", - "title": "Reset Onboarding Progress", - "category": "B2C DX" - }, - { - "command": "b2c-dx.onboarding.changePersona", - "title": "Change Onboarding Role", - "category": "B2C DX" - }, - { - "command": "b2c-dx.walkthrough.validate", - "title": "Validate Walkthrough Configuration", - "category": "B2C DX - Development" - }, - { - "command": "b2c-dx.walkthrough.checkAccessibility", - "title": "Check Walkthrough Accessibility", - "category": "B2C DX - Development" - }, - { - "command": "b2c-dx.walkthrough.showTelemetry", - "title": "Show Walkthrough Telemetry", - "category": "B2C DX - Development" - } - ], - "walkthroughs": [ - { - "id": "b2c-dx.gettingStarted", - "title": "B2C DX — Developer Onboarding", - "description": "A guided 5-phase setup — install, configure, connect, deploy, scaffold — for the Salesforce B2C Commerce Developer Experience extension. Most teams complete this in under 30 minutes.", - "icon": "media/b2c-icon.svg", - "when": "config.b2c-dx.features.onboarding", - "steps": [ - { - "id": "pick-start", - "title": "Phase 0 · Choose your role", - "description": "Different roles need different things first. Open the **role-based deep-dive** for a path tailored to how you'll use B2C Commerce — Storefront, API/Integration, DevOps/Release, or AI-augmented developer. Or skip ahead and follow the universal five-phase flow below.\n\n[Open role-based guide](command:b2c-dx.onboarding.open)\n\nAlready set up? [Mark all phases as done](command:b2c-dx.walkthrough.markAllDone).", - "media": { - "image": { - "light": "media/walkthrough/welcome-hero-light.svg", - "dark": "media/walkthrough/welcome-hero-dark.svg", - "hc": "media/walkthrough/welcome-hero-dark.svg", - "hcLight": "media/walkthrough/welcome-hero-light.svg" - }, - "altText": "B2C DX wordmark above a vertical five-step timeline: Install the B2C CLI (optional), Configure your instance (dw.json), Connect and authenticate, Deploy your first cartridge, and Generate from a scaffold." - }, - "completionEvents": [ - "onCommand:b2c-dx.onboarding.open", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - }, - { - "id": "install-cli", - "title": "Phase 1 · Install the B2C CLI", - "description": "**Optional.** The B2C CLI powers deploys, log tailing, and sandbox commands from the terminal. Skip if you'll only use the VS Code views.\n\nInstall via `npm install -g @salesforce/b2c-cli` or `brew install salesforcecommercecloud/tools/b2c-cli`. \n\nRequires Node.js 22+.\n\n[Verify installation](command:b2c-dx.cli.verify) · [Update to latest](command:b2c-dx.cli.update)", - "media": { - "markdown": "media/walkthrough/install-cli.md" - }, - "completionEvents": [ - "onCommand:b2c-dx.cli.verify", - "onContext:b2c-dx.cliInstalled", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - }, - { - "id": "configure-instance", - "title": "Phase 2 · Configure your instance", - "description": "Run the configuration wizard. Non-secret fields (hostname, code-version, short-code, tenant-id, mrtProject, mrtEnvironment) go into `dw.json`; secret pairs (OAuth `client-id`+`client-secret`, Basic `username`+`password`, MRT API key) are placed independently — Keychain, `pass`, env vars, or dw.json — per the documented Credential Grouping rule.\n\n[Run setup wizard](command:b2c-dx.walkthrough.chooseCredentialStorage) · [Inspect resolved config](command:b2c-dx.walkthrough.inspectSetup) · [Create dw.json (manual)](command:b2c-dx.walkthrough.createDwJson)", - "media": { - "markdown": "media/walkthrough/dw-json-setup.md" - }, - "completionEvents": [ - "onCommand:b2c-dx.walkthrough.chooseCredentialStorage", - "onCommand:b2c-dx.walkthrough.createDwJson", - "onContext:b2c-dx.dwJsonExists", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - }, - { - "id": "connect", - "title": "Phase 3 · Connect & authenticate", - "description": "Verify your config resolves and the extension can reach your instance. The selected instance shows in the status bar (bottom-left).\n\nOAuth fields (`client-id`, `client-secret`, `short-code`) unlock the Sandbox Explorer and API Browser; basic auth (`username`, `password`) is enough for WebDAV and cartridge deploys.\n\n[Inspect resolved config](command:b2c-dx.walkthrough.inspectSetup) — runs `b2c setup inspect` and shows where each value came from (file / env / keychain).\n\n[Inspect selected instance](command:b2c-dx.instance.inspect) · [Open Realm Explorer](command:workbench.view.extension.b2c-dx-sandboxes)", - "media": { - "markdown": "media/walkthrough/oauth-setup.md" - }, - "completionEvents": [ - "onContext:b2c-dx.instanceConnected", - "onCommand:b2c-dx.instance.inspect", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - }, - { - "id": "deploy-cartridge", - "title": "Phase 4 · Deploy your first cartridge", - "description": "Cartridges are folders containing a `.project` file. The extension auto-detects them in your workspace and lists them under the Cartridges view.\n\n[Deploy all cartridges](command:b2c-dx.codeSync.deploy)\n\n[Open Cartridges view](command:workbench.view.extension.b2c-dx)", - "media": { - "markdown": "media/walkthrough/deploy-cartridge.md" - }, - "completionEvents": [ - "onCommand:b2c-dx.codeSync.deploy", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - }, - { - "id": "scaffold", - "title": "Phase 5 · Generate from a scaffold", - "description": "Generate boilerplate for cartridges, controllers, models, and Page Designer pages without leaving VS Code.\n\nRight-click a folder in the Explorer → **B2C DX → New from Scaffold…** or run the command directly.\n\n[New from Scaffold…](command:b2c-dx.scaffold.generate)\n\n---\n\nFinished early? [Mark all steps as done](command:b2c-dx.walkthrough.markAllDone).", - "media": { - "markdown": "media/walkthrough/next-steps.md" - }, - "completionEvents": [ - "onCommand:b2c-dx.scaffold.generate", - "onCommand:b2c-dx.walkthrough.markAllDone" - ] - } - ] } ], "menus": { @@ -1916,54 +1801,6 @@ "command": "b2c-dx.isml.showReferences", "when": "editorLangId == isml" }, - { - "command": "b2c-dx.onboarding.open", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.onboarding.reset", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.onboarding.changePersona", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.open", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.markAllDone", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.resetProgress", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.createDwJson", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.chooseCredentialStorage", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.inspectSetup", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.validate", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.checkAccessibility", - "when": "config.b2c-dx.features.onboarding" - }, - { - "command": "b2c-dx.walkthrough.showTelemetry", - "when": "config.b2c-dx.features.onboarding" - }, { "command": "b2c-dx.jobs.openFilters", "when": "config.b2c-dx.features.jobsExplorer" @@ -2259,6 +2096,58 @@ { "command": "b2c-dx.codeSync.removeFromSitePath", "when": "false" + }, + { + "command": "b2c-dx.cli.installBrew", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.cli.installNpm", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.cli.recheck", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.cli.update", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.cli.verify", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.setup.connection", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.setup.oauth", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.setup.resetSession", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.setup.scapi", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.setup.webdav", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.walkthrough.chooseCredentialStorage", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.walkthrough.createDwJson", + "when": "config.b2c-dx.features.setup" + }, + { + "command": "b2c-dx.walkthrough.inspectSetup", + "when": "config.b2c-dx.features.setup" } ] }, @@ -2268,6 +2157,28 @@ "key": "f2", "when": "focusedView == b2cWebdavExplorer" } + ], + "mcpServerDefinitionProviders": [ + { + "id": "b2c-commerce", + "label": "B2C Commerce" + } + ], + "languageModelTools": [ + { + "name": "b2c_get_ide_context", + "displayName": "B2C IDE Context", + "toolReferenceName": "b2cContext", + "canBeReferencedInPrompt": true, + "icon": "$(cloud)", + "userDescription": "Read the selected B2C instance, project root, and live code-sync status. No credentials are returned.", + "modelDescription": "Read the current B2C Commerce IDE selection and live code-sync status. When the user has not specified a target, call this before configuration-dependent B2C tools and pass the returned projectDirectory, configPath, and instanceName to them. Explicit user targets take precedence. Refresh after an instance switch; existing sessions retain their target. If status is unavailable or unconfigured, do not guess an instance. Code-sync status includes its actual upload target when active. Does not expose credentials or change configuration.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + } ] }, "scripts": { diff --git a/packages/b2c-vs-extension/scripts/esbuild-bundle.mjs b/packages/b2c-vs-extension/scripts/esbuild-bundle.mjs index 2e16e5c78..e95d06b01 100644 --- a/packages/b2c-vs-extension/scripts/esbuild-bundle.mjs +++ b/packages/b2c-vs-extension/scripts/esbuild-bundle.mjs @@ -37,6 +37,7 @@ const scriptTypesRoot = path.join(pkgRoot, '..', 'b2c-script-types'); const watchMode = process.argv.includes('--watch'); const extPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')); +const mcpPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, '..', 'b2c-dx-mcp', 'package.json'), 'utf8')); // Resolve vscode-html-languageservice's ESM entry (its `module` field). We alias // the bare import to this so esbuild bundles the statically-importable ESM build @@ -247,6 +248,7 @@ const buildOptions = { // Build-time constants — read once at bundle time so the runtime doesn't readFileSync(package.json). define: { __EXT_VERSION__: JSON.stringify(extPkg.version), + __MCP_VERSION__: JSON.stringify(mcpPkg.version), __TELEMETRY_CONNECTION_STRING__: JSON.stringify(extPkg.telemetry?.connectionString ?? ''), }, minify: !watchMode, diff --git a/packages/b2c-vs-extension/src/ai/cursor-mcp.ts b/packages/b2c-vs-extension/src/ai/cursor-mcp.ts new file mode 100644 index 000000000..c3f41fd00 --- /dev/null +++ b/packages/b2c-vs-extension/src/ai/cursor-mcp.ts @@ -0,0 +1,85 @@ +/* + * 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 vscode from 'vscode'; +import type {B2CMcpServerDefinitionProvider} from './mcp-provider.js'; +import type {IdeContext} from './ide-context.js'; +import {startIdeContextBridge} from './ide-context-bridge.js'; + +export interface CursorMcpApi { + registerServer(config: {name: string; server: {command: string; args: string[]; env: Record}}): void; + unregisterServer(name: string): void; +} + +export function getCursorMcpApi(): CursorMcpApi | undefined { + const api = (vscode as typeof vscode & {cursor?: {mcp?: CursorMcpApi}}).cursor?.mcp; + return typeof api?.registerServer === 'function' && typeof api.unregisterServer === 'function' ? api : undefined; +} + +const COMMERCE_SERVER = 'salesforce-b2c-commerce'; + +export class CursorMcpRegistration implements vscode.Disposable { + private bridge: Awaited> | undefined; + private definition: string | undefined; + private disposed = false; + private pending = Promise.resolve(); + + constructor( + private readonly api: CursorMcpApi, + private readonly provider: B2CMcpServerDefinitionProvider, + private readonly readContext: () => Promise, + ) {} + + refresh(): Promise { + const update = this.pending.then(() => this.update()); + this.pending = update.catch(() => {}); + return update; + } + + async dispose(): Promise { + this.disposed = true; + await this.pending; + await this.clear(); + } + + private async clear(): Promise { + if (this.definition) this.api.unregisterServer(COMMERCE_SERVER); + this.definition = undefined; + if (this.bridge) { + await this.bridge.dispose(); + this.bridge = undefined; + } + } + + private async update(): Promise { + if (this.disposed) return; + const cancellation = new vscode.CancellationTokenSource(); + try { + const [definition] = await this.provider.provideMcpServerDefinitions(cancellation.token); + if (this.disposed) return; + if (!definition) { + await this.clear(); + return; + } + if (!this.bridge) this.bridge = await startIdeContextBridge(this.readContext); + if (this.disposed) return; + // Cursor has no cwd field; --project-directory is already in the arguments. + const server = { + command: definition.command, + args: [...definition.args, '--ide-context-url', this.bridge.url], + env: {SFCC_IDE_CONTEXT_TOKEN: this.bridge.token}, + }; + const serialized = JSON.stringify(server); + if (serialized !== this.definition) { + if (this.definition) this.api.unregisterServer(COMMERCE_SERVER); + this.definition = undefined; + this.api.registerServer({name: COMMERCE_SERVER, server}); + this.definition = serialized; + } + } finally { + cancellation.dispose(); + } + } +} diff --git a/packages/b2c-vs-extension/src/ai/ide-context-bridge.ts b/packages/b2c-vs-extension/src/ai/ide-context-bridge.ts new file mode 100644 index 000000000..1215d589a --- /dev/null +++ b/packages/b2c-vs-extension/src/ai/ide-context-bridge.ts @@ -0,0 +1,70 @@ +/* + * 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 {randomBytes} from 'node:crypto'; +import {createServer} from 'node:http'; +import type {IdeContext} from './ide-context.js'; + +/** Private, per-window connection from the Commerce MCP process to live IDE state. */ +export async function startIdeContextBridge(readContext: () => Promise): Promise<{ + url: string; + token: string; + dispose(): Promise; +}> { + const token = randomBytes(32).toString('hex'); + let host = ''; + const server = createServer({requestTimeout: 10_000, headersTimeout: 10_000}, (request, response) => { + if ( + request.headers.host !== host || + request.headers.origin || + request.headers.authorization !== `Bearer ${token}` + ) { + response.writeHead(403).end(); + return; + } + if (request.url !== '/context') { + response.writeHead(404).end(); + return; + } + if (request.method !== 'GET') { + response.writeHead(405, {Allow: 'GET'}).end(); + return; + } + const timer = setTimeout(() => { + if (!response.writableEnded && !response.destroyed) response.writeHead(504).end(); + }, 10_000); + response.on('close', () => clearTimeout(timer)); + void readContext() + .then((context) => { + if (!response.writableEnded && !response.destroyed) { + response + .writeHead(200, {'Content-Type': 'application/json', 'Cache-Control': 'no-store'}) + .end(JSON.stringify(context)); + } + }) + .catch(() => { + if (!response.writableEnded && !response.destroyed) response.writeHead(503).end(); + }) + .finally(() => clearTimeout(timer)); + }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.removeListener('error', reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === 'string') throw new Error('Could not start B2C IDE context bridge'); + host = `127.0.0.1:${address.port}`; + return { + url: `http://${host}/context`, + token, + async dispose() { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + }, + }; +} diff --git a/packages/b2c-vs-extension/src/ai/ide-context.ts b/packages/b2c-vs-extension/src/ai/ide-context.ts new file mode 100644 index 000000000..c269f78c3 --- /dev/null +++ b/packages/b2c-vs-extension/src/ai/ide-context.ts @@ -0,0 +1,67 @@ +/* + * 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 vscode from 'vscode'; +import type {B2CExtensionConfig} from '../config-provider.js'; + +export interface CodeSyncStatus { + available: boolean; + active: boolean; + hostname?: string; + codeVersion?: string; +} + +export interface IdeContext { + status: 'ready' | 'unconfigured' | 'unavailable'; + selectionMode: 'workspace' | 'default'; + projectDirectory?: string; + projectRootPinned: boolean; + configPath?: string; + instanceName?: string; + hostname?: string; + codeVersion?: string; + codeSync: CodeSyncStatus; +} + +/** Only allowlisted connection metadata may cross into a model's context. */ +export async function readIdeContext( + configProvider: B2CExtensionConfig, + getCodeSyncStatus: () => CodeSyncStatus, +): Promise { + await configProvider.ensureResolved(); + const config = configProvider.getConfig(); + const selection = configProvider.getWorkspaceInstanceSelection(); + const source = config?.sources.find((entry) => entry.name === 'DwJsonSource' && entry.location); + const projectDirectory = configProvider.getWorkingDirectory() || undefined; + return { + status: config?.hasB2CInstanceConfig() ? 'ready' : selection ? 'unavailable' : 'unconfigured', + selectionMode: selection ? 'workspace' : 'default', + projectDirectory, + projectRootPinned: configProvider.isProjectRootPinned(), + configPath: selection?.location ?? source?.location, + instanceName: selection?.name ?? config?.values.instanceName, + hostname: config?.values.hostname, + codeVersion: config?.values.codeVersion, + codeSync: getCodeSyncStatus(), + }; +} + +export class IdeContextTool implements vscode.LanguageModelTool> { + constructor(private readonly readContext: () => Promise) {} + + prepareInvocation(): vscode.PreparedToolInvocation { + return {invocationMessage: 'Reading the selected B2C instance and code-sync status'}; + } + + async invoke( + _options: vscode.LanguageModelToolInvocationOptions>, + token: vscode.CancellationToken, + ): Promise { + if (token.isCancellationRequested) throw new vscode.CancellationError(); + const context = await this.readContext(); + if (token.isCancellationRequested) throw new vscode.CancellationError(); + return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(JSON.stringify(context))]); + } +} diff --git a/packages/b2c-vs-extension/src/ai/index.ts b/packages/b2c-vs-extension/src/ai/index.ts new file mode 100644 index 000000000..c14c2454f --- /dev/null +++ b/packages/b2c-vs-extension/src/ai/index.ts @@ -0,0 +1,61 @@ +/* + * 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 vscode from 'vscode'; +import type {B2CExtensionConfig} from '../config-provider.js'; +import {IdeContextTool, readIdeContext, type CodeSyncStatus} from './ide-context.js'; +import {B2CMcpServerDefinitionProvider} from './mcp-provider.js'; +import {CursorMcpRegistration, getCursorMcpApi} from './cursor-mcp.js'; +import {getLogger} from '@salesforce/b2c-tooling-sdk/logging'; + +export function registerAiIntegration( + context: vscode.ExtensionContext, + configProvider: B2CExtensionConfig, + getCodeSyncStatus: () => CodeSyncStatus, +): void { + const readContext = () => readIdeContext(configProvider, getCodeSyncStatus); + const cursor = getCursorMcpApi(); + // VS Code forks do not necessarily implement either of these APIs. + if (!cursor && typeof vscode.lm?.registerTool === 'function') { + context.subscriptions.push(vscode.lm.registerTool('b2c_get_ide_context', new IdeContextTool(readContext))); + } + if (!cursor && typeof vscode.lm?.registerMcpServerDefinitionProvider !== 'function') return; + + const changed = new vscode.EventEmitter(); + const version = __MCP_VERSION__; + const provider = new B2CMcpServerDefinitionProvider( + readContext, + () => { + const settings = vscode.workspace.getConfiguration('b2c-dx'); + return { + enabled: settings.get('mcp.enabled', true), + command: settings.get('mcp.command', 'npx'), + args: settings.get('mcp.args') ?? ['-y', `@salesforce/b2c-dx-mcp@${version}`], + }; + }, + changed.event, + version, + ); + if (cursor) { + const registration = new CursorMcpRegistration(cursor, provider, readContext); + const refresh = () => { + void registration.refresh().catch((error: unknown) => { + getLogger().warn({err: error}, 'Could not register B2C MCP servers with Cursor'); + }); + }; + context.subscriptions.push(registration, changed.event(refresh)); + refresh(); + } else { + context.subscriptions.push(vscode.lm.registerMcpServerDefinitionProvider('b2c-commerce', provider)); + } + context.subscriptions.push( + changed, + configProvider.onDidReset(() => changed.fire()), + vscode.workspace.onDidGrantWorkspaceTrust(() => changed.fire()), + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration('b2c-dx.mcp')) changed.fire(); + }), + ); +} diff --git a/packages/b2c-vs-extension/src/ai/mcp-provider.ts b/packages/b2c-vs-extension/src/ai/mcp-provider.ts new file mode 100644 index 000000000..4e0ed4626 --- /dev/null +++ b/packages/b2c-vs-extension/src/ai/mcp-provider.ts @@ -0,0 +1,53 @@ +/* + * 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 vscode from 'vscode'; +import type {IdeContext} from './ide-context.js'; + +export interface McpSettings { + enabled: boolean; + command: string; + args: string[]; +} + +export class B2CMcpServerDefinitionProvider implements vscode.McpServerDefinitionProvider { + constructor( + private readonly readContext: () => Promise, + private readonly getSettings: () => McpSettings, + readonly onDidChangeMcpServerDefinitions: vscode.Event, + private readonly serverVersion: string, + ) {} + + async provideMcpServerDefinitions(token: vscode.CancellationToken): Promise { + if (!this.getSettings().enabled || !vscode.workspace.isTrusted || token.isCancellationRequested) return []; + const context = await this.readContext(); + if (!context.projectDirectory || token.isCancellationRequested) return []; + return [this.createDefinition(context)]; + } + + async resolveMcpServerDefinition( + _server: vscode.McpServerDefinition, + token: vscode.CancellationToken, + ): Promise { + // Resolve again at launch: the user may have switched instances since discovery. + if (!this.getSettings().enabled || !vscode.workspace.isTrusted || token.isCancellationRequested) return; + const context = await this.readContext(); + if (!context.projectDirectory || token.isCancellationRequested) return; + if (context.status === 'unavailable') { + throw new Error('The selected B2C instance is unavailable. Select another instance before starting MCP.'); + } + return this.createDefinition(context); + } + + private createDefinition(context: IdeContext): vscode.McpStdioServerDefinition { + const settings = this.getSettings(); + const args = [...settings.args, '--project-directory', context.projectDirectory!]; + if (context.configPath) args.push('--config', context.configPath); + if (context.instanceName) args.push('--instance', context.instanceName); + const server = new vscode.McpStdioServerDefinition('B2C Commerce', settings.command, args, {}, this.serverVersion); + server.cwd = vscode.Uri.file(context.projectDirectory!); + return server; + } +} diff --git a/packages/b2c-vs-extension/src/build-constants.d.ts b/packages/b2c-vs-extension/src/build-constants.d.ts index 69ff39128..998da82ad 100644 --- a/packages/b2c-vs-extension/src/build-constants.d.ts +++ b/packages/b2c-vs-extension/src/build-constants.d.ts @@ -10,4 +10,5 @@ * as "no telemetry configured". */ declare const __EXT_VERSION__: string; +declare const __MCP_VERSION__: string; declare const __TELEMETRY_CONNECTION_STRING__: string; diff --git a/packages/b2c-vs-extension/src/code-sync/code-sync-manager.ts b/packages/b2c-vs-extension/src/code-sync/code-sync-manager.ts index 1978dcba9..2b54b4018 100644 --- a/packages/b2c-vs-extension/src/code-sync/code-sync-manager.ts +++ b/packages/b2c-vs-extension/src/code-sync/code-sync-manager.ts @@ -50,6 +50,12 @@ export class CodeSyncManager implements vscode.Disposable { return this.watching; } + getStatus(): {active: boolean; hostname?: string; codeVersion?: string} { + return this.watching + ? {active: true, hostname: this.instance?.config.hostname, codeVersion: this.codeVersion} + : {active: false}; + } + get discoveredCartridges(): CartridgeMapping[] { return this.cartridges; } diff --git a/packages/b2c-vs-extension/src/code-sync/index.ts b/packages/b2c-vs-extension/src/code-sync/index.ts index 392e5d51a..140c47bc4 100644 --- a/packages/b2c-vs-extension/src/code-sync/index.ts +++ b/packages/b2c-vs-extension/src/code-sync/index.ts @@ -22,7 +22,7 @@ export function registerCodeSync( configProvider: B2CExtensionConfig, cartridgeService: CartridgeService, log: vscode.OutputChannel, -): void { +): CodeSyncManager { const manager = new CodeSyncManager(context.workspaceState, configProvider); const treeProvider = new CartridgeTreeProvider(cartridgeService); const treeView = vscode.window.createTreeView('b2cCartridgeExplorer', {treeDataProvider: treeProvider}); @@ -210,4 +210,5 @@ export function registerCodeSync( cartridgesSub, ...cartridgeCmdDisposables, ); + return manager; } diff --git a/packages/b2c-vs-extension/src/config-provider.ts b/packages/b2c-vs-extension/src/config-provider.ts index 78481d4f1..32214ef91 100644 --- a/packages/b2c-vs-extension/src/config-provider.ts +++ b/packages/b2c-vs-extension/src/config-provider.ts @@ -117,6 +117,8 @@ export class B2CExtensionConfig implements vscode.Disposable { private instance: B2CInstance | null = null; private configError: string | null = null; private resolved = false; + private resolution: Promise | undefined; + private configRevision = 0; private detectedDirectory = ''; private pinned = false; private resolvedEnvironment: Record; @@ -264,8 +266,16 @@ export class B2CExtensionConfig implements vscode.Disposable { * Call this before reading from getters when you need fresh data. */ async ensureResolved(): Promise { - if (!this.resolved) { - await this.resolveAsync(); + // Readers must wait for resets too, including a selection changed mid-resolution. + while (!this.resolved || this.resolution) { + if (!this.resolution) { + const revision = this.configRevision; + this.resolution = this.resolveAsync().finally(() => { + this.resolution = undefined; + this.resolved = revision === this.configRevision; + }); + } + await this.resolution; } } @@ -289,6 +299,7 @@ export class B2CExtensionConfig implements vscode.Disposable { } reset(): void { + this.configRevision++; this.log.appendLine('[Config] Resetting cached config (will re-resolve asynchronously)'); this.config = null; this.instance = null; @@ -298,7 +309,7 @@ export class B2CExtensionConfig implements vscode.Disposable { this.pinned = false; this.resolvedEnvironment = this.ambientEnvironment; // Re-resolve asynchronously, then fire the event so listeners get fresh data - void this.resolveAsync().then(() => { + void this.ensureResolved().then(() => { this._onDidReset.fire(); }); } @@ -376,7 +387,6 @@ export class B2CExtensionConfig implements vscode.Disposable { } private async resolveAsync(): Promise { - this.resolved = true; try { // Check for pinned project root first const pinnedRoot = this.workspaceState?.get(PROJECT_ROOT_KEY); diff --git a/packages/b2c-vs-extension/src/extension.ts b/packages/b2c-vs-extension/src/extension.ts index 74b71a845..dd39d91b3 100644 --- a/packages/b2c-vs-extension/src/extension.ts +++ b/packages/b2c-vs-extension/src/extension.ts @@ -42,16 +42,9 @@ import { isWorkspaceInstanceSelected, triggerInstancePickerButton, } from './instance-selection.js'; -import { - registerWalkthroughCommands, - resetWorkspaceOnboardingIfFresh, - showWalkthroughOnFirstActivation, - initializeTelemetry, - validateWalkthroughCommand, - checkWalkthroughAccessibilityCommand, - OnboardingStateStore, - OnboardingPanel, -} from './walkthrough/index.js'; +import {registerSetupCommands, resetSetupSessionIfFresh} from './setup/commands.js'; +import {registerAiIntegration} from './ai/index.js'; +import type {CodeSyncManager} from './code-sync/code-sync-manager.js'; let authSessionBackend: VsCodeSecretsAuthSessionBackend | undefined; @@ -299,190 +292,120 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu // before the first resolveConfig() call. Failures are non-fatal. await initializePlugins(); - // Initialize walkthrough telemetry - const walkthroughTelemetry = initializeTelemetry(log); - - // Register walkthrough commands early so they're available for first-time users - runActivationStep(log, 'Walkthrough command registration', () => { - registerWalkthroughCommands(context); - }); - - // Onboarding (next-gen walkthrough) state + panel. - // The configProvider is created later (line ~480), so we expose a lazy getter - // that the panel calls at refresh time — by then the provider is resolved. - const onboardingStore = new OnboardingStateStore(context); - context.subscriptions.push(onboardingStore); - // Forward declare; the actual configProvider is assigned below once created. - let lateConfigProvider: B2CExtensionConfig | null = null; - const getConfigProvider = (): B2CExtensionConfig | null => lateConfigProvider; - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.onboarding.open', () => { - OnboardingPanel.show(context, onboardingStore, log, getConfigProvider); - }), - vscode.commands.registerCommand('b2c-dx.onboarding.reset', async () => { - await onboardingStore.reset(); - OnboardingPanel.show(context, onboardingStore, log, getConfigProvider); - }), - vscode.commands.registerCommand('b2c-dx.onboarding.changePersona', async () => { - await onboardingStore.setPersona(null); - OnboardingPanel.show(context, onboardingStore, log, getConfigProvider); - }), - ); - - // Register walkthrough validation commands (for development/testing) - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.walkthrough.validate', async () => { - await validateWalkthroughCommand(context.extensionPath, log); - }), - vscode.commands.registerCommand('b2c-dx.walkthrough.checkAccessibility', async () => { - await checkWalkthroughAccessibilityCommand(context.extensionPath, log); - }), - vscode.commands.registerCommand('b2c-dx.walkthrough.showTelemetry', () => { - walkthroughTelemetry.logSummary(); - log.show(); - }), - ); + const setupEnabled = vscode.workspace.getConfiguration('b2c-dx').get('features.setup', false); + if (setupEnabled) { + runActivationStep(log, 'Setup command registration', () => { + registerSetupCommands(context); + }); - // "Verify CLI" — runs `b2c --version`, queries npm for the latest, and - // reports back. Flips two context keys: - // b2c-dx.cliInstalled — auto-completes the install-cli walkthrough step. - // b2c-dx.cliOutdated — surfaces the "Update CLI" action when true. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.cli.verify', async () => { - const result = await detectB2cCli(context); - await vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', result.installed); - await vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!result.isOutdated); - - if (!result.installed) { - const action = await vscode.window.showWarningMessage( - 'B2C CLI not found on PATH. Install with `npm install -g @salesforce/b2c-cli` or `brew install salesforcecommercecloud/tools/b2c-cli`.', - 'Open Install Guide', - ); - if (action === 'Open Install Guide') { - await vscode.env.openExternal( - vscode.Uri.parse('https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/installation.html'), + // "Verify CLI" — runs `b2c --version`, queries npm for the latest, and + // reports back. Flips two context keys: + // b2c-dx.cliInstalled — indicates CLI availability. + // b2c-dx.cliOutdated — surfaces the "Update CLI" action when true. + context.subscriptions.push( + vscode.commands.registerCommand('b2c-dx.cli.verify', async () => { + const result = await detectB2cCli(context); + await vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', result.installed); + await vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!result.isOutdated); + + if (!result.installed) { + const action = await vscode.window.showWarningMessage( + 'B2C CLI not found on PATH. Install with `npm install -g @salesforce/b2c-cli` or `brew install salesforcecommercecloud/tools/b2c-cli`.', + 'Open Install Guide', ); + if (action === 'Open Install Guide') { + await vscode.env.openExternal( + vscode.Uri.parse( + 'https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/installation.html', + ), + ); + } + return; } - return; - } - if (result.isOutdated && result.latestVersion) { - const action = await vscode.window.showInformationMessage( - `B2C CLI ${result.version} detected — newer version ${result.latestVersion} available.`, - 'Update now', - 'Copy update command', - 'Later', - ); - if (action === 'Update now') { - await vscode.commands.executeCommand('b2c-dx.cli.update'); - } else if (action === 'Copy update command') { - await vscode.env.clipboard.writeText('npm install -g @salesforce/b2c-cli@latest'); - vscode.window.showInformationMessage('Update command copied to clipboard.'); + if (result.isOutdated && result.latestVersion) { + const action = await vscode.window.showInformationMessage( + `B2C CLI ${result.version} detected — newer version ${result.latestVersion} available.`, + 'Update now', + 'Copy update command', + 'Later', + ); + if (action === 'Update now') { + await vscode.commands.executeCommand('b2c-dx.cli.update'); + } else if (action === 'Copy update command') { + await vscode.env.clipboard.writeText('npm install -g @salesforce/b2c-cli@latest'); + vscode.window.showInformationMessage('Update command copied to clipboard.'); + } + return; } - return; - } - - const suffix = result.latestVersion ? ` (latest)` : ''; - vscode.window.showInformationMessage(`B2C CLI detected: ${result.version}${suffix}`); - }), - ); - // "Install CLI via npm" — opens a terminal with the install command. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.cli.installNpm', async () => { - const term = vscode.window.createTerminal({name: 'B2C DX — CLI install'}); - term.show(); - term.sendText('npm install -g @salesforce/b2c-cli', false); - }), - ); - - // "Install CLI via Homebrew" — opens a terminal with the brew install command. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.cli.installBrew', async () => { - const term = vscode.window.createTerminal({name: 'B2C DX — CLI install'}); - term.show(); - term.sendText('brew install salesforcecommercecloud/tools/b2c-cli', false); - }), - ); - - // "Re-check CLI" — re-runs the CLI detection and refreshes state. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.cli.recheck', async () => { - const result = await detectB2cCli(context); - await vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', result.installed); - await vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!result.isOutdated); - if (result.installed) { - const suffix = - result.isOutdated && result.latestVersion ? ` (v${result.latestVersion} available)` : ' (latest)'; + const suffix = result.latestVersion ? ` (latest)` : ''; vscode.window.showInformationMessage(`B2C CLI detected: ${result.version}${suffix}`); - } else { - vscode.window.showWarningMessage('B2C CLI still not found on PATH. Install it and try again.'); - } - }), - ); + }), + ); - // "Update CLI" — opens a terminal preloaded with the npm update command. - // We never auto-execute: a global npm install can prompt for credentials - // or hit privilege errors, so the user runs it themselves. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.cli.update', async () => { - const cmd = 'npm install -g @salesforce/b2c-cli@latest'; - const choice = await vscode.window.showInformationMessage( - 'Update the B2C CLI to the latest version? This runs an npm global install — you may be prompted for permissions.', - {modal: true}, - 'Run in terminal', - 'Copy command', - 'Cancel', - ); - if (!choice || choice === 'Cancel') return; - if (choice === 'Run in terminal') { - const term = vscode.window.createTerminal({name: 'B2C DX — CLI update'}); + // "Install CLI via npm" — opens a terminal with the install command. + context.subscriptions.push( + vscode.commands.registerCommand('b2c-dx.cli.installNpm', async () => { + const term = vscode.window.createTerminal({name: 'B2C DX — CLI install'}); term.show(); - // Don't auto-execute — user presses Enter so they see the command first. - term.sendText(cmd, false); - } else { - await vscode.env.clipboard.writeText(cmd); - vscode.window.showInformationMessage('Update command copied to clipboard.'); - } - // Invalidate the cached "latest" so the next verify makes a fresh check. - await context.globalState.update(LATEST_CACHE_KEY, undefined); - }), - ); + term.sendText('npm install -g @salesforce/b2c-cli', false); + }), + ); - // "Mark all as done" — fires a single onCommand event that every walkthrough - // step lists in its completionEvents, ticking the entire walkthrough at once. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.walkthrough.markAllDone', async () => { - // Re-open the walkthrough so the user sees the freshly-ticked steps. - await vscode.commands.executeCommand( - 'workbench.action.openWalkthrough', - 'Salesforce.b2c-vs-extension#b2c-dx.gettingStarted', - false, - ); - vscode.window.showInformationMessage('B2C DX: Getting Started marked as complete.'); - }), - // "Reset Getting Started Progress" — clears both surfaces: - // • our per-workspace OnboardingStateStore (deep-dive panel) - // • VS Code's per-installation native walkthrough ticks - // VS Code stores native walkthrough completion in user-global state and - // does not expose a per-workspace API to clear it; this command lets the - // user trigger a clean slate manually when switching workspaces. - vscode.commands.registerCommand('b2c-dx.walkthrough.resetProgress', async () => { - await onboardingStore.reset(); - await context.workspaceState.update('b2c-dx.gettingStarted.autoOpened', undefined); - try { - await vscode.commands.executeCommand('resetGettingStartedProgress'); - } catch { - // built-in command not available in older VS Code releases; no-op - } - await vscode.commands.executeCommand( - 'workbench.action.openWalkthrough', - 'Salesforce.b2c-vs-extension#b2c-dx.gettingStarted', - false, - ); - vscode.window.showInformationMessage('B2C DX: Getting Started progress reset.'); - }), - ); + // "Install CLI via Homebrew" — opens a terminal with the brew install command. + context.subscriptions.push( + vscode.commands.registerCommand('b2c-dx.cli.installBrew', async () => { + const term = vscode.window.createTerminal({name: 'B2C DX — CLI install'}); + term.show(); + term.sendText('brew install salesforcecommercecloud/tools/b2c-cli', false); + }), + ); + + // "Re-check CLI" — re-runs the CLI detection and refreshes state. + context.subscriptions.push( + vscode.commands.registerCommand('b2c-dx.cli.recheck', async () => { + const result = await detectB2cCli(context); + await vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', result.installed); + await vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!result.isOutdated); + if (result.installed) { + const suffix = + result.isOutdated && result.latestVersion ? ` (v${result.latestVersion} available)` : ' (latest)'; + vscode.window.showInformationMessage(`B2C CLI detected: ${result.version}${suffix}`); + } else { + vscode.window.showWarningMessage('B2C CLI still not found on PATH. Install it and try again.'); + } + }), + ); + + // "Update CLI" — opens a terminal preloaded with the npm update command. + // We never auto-execute: a global npm install can prompt for credentials + // or hit privilege errors, so the user runs it themselves. + context.subscriptions.push( + vscode.commands.registerCommand('b2c-dx.cli.update', async () => { + const cmd = 'npm install -g @salesforce/b2c-cli@latest'; + const choice = await vscode.window.showInformationMessage( + 'Update the B2C CLI to the latest version? This runs an npm global install — you may be prompted for permissions.', + {modal: true}, + 'Run in terminal', + 'Copy command', + 'Cancel', + ); + if (!choice || choice === 'Cancel') return; + if (choice === 'Run in terminal') { + const term = vscode.window.createTerminal({name: 'B2C DX — CLI update'}); + term.show(); + // Don't auto-execute — user presses Enter so they see the command first. + term.sendText(cmd, false); + } else { + await vscode.env.clipboard.writeText(cmd); + vscode.window.showInformationMessage('Update command copied to clipboard.'); + } + // Invalidate the cached "latest" so the next verify makes a fresh check. + await context.globalState.update(LATEST_CACHE_KEY, undefined); + }), + ); + } // Theme toggle — flips between the user's preferred light + dark themes. // Persists the last-seen pair so a developer who customised their theme @@ -518,17 +441,19 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu }), ); - // Initialize the cliInstalled context key once (best-effort, non-blocking). - void detectB2cCli(context).then((r) => { - void vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', r.installed); - void vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!r.isOutdated); - }); + if (setupEnabled) { + // Initialize the cliInstalled context key once (best-effort, non-blocking). + void detectB2cCli(context).then((r) => { + void vscode.commands.executeCommand('setContext', 'b2c-dx.cliInstalled', r.installed); + void vscode.commands.executeCommand('setContext', 'b2c-dx.cliOutdated', !!r.isOutdated); + }); - // Initialize the setup-session context keys from workspaceState so welcome - // views can react on first frame. - const sessionInstance = context.workspaceState.get('b2c-dx.setup.activeInstance'); - void vscode.commands.executeCommand('setContext', 'b2c-dx.setupSessionActive', !!sessionInstance); - void vscode.commands.executeCommand('setContext', 'b2c-dx.setupInstance', sessionInstance); + // Initialize the setup-session context keys from workspaceState so welcome + // views can react on first frame. + const sessionInstance = context.workspaceState.get('b2c-dx.setup.activeInstance'); + void vscode.commands.executeCommand('setContext', 'b2c-dx.setupSessionActive', !!sessionInstance); + void vscode.commands.executeCommand('setContext', 'b2c-dx.setupInstance', sessionInstance); + } registerJobLogViewer(context); @@ -541,7 +466,6 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu setAuthSessionBackend(authSessionBackend); const configProvider = new B2CExtensionConfig(log, context.workspaceState); - lateConfigProvider = configProvider; context.subscriptions.push(configProvider); await configProvider.ensureResolved(); @@ -557,9 +481,7 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu const cartridgeService = new CartridgeService(configProvider); context.subscriptions.push(cartridgeService); - // Walkthrough context keys: drive auto-completion of the native walkthrough - // steps. dwJsonExists tracks the per-workspace dw.json file; instanceConnected - // mirrors whether the config provider successfully resolved a config. + // Context keys used by the instance and setup commands. const updateInstanceConnectedContext = () => { const connected = !!configProvider.getConfig(); void vscode.commands.executeCommand('setContext', 'b2c-dx.instanceConnected', connected); @@ -947,11 +869,19 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu registerCap(context, configProvider, log); }); } + let codeSyncManager: CodeSyncManager | undefined; if (settings.get('features.codeSync', true)) { runActivationStep(log, 'Code Sync registration', () => { - registerCodeSync(context, configProvider, cartridgeService, log); + codeSyncManager = registerCodeSync(context, configProvider, cartridgeService, log); }); } + runActivationStep(log, 'AI integration registration', () => { + registerAiIntegration(context, configProvider, () => ({ + available: codeSyncManager !== undefined, + ...(codeSyncManager?.getStatus() ?? {active: false}), + })); + }); + if (settings.get('features.scriptTypes', true)) { runActivationStep(log, 'Script Types registration', () => { registerScriptTypes(context, cartridgeService, log); @@ -1017,30 +947,9 @@ async function activateInner(context: vscode.ExtensionContext, log: vscode.Outpu ); log.appendLine('B2C DX extension activated.'); - // Workspace-only reset: clear stale setup-session keys when the current - // workspace has no dw.json, so a fresh workspace doesn't inherit the - // previous one's onboarding chips/tooltips. - await resetWorkspaceOnboardingIfFresh(context).catch((err) => { - log.appendLine( - `Warning: Failed to reset onboarding for fresh workspace: ${err instanceof Error ? err.message : String(err)}`, - ); - }); - - // Drop the per-workspace onboarding panel state (persona + step records) - // when the workspace has no dw.json, so the deep-dive panel reopens with no - // selection. Cheap to call: workspaceState writes are local. - if (!(await workspaceHasDwJson())) { - await onboardingStore.reset(); - } - - // Show walkthrough on first activation (optional, non-blocking) - // This runs asynchronously after activation is complete. Gated behind the - // onboarding feature flag (Preview, off by default) so it does not surface - // until the walkthrough is ready to ship. - const onboardingEnabled = vscode.workspace.getConfiguration('b2c-dx').get('features.onboarding', false); - if (onboardingEnabled) { - showWalkthroughOnFirstActivation(context).catch((err) => { - log.appendLine(`Warning: Failed to show walkthrough: ${err instanceof Error ? err.message : String(err)}`); + if (setupEnabled) { + await resetSetupSessionIfFresh(context).catch((err) => { + log.appendLine(`Warning: Failed to reset setup session: ${formatErrorMessage(err)}`); }); } } diff --git a/packages/b2c-vs-extension/src/walkthrough/commands.ts b/packages/b2c-vs-extension/src/setup/commands.ts similarity index 96% rename from packages/b2c-vs-extension/src/walkthrough/commands.ts rename to packages/b2c-vs-extension/src/setup/commands.ts index 75d312861..a0272c3b5 100644 --- a/packages/b2c-vs-extension/src/walkthrough/commands.ts +++ b/packages/b2c-vs-extension/src/setup/commands.ts @@ -50,25 +50,8 @@ const DW_JSON_MULTI_INSTANCE_TEMPLATE = { ], }; -/** - * Register walkthrough-related commands. - * These commands support the getting started walkthrough experience. - */ -export function registerWalkthroughCommands(context: vscode.ExtensionContext): void { - // Command: Open the getting started walkthrough. - // The new onboarding panel replaces the built-in walkthrough surface; we - // redirect this legacy command to keep existing menu entries working. - context.subscriptions.push( - vscode.commands.registerCommand('b2c-dx.walkthrough.open', async () => { - try { - await vscode.commands.executeCommand('b2c-dx.onboarding.open'); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - vscode.window.showErrorMessage(`Failed to open walkthrough: ${message}`); - } - }), - ); - +/** Register standalone configuration and credential setup commands. */ +export function registerSetupCommands(context: vscode.ExtensionContext): void { // Command: Create dw.json template file context.subscriptions.push( vscode.commands.registerCommand('b2c-dx.walkthrough.createDwJson', async () => { @@ -1571,57 +1554,14 @@ async function addToGitignore(workspaceRoot: string): Promise { } } -/** - * Defensive per-workspace cleanup of onboarding session state. Runs on every - * activation: if the current workspace has no dw.json, treat it as a fresh - * onboarding context — drop any stale `setup.activeInstance` (which would - * otherwise leak the previous workspace's instance name into chips/tooltips) - * and clear the auto-opened seen flag so the deep-dive panel triggers again. - * - * The OnboardingStateStore itself uses workspaceState and resets naturally - * per workspace; this function only mops up loose keys that aren't covered. - */ -export async function resetWorkspaceOnboardingIfFresh(context: vscode.ExtensionContext): Promise { - const folders = vscode.workspace.workspaceFolders ?? []; - if (folders.length === 0) return; - if (await workspaceHasDwJson()) return; +/** Clear the setup target when the current workspace has no configuration. */ +export async function resetSetupSessionIfFresh(context: vscode.ExtensionContext): Promise { + if (!vscode.workspace.workspaceFolders?.length || (await workspaceHasDwJson())) return; await context.workspaceState.update('b2c-dx.setup.activeInstance', undefined); - await context.workspaceState.update('b2c-dx.gettingStarted.autoOpened', undefined); void vscode.commands.executeCommand('setContext', 'b2c-dx.setupSessionActive', false); void vscode.commands.executeCommand('setContext', 'b2c-dx.setupInstance', undefined); } -/** - * Open the native VS Code walkthrough automatically on first activation, but - * only when no dw.json exists in the workspace — i.e. the user hasn't set the - * extension up yet. Users can re-open it any time via "B2C DX: Open Getting - * Started Guide", and the role-based deep-dive panel via "B2C DX: Open - * Onboarding Panel". - */ -export async function showWalkthroughOnFirstActivation(context: vscode.ExtensionContext): Promise { - const SEEN_KEY = 'b2c-dx.gettingStarted.autoOpened'; - // Per-workspace flag: each workspace gets its own first-run experience. - if (context.workspaceState.get(SEEN_KEY, false)) return; - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) return; - - // Skip auto-open when the workspace already has a dw.json — the user is - // returning, not starting fresh. - if (await workspaceHasDwJson()) { - await context.workspaceState.update(SEEN_KEY, true); - return; - } - - setTimeout(() => { - void vscode.commands.executeCommand( - 'workbench.action.openWalkthrough', - 'Salesforce.b2c-vs-extension#b2c-dx.gettingStarted', - false, - ); - void context.workspaceState.update(SEEN_KEY, true); - }, 1000); -} - // ─── Per-step setup commands + session ────────────────── // // The single-shot wizard above asks everything in one go. Per the docs flow diff --git a/packages/b2c-vs-extension/src/test/ai-context.test.ts b/packages/b2c-vs-extension/src/test/ai-context.test.ts new file mode 100644 index 000000000..654d7f4ec --- /dev/null +++ b/packages/b2c-vs-extension/src/test/ai-context.test.ts @@ -0,0 +1,241 @@ +/* + * 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'; +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import {B2CExtensionConfig} from '../config-provider.js'; +import {IdeContextTool, readIdeContext, type IdeContext} from '../ai/ide-context.js'; +import {B2CMcpServerDefinitionProvider} from '../ai/mcp-provider.js'; +import {CursorMcpRegistration, type CursorMcpApi} from '../ai/cursor-mcp.js'; +import {CodeSyncManager} from '../code-sync/code-sync-manager.js'; + +suite('AI context and MCP registration', () => { + let directory: string; + let configPath: string; + let configProvider: B2CExtensionConfig; + let log: vscode.OutputChannel; + let state: vscode.Memento; + let cancellation: vscode.CancellationTokenSource; + const inactive = () => ({available: false, active: false}); + + setup(async () => { + directory = fs.mkdtempSync(path.join(os.tmpdir(), 'b2c-ai-')); + configPath = path.join(directory, 'dw.json'); + fs.writeFileSync( + configPath, + JSON.stringify({ + configs: [ + { + name: 'development', + hostname: 'development.invalid', + active: true, + 'code-version': 'v1', + password: 'never-return-this', + }, + { + name: 'staging', + hostname: 'staging.invalid', + 'code-version': 'v2', + 'client-secret': 'never-return-this-either', + }, + ], + }), + ); + const values = new Map([['b2c-dx.projectRoot', directory]]); + state = { + keys: () => [...values.keys()], + get: (key: string, fallback?: T) => (values.has(key) ? (values.get(key) as T) : fallback), + update: async (key: string, value: unknown) => { + values.set(key, value); + }, + } as vscode.Memento; + log = vscode.window.createOutputChannel('B2C AI Tests'); + configProvider = new B2CExtensionConfig(log, state, { + B2C_CONFIG_DIR: path.join(directory, 'settings'), + MRT_CREDENTIALS_FILE: path.join(directory, 'missing.mobify'), + }); + cancellation = new vscode.CancellationTokenSource(); + await configProvider.ensureResolved(); + }); + + teardown(() => { + cancellation.dispose(); + configProvider.dispose(); + log.dispose(); + fs.rmSync(directory, {recursive: true, force: true}); + }); + + test('returns exact selection without credentials and waits for an in-progress reset', async () => { + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + const context = await readIdeContext(configProvider, inactive); + assert.strictEqual(context.instanceName, 'staging'); + assert.strictEqual(context.hostname, 'staging.invalid'); + assert.strictEqual(context.codeVersion, 'v2'); + assert.strictEqual(context.configPath, configPath); + assert.strictEqual(context.projectDirectory, directory); + assert.strictEqual(context.selectionMode, 'workspace'); + assert.strictEqual(context.projectRootPinned, true); + assert.ok(!JSON.stringify(context).includes('never-return')); + await configProvider.followDefaultInstance(); + const followed = await readIdeContext(configProvider, inactive); + assert.strictEqual(followed.instanceName, 'development'); + assert.strictEqual(followed.selectionMode, 'default'); + }); + + test('concurrent switches resolve the latest selection, never a stale snapshot', async () => { + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + const firstRead = readIdeContext(configProvider, inactive); + await configProvider.selectInstanceForWorkspace({name: 'development', location: configPath}); + const secondRead = readIdeContext(configProvider, inactive); + for (const result of await Promise.all([firstRead, secondRead])) { + assert.strictEqual(result.instanceName, 'development'); + assert.strictEqual(result.hostname, 'development.invalid'); + } + }); + + test('reports a missing selected instance without falling back to the default', async () => { + await configProvider.selectInstanceForWorkspace({name: 'missing', location: configPath}); + const context = await readIdeContext(configProvider, inactive); + assert.strictEqual(context.status, 'unavailable'); + assert.strictEqual(context.instanceName, 'missing'); + assert.strictEqual(context.hostname, undefined); + }); + + test('reports actual code-sync activity and its retained upload target', async () => { + const cartridge = path.join(directory, 'app_test'); + fs.mkdirSync(path.join(cartridge, 'cartridge'), {recursive: true}); + fs.writeFileSync( + path.join(cartridge, '.project'), + 'app_test', + ); + const manager = new CodeSyncManager(state, configProvider); + try { + assert.deepStrictEqual(manager.getStatus(), {active: false}); + await manager.startWatch(configProvider.getInstance()!, directory); + assert.deepStrictEqual(manager.getStatus(), {active: true, hostname: 'development.invalid', codeVersion: 'v1'}); + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + const context = await readIdeContext(configProvider, () => ({available: true, ...manager.getStatus()})); + assert.strictEqual(context.hostname, 'staging.invalid'); + assert.strictEqual(context.codeSync.hostname, 'development.invalid'); + await manager.stopWatch(); + assert.deepStrictEqual(manager.getStatus(), {active: false}); + } finally { + await manager.stopWatch(); + manager.dispose(); + } + }); + + test('native tool reads on every invocation and supports cancellation', async () => { + const tool = new IdeContextTool(() => readIdeContext(configProvider, inactive)); + const input = {input: {}, toolInvocationToken: undefined}; + const first = await tool.invoke(input, cancellation.token); + assert.ok((first.content[0] as vscode.LanguageModelTextPart).value.includes('development.invalid')); + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + const second = await tool.invoke(input, cancellation.token); + assert.ok((second.content[0] as vscode.LanguageModelTextPart).value.includes('staging.invalid')); + cancellation.cancel(); + await assert.rejects(tool.invoke(input, cancellation.token), vscode.CancellationError); + }); + + test('provider refreshes launch defaults and respects disabled/cancelled discovery', async () => { + let enabled = true; + const provider = new B2CMcpServerDefinitionProvider( + () => readIdeContext(configProvider, inactive), + () => ({enabled, command: 'node', args: ['/test/mcp.js']}), + () => ({dispose() {}}), + '3.0.1', + ); + const [initial] = await provider.provideMcpServerDefinitions(cancellation.token); + assert.strictEqual(initial.cwd?.fsPath, directory); + assert.deepStrictEqual(initial.args, [ + '/test/mcp.js', + '--project-directory', + directory, + '--config', + configPath, + '--instance', + 'development', + ]); + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + const started = await provider.resolveMcpServerDefinition(initial, cancellation.token); + assert.strictEqual(started?.args.at(-1), 'staging'); + assert.deepStrictEqual(started?.env, {}); + await configProvider.selectInstanceForWorkspace({name: 'missing', location: configPath}); + await assert.rejects(provider.resolveMcpServerDefinition(initial, cancellation.token), /unavailable/); + enabled = false; + assert.deepStrictEqual(await provider.provideMcpServerDefinitions(cancellation.token), []); + enabled = true; + cancellation.cancel(); + assert.deepStrictEqual(await provider.provideMcpServerDefinitions(cancellation.token), []); + }); + + test('Cursor registers one server with a live bridge and cleans up on disable', async () => { + const servers = new Map[0]['server']>(); + let registrations = 0; + let enabled = true; + const read = () => readIdeContext(configProvider, inactive); + const provider = new B2CMcpServerDefinitionProvider( + read, + () => ({enabled, command: 'node', args: ['/test/mcp.js']}), + () => ({dispose() {}}), + '3.0.1', + ); + const registration = new CursorMcpRegistration( + { + registerServer({name, server}) { + registrations++; + servers.set(name, server); + }, + unregisterServer(name) { + servers.delete(name); + }, + }, + provider, + read, + ); + try { + await registration.refresh(); + assert.strictEqual(servers.size, 1); + assert.strictEqual(registrations, 1); + await registration.refresh(); + assert.strictEqual(registrations, 1); + await configProvider.selectInstanceForWorkspace({name: 'staging', location: configPath}); + await registration.refresh(); + assert.strictEqual(registrations, 2); + const commerce = servers.get('salesforce-b2c-commerce')!; + assert.ok(commerce.args.includes('staging')); + assert.ok(commerce.args.includes('--ide-context-url')); + const bridgeUrl = commerce.args.at(-1)!; + const headers = {Authorization: `Bearer ${commerce.env.SFCC_IDE_CONTEXT_TOKEN}`}; + assert.ok(!commerce.args.includes(commerce.env.SFCC_IDE_CONTEXT_TOKEN)); + assert.strictEqual((await (await fetch(bridgeUrl, {headers})).json()).instanceName, 'staging'); + enabled = false; + await registration.refresh(); + assert.strictEqual(servers.size, 0); + await assert.rejects(fetch(bridgeUrl, {headers})); + } finally { + await registration.dispose(); + } + }); + + test('provider does not launch from an empty window', async () => { + const context: IdeContext = { + status: 'unconfigured', + selectionMode: 'default', + projectRootPinned: false, + codeSync: inactive(), + }; + const provider = new B2CMcpServerDefinitionProvider( + async () => context, + () => ({enabled: true, command: 'node', args: []}), + () => ({dispose() {}}), + '3.0.1', + ); + assert.deepStrictEqual(await provider.provideMcpServerDefinitions(cancellation.token), []); + }); +}); diff --git a/packages/b2c-vs-extension/src/test/ide-context-bridge.test.ts b/packages/b2c-vs-extension/src/test/ide-context-bridge.test.ts new file mode 100644 index 000000000..a19ffab67 --- /dev/null +++ b/packages/b2c-vs-extension/src/test/ide-context-bridge.test.ts @@ -0,0 +1,46 @@ +/* + * 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 {startIdeContextBridge} from '../ai/ide-context-bridge.js'; + +suite('IDE context bridge', () => { + let bridge: Awaited>; + let instanceName = 'development'; + + setup(async () => { + instanceName = 'development'; + bridge = await startIdeContextBridge(async () => ({ + status: 'ready', + selectionMode: 'workspace', + projectRootPinned: false, + instanceName, + codeSync: {available: true, active: instanceName === 'development'}, + })); + }); + teardown(async () => { + await bridge.dispose(); + }); + + test('returns the live selection and sync state without caching', async () => { + const headers = {Authorization: `Bearer ${bridge.token}`}; + const first = await fetch(bridge.url, {headers}); + assert.strictEqual(first.headers.get('cache-control'), 'no-store'); + assert.strictEqual((await first.json()).codeSync.active, true); + instanceName = 'staging'; + const second = await fetch(bridge.url, {headers}); + const context = await second.json(); + assert.strictEqual(context.instanceName, 'staging'); + assert.strictEqual(context.codeSync.active, false); + }); + + test('rejects unauthenticated, browser-origin, and non-read requests', async () => { + const headers = {Authorization: `Bearer ${bridge.token}`}; + assert.strictEqual((await fetch(bridge.url)).status, 403); + assert.strictEqual((await fetch(bridge.url, {headers: {...headers, Origin: 'https://example.com'}})).status, 403); + assert.strictEqual((await fetch(bridge.url, {method: 'POST', headers, body: '{}'})).status, 405); + assert.strictEqual((await fetch(bridge.url.replace('/context', '/other'), {headers})).status, 404); + }); +}); diff --git a/packages/b2c-vs-extension/src/test/integration/activation.test.ts b/packages/b2c-vs-extension/src/test/integration/activation.test.ts index 9f4111002..74ae568fe 100644 --- a/packages/b2c-vs-extension/src/test/integration/activation.test.ts +++ b/packages/b2c-vs-extension/src/test/integration/activation.test.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import * as path from 'path'; import {fileURLToPath} from 'url'; import * as vscode from 'vscode'; +import {getCursorMcpApi} from '../../ai/cursor-mcp.js'; const EXTENSION_ID = 'Salesforce.b2c-vs-extension'; @@ -58,6 +59,14 @@ suite('extension activation', () => { assert.ok(ext?.isActive, 'extension should be active after suiteSetup activate()'); }); + test('AI integration uses the editor-supported API', () => { + if (vscode.env.appName.toLowerCase().includes('cursor')) { + assert.ok(getCursorMcpApi(), 'Cursor must expose its MCP registration API'); + } else { + assert.ok(vscode.lm.tools.some((tool) => tool.name === 'b2c_get_ide_context')); + } + }); + test('API Browser setup help opens the bundled guide without credentials', async () => { const ext = vscode.extensions.getExtension(EXTENSION_ID)!; const guide = vscode.Uri.joinPath(ext.extensionUri, 'resources', 'api-browser-setup.md'); @@ -111,6 +120,9 @@ suite('extension activation', () => { {prefix: 'b2c-dx.jobs.', feature: 'features.jobsExplorer'}, {prefix: 'b2c-dx.export.', feature: 'features.exportExplorer'}, {prefix: 'b2c-dx.cipAnalytics.', feature: 'features.cipAnalytics'}, + {prefix: 'b2c-dx.setup.', feature: 'features.setup'}, + {prefix: 'b2c-dx.walkthrough.', feature: 'features.setup'}, + {prefix: 'b2c-dx.cli.', feature: 'features.setup'}, ]; const disabledPrefixes = featureGatedPrefixes .filter(({feature}) => !config.get(feature, false)) @@ -133,6 +145,22 @@ suite('extension activation', () => { assert.ok(types.includes('b2c-script'), 'b2c-script debug type must be declared'); }); + test('beta setup commands stay hidden and unregistered by default', async () => { + assert.strictEqual(vscode.workspace.getConfiguration('b2c-dx').get('features.setup'), false); + const registered = new Set(await vscode.commands.getCommands(true)); + const setupCommands = pkg.contributes.commands.filter((entry) => + /b2c-dx\.(setup|walkthrough|cli)\./.test(entry.command), + ); + for (const {command} of setupCommands) { + assert.strictEqual(registered.has(command), false, command); + assert.strictEqual( + pkg.contributes.menus?.commandPalette.find((entry) => entry.command === command)?.when, + 'config.b2c-dx.features.setup', + ); + } + assert.strictEqual(pkg.contributes.walkthroughs, undefined); + }); + // Preview features are gated so they are invisible (no view, no palette // command) unless their b2c-dx.features.* setting is enabled. Assert the // manifest wiring structurally so it can't silently regress. @@ -158,23 +186,12 @@ suite('extension activation', () => { ['b2c-dx.jobs.refresh', 'config.b2c-dx.features.jobsExplorer'], ['b2c-dx.export.run', 'config.b2c-dx.features.exportExplorer'], ['b2c-dx.cipAnalytics.queryBuilder', 'config.b2c-dx.features.cipAnalytics'], - ['b2c-dx.onboarding.open', 'config.b2c-dx.features.onboarding'], ]; for (const [cmd, expected] of cases) { assert.strictEqual(whenFor(cmd), expected, `${cmd} must be palette-gated by ${expected}`); } }); - test('onboarding walkthrough is gated by its feature setting', () => { - const walkthrough = (pkg.contributes.walkthroughs ?? []).find((w) => w.id === 'b2c-dx.gettingStarted'); - assert.ok(walkthrough, 'the b2c-dx.gettingStarted walkthrough must exist'); - assert.strictEqual( - walkthrough!.when, - 'config.b2c-dx.features.onboarding', - 'the onboarding walkthrough must be gated by config.b2c-dx.features.onboarding', - ); - }); - test('every contributed view has an auto-registered focus command', async () => { const registered = new Set(await vscode.commands.getCommands(true)); // A view gated by a `when: config.b2c-dx.features.X` clause is not diff --git a/packages/b2c-vs-extension/src/walkthrough/accessibility.ts b/packages/b2c-vs-extension/src/walkthrough/accessibility.ts deleted file mode 100644 index a9bca60e0..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/accessibility.ts +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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 vscode from 'vscode'; -import * as fs from 'fs/promises'; -import * as path from 'path'; - -/** - * Accessibility validation for walkthrough content. - * Ensures markdown content follows accessibility best practices. - */ - -interface AccessibilityIssue { - file: string; - line?: number; - severity: 'error' | 'warning' | 'info'; - rule: string; - message: string; -} - -/** - * Check walkthrough markdown files for accessibility issues - */ -export async function validateWalkthroughAccessibility(walkthroughDir: string): Promise { - const issues: AccessibilityIssue[] = []; - - try { - const files = await fs.readdir(walkthroughDir); - const mdFiles = files.filter((f) => f.endsWith('.md')); - - for (const file of mdFiles) { - const filePath = path.join(walkthroughDir, file); - const content = await fs.readFile(filePath, 'utf-8'); - const fileIssues = checkMarkdownAccessibility(file, content); - issues.push(...fileIssues); - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - issues.push({ - file: walkthroughDir, - severity: 'error', - rule: 'validation-error', - message: `Failed to validate directory: ${message}`, - }); - } - - return issues; -} - -/** - * Check individual markdown file for accessibility issues - */ -function checkMarkdownAccessibility(filename: string, content: string): AccessibilityIssue[] { - const issues: AccessibilityIssue[] = []; - const lines = content.split('\n'); - - // Check 1: Images should have alt text - lines.forEach((line, index) => { - const imageRegex = /!\[(.*?)\]\((.*?)\)/g; - let match; - - while ((match = imageRegex.exec(line)) !== null) { - const altText = match[1]; - if (!altText || altText.trim().length === 0) { - issues.push({ - file: filename, - line: index + 1, - severity: 'error', - rule: 'image-alt-text', - message: 'Image must have descriptive alt text', - }); - } else if (altText.length < 10) { - issues.push({ - file: filename, - line: index + 1, - severity: 'warning', - rule: 'image-alt-text-short', - message: 'Alt text should be more descriptive (at least 10 characters)', - }); - } - } - }); - - // Check 2: Links should have descriptive text - lines.forEach((line, index) => { - const linkRegex = /\[(.*?)\]\((.*?)\)/g; - let match; - - while ((match = linkRegex.exec(line)) !== null) { - const linkText = match[1]; - const nonDescriptive = ['click here', 'here', 'link', 'read more']; - - if (nonDescriptive.some((phrase) => linkText.toLowerCase().includes(phrase))) { - issues.push({ - file: filename, - line: index + 1, - severity: 'warning', - rule: 'link-descriptive-text', - message: `Link text "${linkText}" is not descriptive. Use text that describes the destination.`, - }); - } - - if (linkText.trim().length === 0) { - issues.push({ - file: filename, - line: index + 1, - severity: 'error', - rule: 'link-empty-text', - message: 'Link must have text content', - }); - } - } - }); - - // Check 3: Headings should follow hierarchy - const headingLevels: number[] = []; - lines.forEach((line, index) => { - const headingMatch = line.match(/^(#{1,6})\s/); - if (headingMatch) { - const level = headingMatch[1].length; - headingLevels.push(level); - - // Check if heading skips levels - if (headingLevels.length > 1) { - const prevLevel = headingLevels[headingLevels.length - 2]; - if (level > prevLevel + 1) { - issues.push({ - file: filename, - line: index + 1, - severity: 'warning', - rule: 'heading-hierarchy', - message: `Heading level ${level} skips level ${prevLevel + 1}. Maintain heading hierarchy.`, - }); - } - } - } - }); - - // Check 4: Code blocks should have language specified - lines.forEach((line, index) => { - if (line.trim().startsWith('```') && line.trim() === '```') { - issues.push({ - file: filename, - line: index + 1, - severity: 'info', - rule: 'code-block-language', - message: 'Code block should specify language for syntax highlighting', - }); - } - }); - - // Check 5: Color-only information - const colorKeywords = ['red', 'green', 'blue', 'yellow', 'color']; - lines.forEach((line, index) => { - colorKeywords.forEach((keyword) => { - if ( - line.toLowerCase().includes(keyword) && - !line.includes('$(') && // Exclude icon references - !line.includes('```') - ) { - // Exclude code blocks - issues.push({ - file: filename, - line: index + 1, - severity: 'info', - rule: 'color-only-information', - message: `Line mentions "${keyword}". Ensure information is not conveyed by color alone.`, - }); - } - }); - }); - - // Check 6: Emoji usage - const emojiRegex = - /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; - lines.forEach((line, index) => { - if (emojiRegex.test(line)) { - // Emojis are generally okay in headings and list items for visual interest - // but should not be the only way to convey information - const emojiCount = (line.match(new RegExp(emojiRegex, 'gu')) || []).length; - if (emojiCount > 3) { - issues.push({ - file: filename, - line: index + 1, - severity: 'info', - rule: 'excessive-emoji', - message: 'Line contains many emoji. Consider if they add value or just clutter.', - }); - } - } - }); - - // Check 7: Table accessibility - lines.forEach((line, index) => { - if (line.includes('|') && line.trim().startsWith('|')) { - // This is likely a table row - const nextLine = lines[index + 1]; - if (nextLine && nextLine.includes('---')) { - // This is a table header row, which is good - } else if (!lines.slice(Math.max(0, index - 2), index).some((l) => l.includes('---'))) { - issues.push({ - file: filename, - line: index + 1, - severity: 'info', - rule: 'table-headers', - message: 'Tables should have header rows for accessibility', - }); - } - } - }); - - return issues; -} - -/** - * Format accessibility issues for display - */ -export function formatAccessibilityReport(issues: AccessibilityIssue[]): string { - if (issues.length === 0) { - return '✅ No accessibility issues found!'; - } - - const lines: string[] = ['=== Walkthrough Accessibility Report ===', `Found ${issues.length} issue(s)`, '']; - - const errorCount = issues.filter((i) => i.severity === 'error').length; - const warningCount = issues.filter((i) => i.severity === 'warning').length; - const infoCount = issues.filter((i) => i.severity === 'info').length; - - lines.push(`Errors: ${errorCount}`); - lines.push(`Warnings: ${warningCount}`); - lines.push(`Info: ${infoCount}`); - lines.push(''); - - // Group by file - const byFile = new Map(); - for (const issue of issues) { - const fileIssues = byFile.get(issue.file) || []; - fileIssues.push(issue); - byFile.set(issue.file, fileIssues); - } - - for (const [file, fileIssues] of byFile) { - lines.push(`File: ${file}`); - for (const issue of fileIssues) { - const severityIcon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️'; - const location = issue.line ? ` Line ${issue.line}` : ''; - lines.push(` ${severityIcon} [${issue.rule}]${location}: ${issue.message}`); - } - lines.push(''); - } - - return lines.join('\n'); -} - -/** - * VS Code command to check walkthrough accessibility - */ -export async function checkWalkthroughAccessibilityCommand( - extensionPath: string, - log: vscode.OutputChannel, -): Promise { - const walkthroughDir = path.join(extensionPath, 'media', 'walkthrough'); - - log.appendLine('Running accessibility validation...'); - - const issues = await validateWalkthroughAccessibility(walkthroughDir); - const report = formatAccessibilityReport(issues); - - log.appendLine(report); - log.show(); - - if (issues.length === 0) { - vscode.window.showInformationMessage('✅ No accessibility issues found in walkthrough!'); - } else { - const errorCount = issues.filter((i) => i.severity === 'error').length; - if (errorCount > 0) { - vscode.window.showErrorMessage(`Found ${errorCount} accessibility error(s). Check Output > B2C DX for details.`); - } else { - vscode.window.showWarningMessage( - `Found ${issues.length} accessibility issue(s). Check Output > B2C DX for details.`, - ); - } - } -} diff --git a/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts b/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts deleted file mode 100644 index 6e3b81e82..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/aiSkillsContent.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* - * 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 fs from 'fs/promises'; -import * as os from 'os'; -import * as path from 'path'; -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). - */ - -export type IdeStatus = 'not-installed' | 'ide-present' | 'skills-installed'; - -export interface AiSkillsTarget { - /** CLI flag value for `b2c setup skills --ide `. */ - id: string; - label: string; - description: string; - /** Filesystem path used to detect IDE presence. */ - detectPath: string; - /** Per-IDE skills install dir for `--global` installs (absolute). */ - globalSkillsDir: string; - /** Per-IDE skills install dir for project-scoped installs (relative to workspace root). */ - projectSkillsDir: string; - /** Marketplace plugin command, if applicable. */ - marketplaceCommand?: string; - /** MCP install one-liner / snippet. */ - mcpCommand?: string; -} - -const home = os.homedir(); - -/** Only IDEs the b2c CLI's `setup skills` command actually supports. */ -export const AI_SKILL_TARGETS: AiSkillsTarget[] = [ - { - id: 'claude-code', - label: 'Claude Code', - description: 'Anthropic CLI agent. Marketplace plugin recommended for auto-updates.', - detectPath: path.join(home, '.claude'), - globalSkillsDir: path.join(home, '.claude', 'skills'), - projectSkillsDir: path.join('.claude', 'skills'), - marketplaceCommand: - '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', - }, - { - id: 'cursor', - label: 'Cursor', - description: 'Cursor IDE. Skills install via b2c CLI; MCP via .cursor/mcp.json.', - detectPath: path.join(home, '.cursor'), - globalSkillsDir: path.join(home, '.cursor', 'skills'), - projectSkillsDir: path.join('.cursor', 'skills'), - mcpCommand: - 'mkdir -p .cursor && printf \'%s\' \'{"mcpServers":{"b2c-dx-mcp":{"command":"npx","args":["-y","@salesforce/b2c-dx-mcp@latest"]}}}\' > .cursor/mcp.json', - }, - { - id: 'windsurf', - label: 'Windsurf', - description: 'Codeium Windsurf editor.', - detectPath: path.join(home, '.codeium', 'windsurf'), - globalSkillsDir: path.join(home, '.codeium', 'windsurf', 'skills'), - projectSkillsDir: path.join('.windsurf', 'skills'), - }, - { - id: 'vscode', - label: 'VS Code / GitHub Copilot', - description: 'Copilot Chat in VS Code. MCP via .vscode/mcp.json.', - detectPath: path.join(home, '.copilot'), - 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"]}}}\' > .vscode/mcp.json', - }, - { - id: 'codex', - label: 'OpenAI Codex CLI', - description: 'Codex CLI agent. Marketplace plugin available.', - detectPath: path.join(home, '.codex'), - globalSkillsDir: path.join(home, '.codex', 'skills'), - projectSkillsDir: path.join('.codex', 'skills'), - marketplaceCommand: 'codex plugin marketplace add SalesforceCommerceCloud/b2c-developer-tooling', - }, - { - id: 'opencode', - label: 'OpenCode', - description: 'OpenCode agentic editor.', - detectPath: path.join(home, '.config', 'opencode'), - globalSkillsDir: path.join(home, '.config', 'opencode', 'skills'), - projectSkillsDir: path.join('.opencode', 'skills'), - }, - { - id: 'agentforce-vibes', - label: 'Agentforce Vibes', - description: 'Salesforce Agentforce Vibes (VS Code extension).', - detectPath: getAgentforceVibesProbePath(), - globalSkillsDir: getAgentforceVibesGlobalDir(), - projectSkillsDir: path.join('.a4drules', 'skills'), - }, -]; - -function getAgentforceVibesGlobalDir(): string { - if (process.platform === 'darwin') { - return path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage'); - } else if (process.platform === 'win32') { - return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Code', 'User', 'globalStorage'); - } - return path.join(home, '.config', 'Code', 'User', 'globalStorage'); -} - -function getAgentforceVibesProbePath(): string { - return path.join(getAgentforceVibesGlobalDir(), 'salesforce.salesforcedx-einstein-gpt'); -} - -async function pathExists(p: string): Promise { - try { - await fs.access(p); - return true; - } catch { - return false; - } -} - -async function dirHasB2cSkills(dir: string): Promise { - try { - const entries = await fs.readdir(dir); - return entries.some((e) => e.toLowerCase().startsWith('b2c')); - } catch { - return false; - } -} - -/** - * Returns the install state for a single target. - * - `not-installed` — no IDE installed - * - `ide-present` — IDE is on disk but no B2C skills found - * - `skills-installed` — at least one b2c-* skill is already in the IDE's skills dir - * - * Checks both the global skills directory AND every workspace folder's - * project-scoped skills dir, since `b2c setup skills` defaults to project - * scope unless `--global` is passed. - */ -export async function detectIdeStatus(target: AiSkillsTarget, workspaceRoots: string[] = []): Promise { - const idePresent = await pathExists(target.detectPath); - if (!idePresent) return 'not-installed'; - - // Project-scoped checks (one per workspace folder). - for (const root of workspaceRoots) { - const dir = path.join(root, target.projectSkillsDir); - if (await dirHasB2cSkills(dir)) return 'skills-installed'; - } - - // Global / user-home check. - if (await dirHasB2cSkills(target.globalSkillsDir)) return 'skills-installed'; - - return 'ide-present'; -} - -export interface DetectedTarget extends AiSkillsTarget { - status: IdeStatus; -} - -/** - * Detect status for every target in parallel. Pulls workspace roots from - * VS Code so project-scoped skill 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)}))); -} - -const escape = (s: string): string => - s.replace(/[&<>"']/g, (c) => - c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''', - ); - -function statusPill(status: IdeStatus): string { - switch (status) { - case 'skills-installed': - return `Ready · Skills installed`; - case 'ide-present': - return `IDE detected`; - case 'not-installed': - return `Not installed`; - } -} - -/** Per-IDE icon SVG. Stylised glyphs only — no logos to avoid trademark issues. */ -function ideIcon(id: string): string { - switch (id) { - case 'claude-code': - // Star-burst (Anthropic-ish accent) - return ``; - case 'cursor': - // Cursor / pointer arrow - return ``; - case 'windsurf': - // Wind/wave lines - return ``; - case 'vscode': - // Chat bubble (Copilot) - return ``; - case 'codex': - // Code brackets - return ``; - case 'opencode': - // Open hexagon - return ``; - case 'agentforce-vibes': - // Spark with cloud - return ``; - default: - return ``; - } -} - -export function generateAiSkillsHtml(targets: DetectedTarget[]): string { - // Render detected IDEs first, then "ready" ones, then "not installed". - const sorted = [...targets].sort((a, b) => { - const order: Record = {'skills-installed': 0, 'ide-present': 1, 'not-installed': 2}; - return order[a.status] - order[b.status]; - }); - - const cards = sorted - .map((t) => { - const isInstalled = t.status === 'skills-installed'; - const isReady = t.status === 'ide-present'; - const isMissing = t.status === 'not-installed'; - - const skillsLabel = isInstalled ? 'Reinstall' : isReady ? 'Install Skills' : 'Install Skills'; - const skillsDisabled = isMissing ? 'disabled' : ''; - const skillsClass = isReady ? 'ai-btn ai-btn--primary' : isInstalled ? 'ai-btn ai-btn--ghost' : 'ai-btn'; - - const secondaryActions: string[] = []; - if (t.marketplaceCommand) { - secondaryActions.push( - ``, - ); - } - if (t.mcpCommand) { - secondaryActions.push( - ``, - ); - } - - return ` -
-
-
${ideIcon(t.id)}
-
-

${escape(t.label)}

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

${escape(t.description)}

-
- - ${secondaryActions.length ? `
${secondaryActions.join('')}
` : ''} -
-
`; - }) - .join(''); - - const installedCount = targets.filter((t) => t.status !== 'not-installed').length; - const skillsInstalledCount = targets.filter((t) => t.status === 'skills-installed').length; - - return ` -
-

Configure once and your AI tools share the same instance, dw.json, and cartridge layout this extension already understands.

- -
-
- ${targets.length}compatible - · - ${installedCount}detected - · - ${skillsInstalledCount}skills installed -
- -
- -
${cards}
- -

One-click install. Click Install Skills on a detected IDE and a terminal opens with b2c setup skills b2c --ide <ide> queued. Press Enter to run; the CLI handles paths, downloads, and overwrites.

- -

What gets installed

-
    -
  • Agent Skills — B2C-specific instructions, prompts, and conventions your AI tool can reference.
  • -
  • MCP server — exposes B2C-specific tools (deploy, log queries, sandbox info) to any MCP-aware client.
  • -
- -

Agent Skills documentation · MCP server documentation

-
`; -} diff --git a/packages/b2c-vs-extension/src/walkthrough/index.ts b/packages/b2c-vs-extension/src/walkthrough/index.ts deleted file mode 100644 index b455ac031..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 - */ - -export { - registerWalkthroughCommands, - resetWorkspaceOnboardingIfFresh, - showWalkthroughOnFirstActivation, -} from './commands.js'; -export {initializeTelemetry, getTelemetry} from './telemetry.js'; -export { - validateWalkthroughAccessibility, - formatAccessibilityReport, - checkWalkthroughAccessibilityCommand, -} from './accessibility.js'; -export {validateWalkthroughConfiguration, formatValidationResult, validateWalkthroughCommand} from './validator.js'; -export {OnboardingStateStore} from './state.js'; -export {OnboardingPanel} from './onboardingPanel.js'; -export {PERSONAS, listPersonas, resolveSteps, STEP_CATALOG} from './personas.js'; -export type {PersonaId, PersonaDefinition, StepDefinition, StepAction} from './personas.js'; -export {detectTools, generateInstallCliHtml} from './toolDetection.js'; -export type {ToolDetectionResult, ToolStatus} from './toolDetection.js'; -export {detectStepConfigurations, getDetectionForStep} from './stepDetection.js'; -export type {DetectionSummary, StepDetection} from './stepDetection.js'; diff --git a/packages/b2c-vs-extension/src/walkthrough/markdown.ts b/packages/b2c-vs-extension/src/walkthrough/markdown.ts deleted file mode 100644 index f75259ca7..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/markdown.ts +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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 - */ - -/** - * Minimal, trust-the-source markdown renderer for our own walkthrough content. - * Handles the subset used by media/walkthrough/*.md: headings, paragraphs, lists - * (ordered + unordered, including nested), fenced code, inline code, emphasis, - * bold, links, and horizontal rules. Escapes all raw HTML — we never embed - * user-authored content here, but defense in depth. - * - * We avoid adding `marked` / `markdown-it` to the extension bundle for a - * ~30KB saving; the rule set below covers every construct present in the - * existing nine walkthrough pages. - */ - -const HTML_ESCAPE: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; - -function escapeHtml(text: string): string { - return text.replace(/[&<>"']/g, (c) => HTML_ESCAPE[c] ?? c); -} - -function renderInline(text: string): string { - let out = escapeHtml(text); - // Inline code: `foo` - out = out.replace(/`([^`]+)`/g, (_, code) => `${code}`); - // Links: [label](url) - out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => { - const safeUrl = /^(https?:|mailto:|command:|#)/i.test(url) ? url : '#'; - return `${label}`; - }); - // Bold: **foo** or __foo__ - out = out.replace(/\*\*([^*]+)\*\*/g, '$1'); - out = out.replace(/__([^_]+)__/g, '$1'); - // Emphasis: *foo* or _foo_ - out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2'); - out = out.replace(/(^|[^_])_([^_\n]+)_/g, '$1$2'); - return out; -} - -interface ListState { - ordered: boolean; - indent: number; - items: string[]; -} - -export function renderMarkdown(input: string): string { - const lines = input.replace(/\r\n/g, '\n').split('\n'); - const out: string[] = []; - const listStack: ListState[] = []; - let paragraph: string[] = []; - let inCodeBlock = false; - let codeLang = ''; - let codeBuffer: string[] = []; - - const flushParagraph = () => { - if (paragraph.length === 0) return; - out.push(`

${renderInline(paragraph.join(' '))}

`); - paragraph = []; - }; - - const closeListsTo = (indent: number) => { - while (listStack.length > 0 && listStack[listStack.length - 1].indent >= indent) { - const list = listStack.pop()!; - const tag = list.ordered ? 'ol' : 'ul'; - out.push(`<${tag}>${list.items.map((i) => `
  • ${i}
  • `).join('')}`); - } - }; - - for (const rawLine of lines) { - const line = rawLine.replace(/\t/g, ' '); - - // Fenced code blocks. When consecutive code blocks are separated only - // by blank lines (the renderer's blank-line rule fires between them), - // we merge them into a single
     with newline separators so they
    -    // render as a tight stanza — no per-block padding stacking.
    -    const fence = line.match(/^```(\w*)\s*$/);
    -    if (fence) {
    -      if (inCodeBlock) {
    -        const html = `
    ${escapeHtml(
    -          codeBuffer.join('\n'),
    -        )}
    `; - // If the previous emission was a
     (i.e. nothing in between but
    -        // whitespace-driven flushParagraph/closeListsTo no-ops), merge the
    -        // two into one block by stripping the closing tag of the previous
    -        // and the opening tag of the new one.
    -        const last = out[out.length - 1];
    -        if (last && last.startsWith('
    ') && last.endsWith('
    ')) { - // Reuse the previous
    's opening; concatenate inner content
    -          // separated by a blank-line so commands stay readable.
    -          const merged =
    -            last.slice(0, last.length - '
    '.length) + - '\n' + - escapeHtml(codeBuffer.join('\n')) + - '
    '; - out[out.length - 1] = merged; - } else { - out.push(html); - } - inCodeBlock = false; - codeBuffer = []; - codeLang = ''; - } else { - flushParagraph(); - closeListsTo(0); - inCodeBlock = true; - codeLang = fence[1] ?? ''; - } - continue; - } - if (inCodeBlock) { - codeBuffer.push(line); - continue; - } - - // Blank line ends paragraph and any open lists at deeper indents than 0 - if (/^\s*$/.test(line)) { - flushParagraph(); - closeListsTo(0); - continue; - } - - // Horizontal rule - if (/^\s*(---|\*\*\*|___)\s*$/.test(line)) { - flushParagraph(); - closeListsTo(0); - out.push('
    '); - continue; - } - - // Heading - const heading = line.match(/^(#{1,6})\s+(.*)$/); - if (heading) { - flushParagraph(); - closeListsTo(0); - const level = heading[1].length; - out.push(`${renderInline(heading[2].trim())}`); - continue; - } - - // List item (unordered or ordered) - const listItem = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/); - if (listItem) { - flushParagraph(); - const indent = listItem[1].length; - const ordered = /^\d+\./.test(listItem[2]); - const content = renderInline(listItem[3]); - - // Close lists deeper than current indent - while (listStack.length > 0 && listStack[listStack.length - 1].indent > indent) { - const list = listStack.pop()!; - const tag = list.ordered ? 'ol' : 'ul'; - const html = `<${tag}>${list.items.map((i) => `
  • ${i}
  • `).join('')}`; - const parent = listStack[listStack.length - 1]; - if (parent) { - parent.items[parent.items.length - 1] += html; - } else { - out.push(html); - } - } - - const top = listStack[listStack.length - 1]; - if (top && top.indent === indent && top.ordered === ordered) { - top.items.push(content); - } else { - if (top && top.indent === indent && top.ordered !== ordered) { - // Same indent, different type — close the old one. - const list = listStack.pop()!; - const tag = list.ordered ? 'ol' : 'ul'; - const html = `<${tag}>${list.items.map((i) => `
  • ${i}
  • `).join('')}`; - const parent = listStack[listStack.length - 1]; - if (parent) parent.items[parent.items.length - 1] += html; - else out.push(html); - } - listStack.push({ordered, indent, items: [content]}); - } - continue; - } - - // Paragraph line - closeListsTo(0); - paragraph.push(line.trim()); - } - - flushParagraph(); - closeListsTo(0); - if (inCodeBlock) { - out.push(`
    ${escapeHtml(codeBuffer.join('\n'))}
    `); - } - return out.join('\n'); -} diff --git a/packages/b2c-vs-extension/src/walkthrough/onboardingPanel.ts b/packages/b2c-vs-extension/src/walkthrough/onboardingPanel.ts deleted file mode 100644 index fd3fab62f..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/onboardingPanel.ts +++ /dev/null @@ -1,2503 +0,0 @@ -/* - * 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 vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs/promises'; - -import {OnboardingStateStore, StepStatus} from './state.js'; -import {PERSONAS, PersonaId, StepAction, StepDefinition, getPersona, listPersonas, resolveSteps} from './personas.js'; -import {renderMarkdown} from './markdown.js'; -import {detectTools, generateInstallCliHtml, ToolDetectionResult} from './toolDetection.js'; -import {detectAllTargets, generateAiSkillsHtml} from './aiSkillsContent.js'; -import { - detectStepConfigurations, - getDetectionForStep, - readDeployContext, - DetectionSummary, - StepDetection, -} from './stepDetection.js'; -import {createScriptsBackend, type CodeVersionInfo} from '@salesforce/b2c-tooling-sdk/operations/code'; -import type {B2CExtensionConfig} from '../config-provider.js'; -import {findCartridgesSafe} from '../workspace-discovery.js'; - -type InboundMessage = - | {type: 'selectPersona'; personaId: PersonaId} - | {type: 'changePersona'} - | {type: 'openStep'; stepId: string} - | {type: 'completeStep'; stepId: string} - | {type: 'skipStep'; stepId: string} - | {type: 'goNext'} - | {type: 'goPrev'} - | {type: 'runAction'; command: string; args?: unknown[]; stepId?: string} - | {type: 'openLink'; url: string} - | {type: 'reset'} - | {type: 'ready'} - | {type: 'aiSkills.installSkills'; ide: string} - | {type: 'aiSkills.runCommand'; cmd: string; label: string} - | {type: 'aiSkills.recheck'}; - -interface PersonaView { - id: PersonaId; - label: string; - tagline: string; - description: string; - stepCount: number; - estimatedMinutes: number; - recommended?: boolean; -} - -interface StepView { - id: string; - title: string; - summary: string; - status: StepStatus; - actions: StepAction[]; - html: string; - /** Optional per-step detection chip ("1 configuration detected"). */ - detection?: {label: string; matchedNames: string[]} | null; -} - -interface ViewState { - persona: PersonaView | null; - personas: PersonaView[]; - steps: StepView[]; - activeStepId: string | null; - setupInstance: string | null; -} - -type DeployedCartridgesResult = - | {kind: 'ok'; names: string[]; source: 'api' | 'webdav'} - | {kind: 'no-provider'} - | {kind: 'no-instance'; reason?: string} - | {kind: 'no-code-version'} - | {kind: 'error'; reason: string}; - -export class OnboardingPanel { - private static current: OnboardingPanel | undefined; - - static show( - context: vscode.ExtensionContext, - store: OnboardingStateStore, - log: vscode.OutputChannel, - getConfigProvider?: () => B2CExtensionConfig | null, - ): void { - if (OnboardingPanel.current) { - OnboardingPanel.current.panel.reveal(); - return; - } - const panel = vscode.window.createWebviewPanel('b2c-dx.onboarding', 'B2C DX: Get Started', vscode.ViewColumn.One, { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [vscode.Uri.file(context.extensionPath)], - }); - OnboardingPanel.current = new OnboardingPanel(context, store, log, panel, getConfigProvider); - } - - private readonly disposables: vscode.Disposable[] = []; - private activeStepId: string | null = null; - - private constructor( - private readonly context: vscode.ExtensionContext, - private readonly store: OnboardingStateStore, - private readonly log: vscode.OutputChannel, - private readonly panel: vscode.WebviewPanel, - private readonly getConfigProvider?: () => B2CExtensionConfig | null, - ) { - this.panel.webview.html = this.renderShell(); - this.disposables.push( - this.panel.onDidDispose(() => this.dispose()), - this.panel.webview.onDidReceiveMessage((msg) => this.handleMessage(msg as InboundMessage)), - this.store.onDidChange(() => void this.refresh()), - // Re-check whenever the user returns to the panel — covers the case where - // they ran an install in the terminal and switched back. - this.panel.onDidChangeViewState((e) => { - if (e.webviewPanel.active) { - if (this.activeStepId === 'ai-skills') { - this.aiSkillsCache = null; - } - void this.refresh(); - } - }), - ); - } - - /** Cached AI-skills detection result, invalidated on install actions. */ - private aiSkillsCache: import('./aiSkillsContent.js').DetectedTarget[] | null = null; - /** Tracks the in-flight watcher so multiple installs don't stack. */ - private aiSkillsWatcher: NodeJS.Timeout | null = null; - /** Disposables for terminal-shell-execution and close listeners on the in-flight install. */ - private aiSkillsTermDisposables: vscode.Disposable[] = []; - - /** - * Subscribes to terminal events for the install terminal so we can - * deterministically refresh as soon as the install command exits — much - * more responsive than polling. - */ - private watchTerminalForAiSkills(terminal: vscode.Terminal): void { - // Clear any previous subscriptions; only one in-flight install at a time. - this.aiSkillsTermDisposables.forEach((d) => d.dispose()); - this.aiSkillsTermDisposables = []; - - // Stronger signal: shell-execution end (proposed but stable in 1.93+). - // Falls back silently on older runtimes via try/catch. - try { - const api = vscode.window as unknown as { - onDidEndTerminalShellExecution?: (listener: (e: {terminal: vscode.Terminal}) => void) => vscode.Disposable; - }; - if (typeof api.onDidEndTerminalShellExecution === 'function') { - const sub = api.onDidEndTerminalShellExecution((e) => { - if (e.terminal === terminal) { - this.aiSkillsCache = null; - void this.refresh(); - } - }); - this.aiSkillsTermDisposables.push(sub); - } - } catch { - // ignore - } - - // Fallback: when the user closes the terminal, refresh. - const closeSub = vscode.window.onDidCloseTerminal((closed) => { - if (closed === terminal) { - this.aiSkillsCache = null; - void this.refresh(); - this.aiSkillsTermDisposables.forEach((d) => d.dispose()); - this.aiSkillsTermDisposables = []; - } - }); - this.aiSkillsTermDisposables.push(closeSub); - } - - /** - * After kicking off an install in the terminal we don't get a deterministic - * completion signal, so we poll the filesystem every 2s for up to ~60s. - * As soon as detection produces a different snapshot we refresh and stop. - */ - private startAiSkillsWatcher(): void { - if (this.aiSkillsWatcher) clearInterval(this.aiSkillsWatcher); - const before = JSON.stringify(this.aiSkillsCache?.map((t) => ({id: t.id, status: t.status})) ?? []); - let elapsed = 0; - const TICK_MS = 2000; - const MAX_MS = 60000; - this.aiSkillsWatcher = setInterval(async () => { - elapsed += TICK_MS; - try { - const {detectAllTargets} = await import('./aiSkillsContent.js'); - const fresh = await detectAllTargets(); - const snapshot = JSON.stringify(fresh.map((t) => ({id: t.id, status: t.status}))); - if (snapshot !== before) { - this.aiSkillsCache = fresh; - if (this.aiSkillsWatcher) { - clearInterval(this.aiSkillsWatcher); - this.aiSkillsWatcher = null; - } - await this.refresh(); - return; - } - } catch { - // best-effort — keep polling - } - if (elapsed >= MAX_MS && this.aiSkillsWatcher) { - clearInterval(this.aiSkillsWatcher); - this.aiSkillsWatcher = null; - } - }, TICK_MS); - } - - private async handleMessage(msg: InboundMessage): Promise { - try { - switch (msg.type) { - case 'ready': - await this.refresh(); - return; - case 'selectPersona': - await this.store.setPersona(msg.personaId); - this.activeStepId = PERSONAS[msg.personaId]?.stepIds[0] ?? null; - await this.refresh(); - return; - case 'changePersona': - await this.store.setPersona(null); - this.activeStepId = null; - await this.refresh(); - return; - case 'openStep': { - const persona = this.store.getPersona(); - if (!persona) return; - // Ignore clicks on locked steps: the user must complete predecessors. - const view = await this.buildViewState(); - const target = view.steps.find((s) => s.id === msg.stepId); - if (!target || target.status === 'locked') return; - this.activeStepId = msg.stepId; - await this.store.markStarted(persona, msg.stepId); - await this.refresh(); - return; - } - case 'completeStep': { - const persona = this.store.getPersona(); - if (!persona) return; - await this.store.markCompleted(persona, msg.stepId); - return; - } - case 'skipStep': { - const persona = this.store.getPersona(); - if (!persona) return; - await this.store.markSkipped(persona, msg.stepId); - return; - } - case 'runAction': { - const persona = this.store.getPersona(); - if (persona && msg.stepId) { - await this.store.markStarted(persona, msg.stepId); - } - await vscode.commands.executeCommand(msg.command, ...(msg.args ?? [])); - // Setup commands mutate workspaceState; ensure the chip + Start Over - // button re-render once the action returns. - if (typeof msg.command === 'string' && msg.command.startsWith('b2c-dx.setup.')) { - await this.refresh(); - } - // CLI actions (install, recheck) should re-detect tools and refresh. - if (typeof msg.command === 'string' && msg.command.startsWith('b2c-dx.cli.')) { - this.invalidateToolDetection(); - await this.refresh(); - } - return; - } - case 'openLink': { - // All markdown link clicks route through here. Safely dispatch - // command: URIs and open http(s) externally; ignore anything else. - const url = msg.url; - if (url.startsWith('command:')) { - const rest = url.slice('command:'.length); - const qIdx = rest.indexOf('?'); - const commandId = qIdx >= 0 ? rest.slice(0, qIdx) : rest; - let args: unknown[] = []; - if (qIdx >= 0) { - try { - const parsed = JSON.parse(decodeURIComponent(rest.slice(qIdx + 1))); - args = Array.isArray(parsed) ? parsed : [parsed]; - } catch { - args = []; - } - } - await vscode.commands.executeCommand(commandId, ...args); - } else if (/^https?:/i.test(url) || url.startsWith('mailto:')) { - await vscode.env.openExternal(vscode.Uri.parse(url)); - } - return; - } - case 'goNext': { - const persona = this.store.getPersona(); - if (!persona) return; - const steps = resolveSteps(persona as PersonaId); - const currentIdx = steps.findIndex((s) => s.id === this.activeStepId); - if (currentIdx >= 0) { - await this.store.markCompleted(persona, steps[currentIdx].id); - } - const next = steps[currentIdx + 1]; - if (next) { - this.activeStepId = next.id; - await this.store.markStarted(persona, next.id); - } - await this.refresh(); - return; - } - case 'goPrev': { - const persona = this.store.getPersona(); - if (!persona) return; - const steps = resolveSteps(persona as PersonaId); - const currentIdx = steps.findIndex((s) => s.id === this.activeStepId); - const prev = currentIdx > 0 ? steps[currentIdx - 1] : null; - if (prev) { - this.activeStepId = prev.id; - await this.refresh(); - } - return; - } - case 'reset': - await this.store.reset(); - this.activeStepId = null; - await this.refresh(); - return; - case 'aiSkills.installSkills': { - // Open a terminal preloaded with `b2c setup skills b2c --ide `. - // We don't auto-run — the user reviews it and presses Enter. - const ide = msg.ide.replace(/[^a-z0-9-]/gi, ''); - if (!ide) return; - const term = vscode.window.createTerminal({name: `B2C DX — Skills (${ide})`}); - term.show(); - term.sendText(`b2c setup skills b2c --ide ${ide}`, false); - this.watchTerminalForAiSkills(term); - this.startAiSkillsWatcher(); - return; - } - case 'aiSkills.runCommand': { - const safeLabel = msg.label.replace(/[^\w\s—()-]/g, '').slice(0, 40) || 'Install'; - const term = vscode.window.createTerminal({name: `B2C DX — ${safeLabel}`}); - term.show(); - term.sendText(msg.cmd, false); - this.watchTerminalForAiSkills(term); - this.startAiSkillsWatcher(); - return; - } - case 'aiSkills.recheck': { - this.aiSkillsCache = null; - await this.refresh(); - return; - } - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - this.log.appendLine(`[onboarding] handleMessage(${msg.type}) failed: ${message}`); - vscode.window.showErrorMessage(`Onboarding: ${message}`); - } - } - - private async refresh(): Promise { - const view = await this.buildViewState(); - if (view.persona) { - const active = view.steps.find((s) => s.id === this.activeStepId); - // If no active step, or the active one is now locked (shouldn't happen - // but defend against it), pick the first step the user can work on. - if (!active || active.status === 'locked') { - const pick = - view.steps.find((s) => s.status === 'in-progress') ?? - view.steps.find((s) => s.status === 'available') ?? - view.steps[0]; - this.activeStepId = pick?.id ?? null; - } - } - this.panel.webview.postMessage({type: 'state', state: {...view, activeStepId: this.activeStepId}}); - } - - private async buildViewState(): Promise { - // Per-persona time estimates (minutes). Tuned from real walkthrough runs; - // these override the previous step-count × 3.5 formula which over-counted - // assistive UI steps (welcome, next-steps) that take seconds, not minutes. - const PERSONA_MINUTES: Record = { - storefront: 8, - 'api-integration': 10, - 'devops-release': 6, - 'ai-augmented': 12, - }; - const estimateMinutes = (id: string, stepCount: number): number => - PERSONA_MINUTES[id] ?? Math.max(5, Math.round((stepCount * 1.25) / 1) * 1); - - // The "ai-augmented" persona gets a `recommended` flag so the gate - // highlights it as the new path. - const personas: PersonaView[] = listPersonas().map((p) => ({ - id: p.id, - label: p.label, - tagline: p.tagline, - description: p.description, - stepCount: p.stepIds.length, - estimatedMinutes: estimateMinutes(p.id, p.stepIds.length), - recommended: p.id === 'ai-augmented', - })); - const personaId = this.store.getPersona(); - const personaDef = getPersona(personaId); - const setupInstance = this.context.workspaceState.get('b2c-dx.setup.activeInstance') ?? null; - if (!personaDef) { - return {persona: null, personas, steps: [], activeStepId: null, setupInstance}; - } - const defs = resolveSteps(personaDef.id); - const workspaceRoot = - this.getConfigProvider?.()?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const detectionSummary = await detectStepConfigurations(workspaceRoot); - const rawSteps = await Promise.all(defs.map((def) => this.buildStepView(personaDef.id, def, detectionSummary))); - // Sequential gating: a step is locked until every step before it is done - // or skipped. The first step is always available. - const steps = rawSteps.map((step, idx) => { - if (idx === 0) return step; - const allPriorResolved = rawSteps - .slice(0, idx) - .every((prior) => prior.status === 'done' || prior.status === 'skipped'); - if (!allPriorResolved && step.status !== 'done') { - return {...step, status: 'locked' as const}; - } - return step; - }); - const activePersonaView = personas.find((p) => p.id === personaDef.id) ?? { - id: personaDef.id, - label: personaDef.label, - tagline: personaDef.tagline, - description: personaDef.description, - stepCount: personaDef.stepIds.length, - estimatedMinutes: estimateMinutes(personaDef.id, personaDef.stepIds.length), - }; - return { - persona: activePersonaView, - personas, - steps, - activeStepId: this.activeStepId, - setupInstance, - }; - } - - private toolDetectionCache: ToolDetectionResult | null = null; - - private async buildStepView( - personaId: PersonaId, - def: StepDefinition, - detectionSummary?: DetectionSummary, - ): Promise { - const record = this.store.getStep(personaId, def.id); - let html: string; - let actions = def.actions ?? []; - - if (def.id === 'install-cli') { - const result = await this.getToolDetection(); - html = generateInstallCliHtml(result); - actions = this.buildInstallCliActions(result); - } else if (def.id === 'ai-skills') { - if (!this.aiSkillsCache) { - this.aiSkillsCache = await detectAllTargets(); - } - html = generateAiSkillsHtml(this.aiSkillsCache); - } else { - const markdown = await this.readMarkdown(def.markdown); - html = renderMarkdown(markdown); - // For the deploy step, prepend a banner showing exactly what will be - // deployed when the user clicks "Deploy Recommended Cartridge", and - // gray out the primary action if the recommended cartridge is already - // present in the active code version. - if (def.id === 'deploy-code') { - const result = await this.buildDeployBanner(); - if (result) { - html = result.html + html; - if (result.alreadyDeployed && actions.length > 0) { - actions = actions.map((a) => - a.command === 'b2c-dx.codeSync.deployOne' - ? { - ...a, - label: `Already Deployed${result.cartridgeName ? ` · ${result.cartridgeName}` : ''}`, - disabled: true, - tooltip: 'This cartridge is already in the active code version.', - } - : a, - ); - } - } - } - } - - let detection: StepView['detection'] = null; - if (detectionSummary) { - const found: StepDetection | null = getDetectionForStep(def.id, detectionSummary); - if (found && found.matchCount > 0 && found.label) { - detection = {label: found.label, matchedNames: found.matchedNames ?? []}; - } - } - - return { - id: def.id, - title: def.title, - summary: def.summary, - status: record?.status ?? 'available', - actions, - html, - detection, - }; - } - - /** Cached list of deployed cartridges per code-version, keyed by `host|version`. */ - private deployedCartridgesCache = new Map(); - - /** - * Fetch the cartridges currently deployed to the active code version. - * Tries the configured SCAPI-first code-version backend and falls back to a - * WebDAV PROPFIND on `Cartridges//` (which is what the deploy - * command itself uses, so credentials are usually already set up). - * - * Returns a tagged result so the banner can show a specific reason instead - * of a generic "OAuth not configured" message. - * - * Cached for 30 seconds to avoid hammering the network on every refresh. - */ - private async fetchDeployedCartridges(codeVersion: string | undefined): Promise { - const provider = this.getConfigProvider?.(); - if (!provider) return {kind: 'no-provider'}; - const instance = provider.getInstance(); - if (!instance) { - const err = provider.getConfigError?.(); - return {kind: 'no-instance', reason: err ?? undefined}; - } - if (!codeVersion) return {kind: 'no-code-version'}; - - const host = provider.getConfig()?.values.hostname ?? 'unknown'; - const cacheKey = `${host}|${codeVersion}`; - const cached = this.deployedCartridgesCache.get(cacheKey); - if (cached && Date.now() - cached.fetchedAt < 30_000) return cached.result; - - let apiError: string | undefined; - - // 1) Try the configured code-version backend (SCAPI-first in auto mode). - try { - const versions: CodeVersionInfo[] = await createScriptsBackend({instance}).listCodeVersions(); - const target = versions.find((v) => v.id === codeVersion); - if (target) { - const names = target.cartridges ?? []; - const result: DeployedCartridgesResult = {kind: 'ok', names, source: 'api'}; - this.deployedCartridgesCache.set(cacheKey, {result, fetchedAt: Date.now()}); - return result; - } - apiError = `code version "${codeVersion}" not found on instance`; - } catch (err) { - apiError = err instanceof Error ? err.message : String(err); - this.log.appendLine(`[onboarding] Code-version discovery failed: ${apiError}`); - } - - // 2) Fallback to WebDAV — same auth path as the deploy command itself. - try { - const entries = await instance.webdav.propfind(`Cartridges/${codeVersion}`, '1'); - const names = entries - .filter((e) => e.isCollection && e.displayName && e.displayName !== codeVersion) - .map((e) => e.displayName as string); - const result: DeployedCartridgesResult = {kind: 'ok', names, source: 'webdav'}; - this.deployedCartridgesCache.set(cacheKey, {result, fetchedAt: Date.now()}); - return result; - } catch (err) { - const webdavError = err instanceof Error ? err.message : String(err); - this.log.appendLine(`[onboarding] WebDAV propfind failed: ${webdavError}`); - return {kind: 'error', reason: apiError ?? webdavError}; - } - } - - /** - * Builds a "what will be deployed" + "already deployed" banner for the - * deploy-code step. Returns the HTML plus a flag indicating whether the - * recommended cartridge is already in the active code version (so the - * caller can disable the primary action). - */ - private async buildDeployBanner(): Promise<{html: string; alreadyDeployed: boolean; cartridgeName?: string} | null> { - const workspaceRoot = - this.getConfigProvider?.()?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const ctx = await readDeployContext(workspaceRoot); - const lastScaffolded = this.context.workspaceState.get('b2c-dx.scaffold.lastCartridgeName'); - - // Mirror the resolution logic in createDeployOneCommand: - // 1. If the last-scaffolded cartridge still exists in the workspace, use it. - // 2. Otherwise, if there's only one cartridge, that's what gets deployed. - // 3. Otherwise the user is shown a picker (we can't predict the choice). - let resolvedCartridge: string | undefined; - let cartridgeSource: 'scaffolded' | 'only' | 'picker' | 'none' = 'none'; - if (workspaceRoot) { - try { - const cartridges = findCartridgesSafe(workspaceRoot); - if (lastScaffolded && cartridges.some((c) => c.name === lastScaffolded)) { - resolvedCartridge = lastScaffolded; - cartridgeSource = 'scaffolded'; - } else if (cartridges.length === 1) { - resolvedCartridge = cartridges[0].name; - cartridgeSource = 'only'; - } else if (cartridges.length > 1) { - cartridgeSource = 'picker'; - } - } catch { - // best-effort — leave as 'none' - } - } - - // Nothing to show if every field is empty. - if (!resolvedCartridge && cartridgeSource === 'none' && !ctx.hostname && !ctx.codeVersion) return null; - - const escape = (s: string) => - s.replace(/[&<>"']/g, (c) => - c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : ''', - ); - const fmt = (v?: string) => - v ? `${escape(v)}` : `not set`; - - const sourceHint = - cartridgeSource === 'scaffolded' - ? 'recently scaffolded' - : cartridgeSource === 'only' - ? 'only cartridge in workspace' - : ''; - const cartridgeLabel = resolvedCartridge - ? `${escape(resolvedCartridge)} ${sourceHint}` - : cartridgeSource === 'picker' - ? `multiple found — you'll be asked to pick one` - : `no cartridges found in workspace`; - - const cartridgeReady = !!resolvedCartridge; - const allReady = cartridgeReady && !!ctx.codeVersion && !!ctx.hostname; - - // Fetch deployed cartridges (best-effort — falls back gracefully). - const deployedResult = await this.fetchDeployedCartridges(ctx.codeVersion); - const deployedNames = deployedResult.kind === 'ok' ? deployedResult.names : []; - const alreadyDeployed = !!(resolvedCartridge && deployedNames.includes(resolvedCartridge)); - - const deployedSection = (() => { - const folderIcon = ``; - const warnIcon = ``; - - if (deployedResult.kind === 'no-provider') { - return `
    - ${warnIcon} - Deployed - - resolving connection… - -
    `; - } - if (deployedResult.kind === 'no-instance') { - const detail = deployedResult.reason ? ` — ${escape(deployedResult.reason)}` : ''; - return `
    - ${warnIcon} - Deployed - - no active B2C instance${detail} - -
    `; - } - if (deployedResult.kind === 'no-code-version') { - return `
    - ${warnIcon} - Deployed - - code-version not set in dw.json - -
    `; - } - if (deployedResult.kind === 'error') { - return `
    - ${warnIcon} - Deployed - - unable to query — ${escape(deployedResult.reason)} - -
    `; - } - const names = deployedResult.names; - if (names.length === 0) { - return `
    - ${folderIcon} - Deployed - - no cartridges deployed yet - -
    `; - } - const chips = names - .map( - (n) => - `${escape(n)}`, - ) - .join(''); - return `
    - ${folderIcon} - Deployed ${names.length} - ${chips} -
    `; - })(); - - const html = ` -
    -
    - - - On click of "Deploy Recommended Cartridge" - - - - ${alreadyDeployed ? 'Already deployed' : allReady ? 'Ready to deploy' : 'Missing details'} - -
    -
    -
    - - Cartridge - ${cartridgeLabel} -
    -
    - - Code version - ${fmt(ctx.codeVersion)} -
    -
    - - Target host - ${fmt(ctx.hostname)} -
    - ${deployedSection} -
    -
    `; - - return {html, alreadyDeployed, cartridgeName: resolvedCartridge}; - } - - private async getToolDetection(): Promise { - if (!this.toolDetectionCache) { - const cached = this.context.globalState.get<{version: string; fetchedAt: number}>( - 'b2c-dx.cli.latestVersionCache', - ); - const latestVersion = cached?.version; - this.toolDetectionCache = await detectTools(latestVersion); - } - return this.toolDetectionCache; - } - - /** Invalidates cached detection so the next refresh re-detects. */ - invalidateToolDetection(): void { - this.toolDetectionCache = null; - } - - private buildInstallCliActions(result: ToolDetectionResult): StepAction[] { - const actions: StepAction[] = []; - - if (!result.b2cCli.installed) { - if (result.npm.installed) { - actions.push({label: 'Install via npm', command: 'b2c-dx.cli.installNpm', primary: true}); - } else if (result.homebrew.installed) { - actions.push({label: 'Install via Homebrew', command: 'b2c-dx.cli.installBrew', primary: true}); - } - actions.push({label: 'Verify CLI', command: 'b2c-dx.cli.verify'}); - actions.push({label: 'Re-check', command: 'b2c-dx.cli.recheck'}); - } else if (result.b2cCliOutdated) { - actions.push({label: 'Update CLI', command: 'b2c-dx.cli.update', primary: true}); - actions.push({label: 'Verify CLI', command: 'b2c-dx.cli.verify'}); - actions.push({label: 'Re-check', command: 'b2c-dx.cli.recheck'}); - } else { - actions.push({label: 'Verify CLI', command: 'b2c-dx.cli.verify', primary: true}); - actions.push({label: 'Update CLI', command: 'b2c-dx.cli.update'}); - actions.push({label: 'Re-check', command: 'b2c-dx.cli.recheck'}); - } - - return actions; - } - - private async readMarkdown(relativePath: string): Promise { - try { - const abs = path.join(this.context.extensionPath, relativePath); - return await fs.readFile(abs, 'utf-8'); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - this.log.appendLine(`[onboarding] failed to read ${relativePath}: ${message}`); - return `# Content unavailable\n\n\`${relativePath}\` could not be loaded.`; - } - } - - private renderShell(): string { - const webview = this.panel.webview; - const nonce = makeNonce(); - const cspSource = webview.cspSource; - const csp = [ - `default-src 'none'`, - `img-src ${cspSource} https: data:`, - `style-src ${cspSource} 'unsafe-inline'`, - `script-src 'nonce-${nonce}'`, - `font-src ${cspSource}`, - ].join('; '); - - return /* html */ ` - - - - - B2C DX: Get Started - - - - - - - - - - - -`; - } - - dispose(): void { - OnboardingPanel.current = undefined; - if (this.aiSkillsWatcher) { - clearInterval(this.aiSkillsWatcher); - this.aiSkillsWatcher = null; - } - this.aiSkillsTermDisposables.forEach((d) => d.dispose()); - this.aiSkillsTermDisposables = []; - this.disposables.forEach((d) => d.dispose()); - this.panel.dispose(); - } -} - -function makeNonce(): string { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let out = ''; - for (let i = 0; i < 32; i++) out += chars.charAt(Math.floor(Math.random() * chars.length)); - return out; -} - -const PANEL_CSS = ` -:root { - color-scheme: light dark; - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 14px; - --sidebar-width: 308px; - --content-max: 920px; - --brand-blue: #0176D3; - --brand-blue-deep: #014486; - --brand-blue-soft: rgba(1, 118, 211, 0.10); - --brand-blue-hairline: rgba(1, 118, 211, 0.28); - --brand-green: #1A8754; - --brand-green-bright: #2FA86A; - --brand-green-soft: rgba(26, 135, 84, 0.12); - --brand-green-hairline: rgba(26, 135, 84, 0.40); - /* Status palette for the sidebar checklist: - done = green, in-progress = amber/yellow, idle/locked/skipped = neutral grey. */ - --status-amber: #C77700; - --status-amber-bright: #E58A0F; - --status-amber-soft: rgba(199, 119, 0, 0.14); - --status-grey: rgba(127, 127, 127, 0.45); - --status-grey-soft: rgba(127, 127, 127, 0.18); - --surface-card: var(--vscode-editorWidget-background, var(--vscode-editor-background)); - --surface-elevated: var(--vscode-sideBar-background, var(--vscode-editor-background)); - --hairline: var(--vscode-panel-border, var(--vscode-editorGroup-border, rgba(128,128,128,0.25))); - --shadow-sm: 0 1px 2px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.04); - --shadow-md: 0 4px 14px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04); -} -* { box-sizing: border-box; } -body { - margin: 0; - font-family: var(--vscode-font-family); - color: var(--vscode-foreground); - /* Layered page gradient: a soft brand-blue radial wash at the top-left, - plus a faint diagonal gradient that fades into the editor background. - Reads as a polished hero surface in light mode and stays subtle in dark - mode because the brand-blue tints sit at low alpha against any base. */ - background: - radial-gradient(ellipse 1200px 600px at 8% -10%, var(--brand-blue-soft), transparent 60%), - radial-gradient(ellipse 900px 500px at 110% 0%, rgba(26, 135, 84, 0.06), transparent 55%), - linear-gradient(180deg, var(--vscode-editor-background) 0%, var(--vscode-editor-background) 100%); - background-attachment: fixed; - padding: 0; - min-height: 100vh; -} -h1, h2, h3 { letter-spacing: -0.01em; } -.muted { color: var(--vscode-descriptionForeground); margin: 0; } -.eyebrow { - display: inline-block; - text-transform: uppercase; - letter-spacing: 0.12em; - font-size: 0.7rem; - font-weight: 600; - color: var(--brand-blue); - margin-bottom: 8px; -} - -/* ─── Brand bar ─────────────────────────────────────── */ -.brand-bar { - position: sticky; - top: 0; - z-index: 10; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 14px 32px; - /* Translucent so the body's soft page gradient shows through. */ - background: color-mix(in srgb, var(--vscode-editor-background) 80%, transparent); - border-bottom: 1px solid var(--hairline); - backdrop-filter: saturate(180%) blur(8px); -} -.brand { display: flex; align-items: center; gap: 14px; min-width: 0; } -.brand-mark { - font-family: 'Inter', 'SF Pro Display', 'Segoe UI Variable Display', 'Segoe UI', system-ui, -apple-system, sans-serif; - font-weight: 800; - font-size: 1.55rem; - letter-spacing: -0.02em; - line-height: 1; - white-space: nowrap; -} -.brand-mark__b2c { - color: var(--brand-blue); - background: linear-gradient(135deg, #1B96FF 0%, var(--brand-blue) 50%, var(--brand-blue-deep) 100%); - -webkit-background-clip: text; - background-clip: text; - -webkit-text-fill-color: transparent; - padding-right: 6px; -} -.brand-mark__dx { - color: var(--brand-blue); - font-style: italic; - font-weight: 700; - position: relative; -} -.brand-mark__dx::before { - content: "·"; - color: var(--brand-blue); - margin-right: 6px; - font-style: normal; - font-weight: 700; -} -.brand-divider { - width: 1px; - height: 22px; - background: var(--hairline); - margin: 0 4px; -} -.brand-tag { - font-size: 0.78rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.14em; - color: var(--vscode-descriptionForeground); -} -/* Active-session chip — shown when a dw.json setup session has named an - instance for this workspace. Clicking it isn't required; users reach the - reset action via the adjacent "Start over" button. */ -.setup-chip { - display: inline-flex; - align-items: center; - gap: 8px; - margin-left: 12px; - padding: 4px 10px; - border-radius: 999px; - background: var(--brand-green-soft); - border: 1px solid var(--brand-green-hairline); - color: var(--brand-green); - font-size: 0.74rem; - font-weight: 600; - letter-spacing: 0.04em; -} -.setup-chip__dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--brand-green); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand-green) 22%, transparent); -} -.brand-actions { display: flex; gap: 8px; flex-shrink: 0; align-items: center; } -button.icon-only { - width: 36px; - height: 36px; - padding: 0; - display: inline-grid; - place-items: center; - border-radius: 50%; -} -button.icon-only .theme-glyph { - font-size: 1.05rem; - line-height: 1; - display: inline-block; - transition: transform 200ms ease; -} -button.icon-only:hover .theme-glyph { transform: rotate(20deg); } -button { - background: var(--brand-blue); - color: #fff; - border: 1px solid transparent; - border-radius: var(--radius-sm); - padding: 7px 14px; - font: inherit; - font-weight: 500; - cursor: pointer; - transition: background 120ms ease, border-color 120ms ease, transform 80ms ease, box-shadow 120ms ease; -} -button:hover { background: var(--brand-blue-deep); } -button:active { transform: translateY(1px); } -button:focus-visible { - outline: 2px solid var(--brand-blue); - outline-offset: 2px; -} -button.ghost { - background: transparent; - color: var(--vscode-foreground); - border-color: var(--hairline); -} -button.ghost:hover { - background: var(--brand-blue-soft); - border-color: var(--brand-blue-hairline); - color: var(--brand-blue); -} -button.secondary { - background: var(--vscode-button-secondaryBackground, transparent); - color: var(--vscode-button-secondaryForeground, var(--vscode-foreground)); - border-color: var(--hairline); -} -button.secondary:hover { - background: var(--vscode-button-secondaryHoverBackground, var(--brand-blue-soft)); -} - -/* ─── Persona gate ─────────────────────────────────── */ -#persona-gate { - position: relative; - max-width: 1080px; - margin: 0 auto; - padding: 64px 40px 56px; -} - -/* Top-right corner: phase chip + concentric rings */ -.gate-corner { - position: absolute; - top: 56px; - right: 40px; - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 18px; - pointer-events: none; - z-index: 1; -} -.phase-chip { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 7px 14px; - border-radius: 999px; - background: var(--surface-card); - border: 1px solid var(--brand-blue-hairline); - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.18em; - color: var(--vscode-foreground); -} -.phase-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--brand-green); - box-shadow: 0 0 0 3px var(--brand-green-soft); -} -.gate-rings { - width: 160px; - height: 160px; - opacity: 0.55; -} -.gate-ring { stroke: var(--brand-blue-hairline); stroke-width: 1; } -.gate-ring.dashed { stroke-dasharray: 2 6; } - -.gate-hero { - position: relative; - z-index: 2; - text-align: center; - margin: 24px auto 32px; - max-width: 760px; -} -.gate-hero .eyebrow { - margin-bottom: 14px; -} -.gate-hero h1 { - font-family: 'Inter','SF Pro Display','Segoe UI Variable Display','Segoe UI',system-ui,-apple-system,sans-serif; - font-size: 3.25rem; - font-weight: 800; - letter-spacing: -0.035em; - line-height: 1.05; - margin: 0 0 16px; -} -.gate-hero .lede { - max-width: 640px; - margin: 0 auto; - font-size: 1.05rem; - line-height: 1.6; - color: var(--vscode-descriptionForeground); -} - -/* Stat strip — enterprise trust signal */ -.stat-strip { - display: grid; - grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; - align-items: center; - gap: 0; - margin: 0 auto 36px; - max-width: 720px; - padding: 22px 12px; - border-top: 1px solid var(--hairline); - border-bottom: 1px solid var(--hairline); -} -.stat { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; -} -.stat-value { - font-family: 'Inter','SF Pro Display','Segoe UI Variable Display','Segoe UI',system-ui,-apple-system,sans-serif; - font-size: 1.85rem; - font-weight: 800; - letter-spacing: -0.03em; - line-height: 1; - color: var(--vscode-foreground); -} -.stat-value small { - font-size: 0.55em; - font-weight: 700; - margin-left: 2px; - color: var(--vscode-descriptionForeground); -} -.stat-value.stat-check { color: var(--brand-green); } -.stat-label { - font-size: 0.7rem; - font-weight: 700; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--vscode-descriptionForeground); -} -.stat-divider { - width: 1px; - height: 36px; - background: var(--hairline); -} -@media (max-width: 720px) { - .stat-strip { grid-template-columns: 1fr 1fr; gap: 18px 0; } - .stat-divider { display: none; } -} - -/* Role cards */ -.persona-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; - margin-bottom: 32px; -} -@media (max-width: 720px) { .persona-grid { grid-template-columns: 1fr; } } -.persona-card { - position: relative; - color: var(--vscode-foreground); - background: var(--surface-card); - border: 1px solid var(--hairline); - border-radius: 16px; - padding: 24px 28px 64px; - display: grid; - grid-template-columns: 56px 1fr; - column-gap: 20px; - row-gap: 6px; - align-items: start; - cursor: pointer; - user-select: none; - overflow: hidden; - box-shadow: var(--shadow-sm); - transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; -} -.persona-card::before { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient(135deg, var(--brand-blue-soft) 0%, transparent 55%); - opacity: 0; - transition: opacity 160ms ease; - pointer-events: none; -} -.persona-card:hover { - border-color: var(--brand-blue-hairline); - box-shadow: var(--shadow-md); - transform: translateY(-2px); -} -.persona-card:hover::before { opacity: 1; } -.persona-card:hover .persona-arrow { transform: translateX(3px); } -.persona-card:focus-visible { - outline: 2px solid var(--brand-blue); - outline-offset: 3px; -} -.persona-card.is-recommended { - border-color: var(--brand-green-hairline); -} -.persona-card.is-recommended::before { - background: linear-gradient(135deg, var(--brand-green-soft) 0%, transparent 55%); -} -.persona-card.is-recommended:hover { border-color: var(--brand-green-hairline); } - -.persona-avatar { - grid-row: 1 / span 3; - width: 56px; - height: 56px; - border-radius: 14px; - background: linear-gradient(135deg, #1B96FF, var(--brand-blue) 60%, var(--brand-blue-deep)); - color: #fff; - display: grid; - place-items: center; - font-weight: 700; - font-size: 1.05rem; - letter-spacing: 0.02em; - box-shadow: 0 4px 12px rgba(1, 118, 211, 0.25); - position: relative; - z-index: 1; - flex-shrink: 0; -} -.persona-avatar svg { - width: 28px; - height: 28px; - color: #fff; -} -.persona-card.is-recommended .persona-avatar { - background: linear-gradient(135deg, var(--brand-green-bright), var(--brand-green)); - box-shadow: 0 4px 12px rgba(26, 135, 84, 0.30); -} -.persona-card h3 { - margin: 0; - font-size: 1.10rem; - font-weight: 700; - line-height: 1.3; - position: relative; - z-index: 1; -} -.persona-card .tagline { - color: var(--brand-blue); - font-size: 0.86rem; - font-weight: 500; - margin: 0; - line-height: 1.4; - position: relative; - z-index: 1; -} -.persona-card.is-recommended .tagline { color: var(--brand-green); } -.persona-card .desc { - color: var(--vscode-foreground); - opacity: 0.78; - font-size: 0.88rem; - line-height: 1.55; - margin: 10px 0 0; - grid-column: 2; - position: relative; - z-index: 1; -} -.persona-meta { - position: absolute; - left: 28px; - bottom: 26px; - font-size: 0.74rem; - font-weight: 700; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--brand-green); - z-index: 1; -} -/* Scoped under .persona-card so we beat the generic button.ghost rules - (same fix that the gate-cta pill needed). The pill renders large and - bright so it reads as "the action" at a glance. */ -.persona-card .persona-arrow, -.persona-card span.persona-arrow { - position: absolute; - right: 20px; - bottom: 16px; - display: inline-flex; - align-items: center; - gap: 8px; - padding: 9px 18px; - border-radius: 999px; - background: var(--brand-blue); - color: #FFFFFF; - font-size: 0.84rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - box-shadow: 0 6px 14px rgba(1, 118, 211, 0.32), 0 1px 2px rgba(1, 118, 211, 0.30); - transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease; - z-index: 2; - pointer-events: none; /* card receives the click */ -} -.persona-card .persona-arrow > span { color: #FFFFFF; } -.persona-card .persona-arrow svg { color: #FFFFFF; stroke: #FFFFFF; } -.persona-card:hover .persona-arrow { - transform: translateX(4px); - box-shadow: 0 8px 18px rgba(1, 118, 211, 0.42), 0 1px 2px rgba(1, 118, 211, 0.30); -} -.persona-card.is-recommended .persona-arrow { - background: var(--brand-green); - box-shadow: 0 6px 14px rgba(26, 135, 84, 0.32), 0 1px 2px rgba(26, 135, 84, 0.30); -} -.persona-card.is-recommended:hover .persona-arrow { - box-shadow: 0 8px 18px rgba(26, 135, 84, 0.42), 0 1px 2px rgba(26, 135, 84, 0.30); -} -.persona-new-pill { - position: absolute; - top: 22px; - right: 22px; - padding: 3px 10px; - border-radius: 999px; - font-size: 0.66rem; - font-weight: 700; - letter-spacing: 0.16em; - background: var(--brand-green-soft); - color: var(--brand-green); - border: 1px solid var(--brand-green-hairline); - z-index: 1; -} - -/* Bottom CTA strip */ -.gate-cta { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 22px 28px; - border-radius: 16px; - background: linear-gradient(90deg, var(--brand-blue), var(--brand-blue-deep)); - color: #fff; - flex-wrap: wrap; - box-shadow: 0 6px 20px rgba(1, 118, 211, 0.20); -} -.gate-cta__copy { - display: flex; - flex-direction: column; - gap: 2px; -} -.gate-cta__copy strong { - font-size: 1.05rem; - font-weight: 700; -} -.gate-cta__copy span { - font-size: 0.85rem; - opacity: 0.85; -} -/* Scoped under .gate-cta to win against the generic button.ghost:hover - rule above, which would otherwise force the label back to brand-blue - on a brand-blue background. */ -.gate-cta .cta-pill, -.gate-cta button.ghost.cta-pill { - background: rgba(255, 255, 255, 0.16); - color: #FFFFFF; - border: 1px solid rgba(255, 255, 255, 0.55); - border-radius: 999px; - padding: 9px 18px; - font-weight: 600; - font-size: 0.86rem; - letter-spacing: 0.02em; - text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); -} -.gate-cta .cta-pill:hover, -.gate-cta button.ghost.cta-pill:hover, -.gate-cta button.ghost.cta-pill:focus-visible { - background: rgba(255, 255, 255, 0.28); - border-color: rgba(255, 255, 255, 0.85); - color: #FFFFFF; -} - -/* ─── Dashboard ─────────────────────────────────────── */ -#dashboard { - max-width: 1240px; - margin: 0 auto; - padding: 32px 32px 48px; -} -.dashboard-hero { - display: flex; - justify-content: space-between; - /* Vertically centre the progress block against the eyebrow + headline pair. */ - align-items: center; - gap: 32px; - margin-bottom: 28px; - flex-wrap: wrap; -} -.dashboard-hero h1 { - /* Refined enterprise serif-grotesk pairing: prefer Salesforce Sans → - IBM Plex Sans → Source Sans 3 (humanist sans, used by Stripe / Shopify) - before the system-ui fallbacks, so SCAPI / OCAPI sit cleanly without - the chunky display weight from the previous Inter/SF-Pro stack. */ - font-family: - 'Salesforce Sans', 'IBM Plex Sans', 'Source Sans 3', 'Source Sans Pro', - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', - Arial, sans-serif; - font-size: 1.75rem; - font-weight: 600; - font-style: normal; - letter-spacing: -0.01em; - line-height: 1.25; - margin: 0; - max-width: 620px; - color: var(--vscode-foreground); -} -.progress-block { - flex: 0 0 auto; - align-self: center; - width: 320px; - max-width: 100%; -} -.progress-meta { - display: flex; - justify-content: space-between; - align-items: baseline; - font-size: 0.82rem; - font-weight: 500; - margin-bottom: 8px; -} -.progress-track { - height: 8px; - border-radius: 999px; - background: var(--brand-blue-soft); - overflow: hidden; - border: 1px solid var(--brand-blue-hairline); -} -.progress-fill { - height: 100%; - width: 0%; - background: linear-gradient(90deg, #1B96FF, var(--brand-blue) 60%, var(--brand-blue-deep)); - transition: width 240ms cubic-bezier(0.4, 0, 0.2, 1); -} - -.layout { - display: grid; - grid-template-columns: var(--sidebar-width) 1fr; - gap: 32px; - align-items: start; -} -@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } } - -/* ─── Sidebar ───────────────────────────────────────── */ -.sidebar { - position: sticky; - top: 80px; - background: var(--surface-elevated); - border: 1px solid var(--hairline); - border-radius: var(--radius-md); - padding: 16px; -} -.sidebar-title { - text-transform: uppercase; - letter-spacing: 0.12em; - font-size: 0.7rem; - font-weight: 600; - color: var(--vscode-descriptionForeground); - padding: 0 6px 10px; -} -.step-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 2px; -} -.step-item { - position: relative; - display: flex; - align-items: flex-start; - gap: 12px; - padding: 10px 12px; - border-radius: var(--radius-sm); - cursor: pointer; - border: 1px solid transparent; - color: var(--vscode-foreground); - transition: background 120ms ease, color 120ms ease; -} -.step-item:hover { background: var(--brand-blue-soft); } -.step-item.active { - background: var(--brand-blue-soft); - border-color: var(--brand-blue-hairline); -} -.step-item.active::before { - content: ""; - position: absolute; - left: -16px; - top: 12px; - bottom: 12px; - width: 3px; - background: var(--brand-blue); - border-radius: 0 3px 3px 0; -} -.step-item .status { - flex: 0 0 auto; - width: 22px; - height: 22px; - border-radius: 50%; - display: grid; - place-items: center; - font-size: 11px; - font-weight: 600; - margin-top: 1px; - /* Default = "available" / not-yet-touched: greyed disabled-looking dot. */ - background: var(--status-grey-soft); - color: var(--vscode-descriptionForeground); - border: 1px solid var(--status-grey); -} -.step-item[data-status="done"] .status { - background: var(--brand-green); - color: #fff; - border-color: var(--brand-green); - box-shadow: 0 0 0 2px var(--brand-green-soft); -} -.step-item[data-status="in-progress"] .status { - background: var(--status-amber); - color: #fff; - border-color: var(--status-amber); - box-shadow: 0 0 0 2px var(--status-amber-soft); -} -.step-item[data-status="skipped"] .status { - background: var(--status-grey-soft); - color: var(--vscode-descriptionForeground); - border-color: var(--status-grey); - border-style: dashed; -} -.step-item[data-status="locked"] .status { - background: transparent; - color: var(--vscode-descriptionForeground); - border-color: var(--status-grey); - border-style: dashed; -} -.step-item.locked { cursor: not-allowed; opacity: 0.55; } -.step-item.locked:hover { background: transparent; } -.step-item.locked .label { color: var(--vscode-descriptionForeground); } -/* Lighter type weight: the previous 500 read as bold at small sizes. */ -.step-item .label { - font-size: 0.9rem; - line-height: 1.4; - min-width: 0; - font-weight: 400; - letter-spacing: 0.005em; -} -.step-item .label .title { font-weight: 450; color: var(--vscode-foreground); } -.step-item.active .label .title { font-weight: 600; } -.step-item[data-status="done"] .label .title { color: var(--vscode-descriptionForeground); } -.step-item .label small { - display: block; - color: var(--vscode-descriptionForeground); - font-size: 0.72rem; - margin-top: 2px; - font-weight: 400; - letter-spacing: 0.02em; -} - -/* ─── Step card ─────────────────────────────────────── */ -.content { min-width: 0; } -.step-card { - position: relative; - background: var(--surface-card); - border: 1px solid var(--hairline); - border-radius: var(--radius-lg); - padding: 28px 32px; - box-shadow: var(--shadow-sm); - overflow: hidden; -} -.step-card__rail { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 4px; - background: linear-gradient(180deg, #1B96FF, var(--brand-blue) 50%, var(--brand-blue-deep)); -} -.step-card__header { - display: grid; - grid-template-columns: 56px 1fr; - gap: 18px; - align-items: start; - margin-bottom: 6px; -} -.step-number { - width: 56px; - height: 56px; - border-radius: 14px; - background: linear-gradient(135deg, #1B96FF, var(--brand-blue) 60%, var(--brand-blue-deep)); - color: #fff; - display: grid; - place-items: center; - font-family: 'Inter', system-ui, sans-serif; - font-weight: 800; - font-size: 1.5rem; - letter-spacing: -0.02em; - box-shadow: var(--shadow-sm); -} -.step-card__title-block { min-width: 0; } -.step-position { - font-size: 0.74rem; - text-transform: uppercase; - letter-spacing: 0.12em; - font-weight: 600; - color: var(--brand-blue); - margin: 0 0 4px; - display: block; -} -.step-card__title-block h2 { - margin: 0 0 6px; - font-size: 1.45rem; - font-weight: 600; - line-height: 1.25; -} -.step-card__title-block p { - margin: 0; - color: var(--vscode-descriptionForeground); - font-size: 0.95rem; - line-height: 1.55; -} -.step-card__actions { - margin-top: 22px; - padding: 14px 16px; - background: var(--brand-blue-soft); - border: 1px solid var(--brand-blue-hairline); - border-radius: var(--radius-md); -} -.step-card__section-label { - display: block; - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.12em; - font-weight: 600; - color: var(--brand-blue-deep); -} -.step-card__actions-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; - margin-bottom: 10px; -} -.detection-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 3px 10px; - border-radius: 999px; - background: var(--brand-green-soft, rgba(26, 135, 84, 0.12)); - color: var(--brand-green, #1A8754); - border: 1px solid var(--brand-green-hairline, rgba(26, 135, 84, 0.40)); - font-size: 0.74rem; - font-weight: 600; - letter-spacing: 0.01em; - white-space: nowrap; - cursor: default; -} -.detection-chip svg { color: inherit; } -.step-actions { - display: flex; - gap: 10px; - flex-wrap: wrap; -} -.step-actions button { min-height: 36px; padding: 7px 16px; font-weight: 600; } -/* Scoped under .step-actions so we beat the generic button.ghost rule: - inside the Quick-actions panel a "secondary" button needs to read on - the brand-blue-soft tint, not vanish into it. */ -.step-actions button.ghost, -.step-actions button.ghost:hover, -.step-actions button.ghost:focus-visible { - background: var(--surface-card); - color: var(--brand-blue); - border: 1px solid var(--brand-blue-hairline); -} -.step-actions button.ghost:hover { - background: var(--brand-blue-soft); - border-color: var(--brand-blue); - color: var(--brand-blue-deep); -} -.step-actions button:disabled, -.step-actions button.ghost:disabled, -.step-actions button:disabled:hover { - cursor: not-allowed; - opacity: 0.55; - background: var(--surface-card); - color: var(--vscode-descriptionForeground); - border: 1px solid var(--hairline); - transform: none; - box-shadow: none; -} -.step-card__body { - margin-top: 22px; - padding-top: 22px; - border-top: 1px solid var(--hairline); -} - -/* ─── Markdown body ─────────────────────────────────── */ -.markdown-body { - line-height: 1.65; - font-size: 0.95rem; - max-width: 720px; -} -.markdown-body > *:first-child { margin-top: 0; } -.markdown-body h1, .markdown-body h2, .markdown-body h3 { - margin-top: 1.6em; - margin-bottom: 0.5em; - font-weight: 600; - letter-spacing: -0.01em; -} -.markdown-body h1 { font-size: 1.25rem; } -.markdown-body h2 { - font-size: 1.1rem; - padding-bottom: 6px; - border-bottom: 1px solid var(--hairline); -} -.markdown-body h3 { font-size: 1rem; color: var(--brand-blue-deep); } -.markdown-body p { margin: 0 0 0.9em; } -.markdown-body code { - background: var(--brand-blue-soft); - color: var(--brand-blue-deep); - padding: 1px 6px; - border-radius: 4px; - font-family: var(--vscode-editor-font-family, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 0.86em; - border: 1px solid var(--brand-blue-hairline); -} -.markdown-body pre { - background: var(--vscode-textCodeBlock-background, rgba(127,127,127,0.10)); - padding: 10px 14px; - border-radius: var(--radius-sm); - overflow-x: auto; - border: 1px solid var(--hairline); - margin: 0 0 12px; - /* Tight single-line-height box. Body inherits 1.65; without these resets, - single-line commands render with empty rows of air. */ - line-height: 1.55; - font-family: var(--vscode-editor-font-family, ui-monospace, SFMono-Regular, Menlo, monospace); - font-size: 0.86em; - min-height: 0; - white-space: pre; -} -.markdown-body pre code { - background: transparent; - padding: 0; - border: none; - margin: 0; - color: var(--vscode-foreground); - font: inherit; - line-height: inherit; - /* Inline so the block doesn't add its own line-box height. */ - display: inline; - white-space: inherit; -} -/* Tight stanza for adjacent code blocks. The adjacent-sibling combinator - (+) breaks when the renderer joins blocks with newlines (whitespace - text nodes between siblings); use general-sibling (~) instead. */ -.markdown-body pre ~ pre { margin-top: 0; } -.markdown-body p + pre { margin-top: 0; } -.markdown-body pre + p { margin-top: 8px; } -.markdown-body a { color: var(--brand-blue); text-decoration: none; border-bottom: 1px solid var(--brand-blue-hairline); } -.markdown-body a:hover { color: var(--brand-blue-deep); border-bottom-color: var(--brand-blue); } -.markdown-body hr { border: none; border-top: 1px solid var(--hairline); margin: 24px 0; } -.markdown-body ul, .markdown-body ol { padding-left: 1.4em; } -.markdown-body li { margin: 0.25em 0; } -.markdown-body blockquote { - margin: 0 0 1em; - padding: 10px 16px; - border-left: 3px solid var(--brand-blue); - background: var(--brand-blue-soft); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; - color: var(--vscode-foreground); -} -.markdown-body table { - border-collapse: collapse; - width: 100%; - margin: 0 0 1em; - font-size: 0.9em; -} -.markdown-body th, .markdown-body td { - text-align: left; - padding: 8px 10px; - border-bottom: 1px solid var(--hairline); -} -.markdown-body th { - background: var(--brand-blue-soft); - color: var(--brand-blue-deep); - font-weight: 600; -} -.markdown-body strong { font-weight: 600; } - -/* ─── Bottom nav ───────────────────────────────────── */ -/* Sticky to the bottom of the viewport so Previous/Skip/Next stay reachable - without scrolling — matches the pattern Stripe and Datadog use for - long-form onboarding. Translucent background + backdrop-blur lets the - page wash bleed through. */ -.step-nav { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 16px; - margin-top: 24px; - position: sticky; - bottom: 0; - z-index: 5; - padding: 14px 16px 16px; - /* Solid editor background so scrolling body text doesn't bleed through. - The hairline + lifted shadow signal the sticky boundary cleanly. */ - background: var(--vscode-editor-background); - border-top: 1px solid var(--hairline); - border-radius: var(--radius-md) var(--radius-md) 0 0; - box-shadow: 0 -10px 24px -16px rgba(0, 0, 0, 0.18); -} -/* Bottom padding on .content so the last paragraph isn't trapped behind the - sticky bar (~96px = nav height + breathing room). */ -.content { padding-bottom: 96px; } -.nav-btn { - display: flex; - align-items: center; - gap: 12px; - padding: 12px 16px; - background: var(--surface-card); - color: var(--vscode-foreground); - border: 1px solid var(--hairline); - border-radius: var(--radius-md); - cursor: pointer; - text-align: left; - min-height: 56px; - width: 100%; - /* Use the same humanist sans as the dashboard headline so the nav reads - as part of the chrome, not the markdown body. */ - font-family: - 'Salesforce Sans', 'IBM Plex Sans', 'Source Sans 3', 'Source Sans Pro', - -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-weight: 500; - letter-spacing: 0; -} -.nav-btn:hover:not([disabled]) { - border-color: var(--brand-blue-hairline); - background: var(--brand-blue-soft); - color: var(--vscode-foreground); -} -.nav-btn[disabled] { opacity: 0.4; cursor: not-allowed; } -.nav-btn.next { justify-content: flex-end; } -.nav-btn.next.primary { - background: var(--brand-blue); - color: #fff; - border-color: var(--brand-blue); -} -.nav-btn.next.primary:hover:not([disabled]) { - background: var(--brand-blue-deep); - color: #fff; -} -.nav-btn .nav-text { display: flex; flex-direction: column; min-width: 0; } -.nav-btn .nav-text.right { text-align: right; } -.nav-btn .nav-text small { - font-size: 0.68rem; - text-transform: uppercase; - letter-spacing: 0.14em; - font-weight: 700; - color: var(--vscode-descriptionForeground); - opacity: 0.85; -} -.nav-btn.next.primary .nav-text small { color: rgba(255, 255, 255, 0.85); opacity: 1; } -.nav-btn .nav-text span:not(small) { - font-size: 0.92rem; - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.nav-btn .nav-chevron { - font-size: 1.5rem; - line-height: 1; - flex-shrink: 0; -} -.nav-spacer { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; -} -.link-btn { - background: transparent; - color: var(--brand-blue); - border: none; - padding: 4px 10px; - cursor: pointer; - font-weight: 500; - font-size: 0.88rem; - border-radius: 4px; -} -.link-btn:hover { background: var(--brand-blue-soft); color: var(--brand-blue-deep); } -.kbd-hint { font-size: 0.75rem; } -.kbd-hint kbd { - background: var(--vscode-keybindingLabel-background, rgba(127,127,127,0.18)); - border: 1px solid var(--vscode-keybindingLabel-border, rgba(127,127,127,0.3)); - border-radius: 3px; - padding: 1px 5px; - font-family: var(--vscode-editor-font-family, monospace); - font-size: 0.72rem; -} -@media (max-width: 720px) { - .step-nav { grid-template-columns: 1fr; } - .nav-btn { width: 100%; } -} -`; - -const PANEL_JS = ` -(function () { - const vscode = acquireVsCodeApi(); - const gate = document.getElementById('persona-gate'); - const dashboard = document.getElementById('dashboard'); - const personaCards = document.getElementById('persona-cards'); - const personaLabel = document.getElementById('persona-label'); - const personaTagline = document.getElementById('persona-tagline'); - const stepList = document.getElementById('step-list'); - const stepNumber = document.getElementById('step-number'); - const stepPosition = document.getElementById('step-position'); - const stepTitle = document.getElementById('step-title'); - const stepSummary = document.getElementById('step-summary'); - const stepActions = document.getElementById('step-actions'); - const stepActionsWrap = document.getElementById('step-actions-wrap'); - const stepBody = document.getElementById('step-body'); - const stepCard = document.querySelector('.step-card'); - const btnPrev = document.getElementById('btn-prev'); - const btnNext = document.getElementById('btn-next'); - const btnPrevTop = document.getElementById('btn-prev-top'); - const btnNextTop = document.getElementById('btn-next-top'); - const prevTitleEl = document.getElementById('prev-title'); - const nextTitleEl = document.getElementById('next-title'); - const progressCounter = document.getElementById('progress-counter'); - const progressPercent = document.getElementById('progress-percent'); - const progressFill = document.getElementById('progress-fill'); - const btnSkip = document.getElementById('btn-skip'); - const btnChangePersona = document.getElementById('btn-change-persona'); - const btnReset = document.getElementById('btn-reset'); - const btnMarkAllDone = document.getElementById('btn-mark-all-done'); - const btnThemeToggle = document.getElementById('btn-theme-toggle'); - const btnCtaMarkAllDone = document.getElementById('btn-cta-mark-all-done'); - const btnStartOver = document.getElementById('btn-start-over'); - const setupChip = document.getElementById('setup-chip'); - const setupChipName = document.getElementById('setup-chip-name'); - - let currentState = null; - - function post(msg) { vscode.postMessage(msg); } - - function renderSetupChip(state) { - if (!setupChip || !btnStartOver || !setupChipName) return; - if (state.setupInstance) { - setupChipName.textContent = state.setupInstance; - setupChip.hidden = false; - btnStartOver.hidden = false; - } else { - setupChip.hidden = true; - btnStartOver.hidden = true; - } - } - - function render(state) { - currentState = state; - renderSetupChip(state); - if (!state.persona) { - gate.hidden = false; - dashboard.hidden = true; - renderPersonaGate(state.personas); - return; - } - gate.hidden = true; - dashboard.hidden = false; - personaLabel.textContent = state.persona.label; - // Headers don't carry trailing punctuation — strip a single period if - // the persona definition's tagline ends with one. - personaTagline.textContent = state.persona.tagline.replace(/\.$/, ''); - renderStepList(state.steps, state.activeStepId); - - const activeIdx = state.steps.findIndex((s) => s.id === state.activeStepId); - const idx = activeIdx >= 0 ? activeIdx : 0; - const active = state.steps[idx]; - const prevStep = idx > 0 ? state.steps[idx - 1] : null; - const nextStep = idx < state.steps.length - 1 ? state.steps[idx + 1] : null; - renderActiveStep(active, idx, state.steps.length); - renderNav(prevStep, nextStep); - renderProgress(state.steps, idx); - } - - function renderProgress(steps, idx) { - const total = steps.length; - const doneCount = steps.filter((s) => s.status === 'done').length; - const pct = total === 0 ? 0 : Math.round((doneCount / total) * 100); - progressCounter.textContent = 'Step ' + (idx + 1) + ' of ' + total; - progressPercent.textContent = pct + '% complete'; - progressFill.style.width = pct + '%'; - } - - function renderNav(prev, next) { - prevTitleEl.textContent = prev ? prev.title : 'Start of walkthrough'; - nextTitleEl.textContent = next ? next.title : 'Finish walkthrough'; - btnPrev.disabled = !prev; - if (btnPrevTop) btnPrevTop.disabled = !prev; - } - - // Per-persona icon SVGs. Stroke uses currentColor so the avatar tile's - // foreground (white inside the gradient square) drives them. - const PERSONA_ICONS = { - 'storefront': - '', - 'api-integration': - '', - 'devops-release': - '', - 'ai-augmented': - '', - }; - - function renderPersonaGate(personas) { - personaCards.innerHTML = ''; - personas.forEach((p) => { - const card = document.createElement('div'); - card.className = 'persona-card' + (p.recommended ? ' is-recommended' : ''); - card.setAttribute('role', 'button'); - card.setAttribute('tabindex', '0'); - card.setAttribute('aria-label', p.label); - const newPill = p.recommended ? 'NEW' : ''; - // Generic fallback icon (a small square cluster) for any persona that - // doesn't have a dedicated SVG yet — still icon-based, never letters. - const fallbackIcon = - ''; - const iconHtml = PERSONA_ICONS[p.id] || fallbackIcon; - card.innerHTML = [ - '', - '

    ', - '

    ', - '

    ', - '', - '', - newPill, - ].join(''); - card.querySelector('h3').textContent = p.label; - card.querySelector('.tagline').textContent = p.tagline.replace(/\.$/, ''); - card.querySelector('.desc').textContent = p.description; - card.querySelector('.persona-meta').textContent = - p.stepCount + ' phases · ~' + p.estimatedMinutes + ' min'; - const select = () => post({type: 'selectPersona', personaId: p.id}); - card.addEventListener('click', select); - card.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - select(); - } - }); - personaCards.appendChild(card); - }); - } - - function renderStepList(steps, activeId) { - stepList.innerHTML = ''; - steps.forEach((step, idx) => { - const locked = step.status === 'locked'; - const li = document.createElement('li'); - li.className = 'step-item' + (step.id === activeId ? ' active' : '') + (locked ? ' locked' : ''); - li.dataset.status = step.status; - li.innerHTML = [ - '', - '', - ].join(''); - li.querySelector('.status').textContent = statusGlyph(step.status, idx + 1); - li.querySelector('.title').textContent = step.title; - li.querySelector('small').textContent = labelForStatus(step.status); - if (locked) { - li.setAttribute('aria-disabled', 'true'); - li.setAttribute('title', 'Complete the previous step to unlock this one.'); - } else { - li.setAttribute('role', 'button'); - li.setAttribute('tabindex', '0'); - li.addEventListener('click', () => post({type: 'openStep', stepId: step.id})); - li.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - post({type: 'openStep', stepId: step.id}); - } - }); - } - stepList.appendChild(li); - }); - } - - function statusGlyph(status, ordinal) { - switch (status) { - case 'done': return '✓'; - case 'in-progress': return '●'; - case 'skipped': return '–'; - case 'locked': return '🔒'; - default: return String(ordinal); - } - } - - function labelForStatus(status) { - switch (status) { - case 'done': return 'Completed'; - case 'in-progress': return 'In progress'; - case 'skipped': return 'Skipped'; - case 'locked': return 'Locked — complete the previous step'; - default: return ''; - } - } - - function renderActiveStep(step, idx, total) { - const detectionChip = document.getElementById('step-detection-chip'); - const detectionLabel = document.getElementById('step-detection-label'); - if (!step) { - stepNumber.textContent = ''; - stepPosition.textContent = ''; - stepTitle.textContent = ''; - stepSummary.textContent = ''; - stepActions.innerHTML = ''; - stepActionsWrap.hidden = true; - stepBody.innerHTML = ''; - if (detectionChip) detectionChip.hidden = true; - return; - } - stepNumber.textContent = String(idx + 1); - stepPosition.textContent = 'Step ' + (idx + 1) + ' of ' + total; - stepTitle.textContent = step.title; - stepSummary.textContent = step.summary; - stepBody.innerHTML = step.html; - stepActions.innerHTML = ''; - if (step.actions && step.actions.length > 0) { - stepActionsWrap.hidden = false; - step.actions.forEach((action) => { - const btn = document.createElement('button'); - if (!action.primary) btn.className = 'ghost'; - btn.textContent = action.label; - if (action.tooltip) btn.title = action.tooltip; - if (action.disabled) { - btn.disabled = true; - btn.setAttribute('aria-disabled', 'true'); - } else { - btn.addEventListener('click', () => - post({type: 'runAction', command: action.command, args: action.args, stepId: step.id}), - ); - } - stepActions.appendChild(btn); - }); - } else { - stepActionsWrap.hidden = true; - } - // Per-step config detection chip (e.g., "1 configuration detected") - if (detectionChip && detectionLabel) { - if (step.detection && step.detection.label) { - detectionLabel.textContent = step.detection.label; - const names = step.detection.matchedNames || []; - detectionChip.title = names.length - ? 'Detected in dw.json: ' + names.join(', ') - : 'Detected in dw.json'; - detectionChip.hidden = false; - // Ensure the actions wrapper is visible even if there are no actions, - // so the chip alone can communicate "this step is already configured". - if (step.actions && step.actions.length === 0) stepActionsWrap.hidden = false; - } else { - detectionChip.hidden = true; - } - } - // Scroll the card (not the body) so step-header stays visible after navigation. - if (stepCard && typeof stepCard.scrollIntoView === 'function') { - stepCard.scrollIntoView({behavior: 'smooth', block: 'start'}); - } - } - - // AI skills step: dispatch button clicks (install skills / run cmd) before - // the generic link interceptor sees them. - document.addEventListener('click', (e) => { - const btn = e.target && e.target.closest && e.target.closest('[data-action]'); - if (!btn) return; - const action = btn.getAttribute('data-action'); - if (action === 'install-skills') { - const ide = btn.getAttribute('data-ide') || ''; - e.preventDefault(); - post({type: 'aiSkills.installSkills', ide: ide}); - } else if (action === 'run-cmd') { - const cmd = btn.getAttribute('data-cmd') || ''; - const label = btn.getAttribute('data-label') || 'Install'; - e.preventDefault(); - post({type: 'aiSkills.runCommand', cmd: cmd, label: label}); - } else if (action === 'ai-recheck') { - e.preventDefault(); - post({type: 'aiSkills.recheck'}); - } - }); - - // Intercept clicks on any link inside the content area. We NEVER let the - // webview follow command: or http links directly — all routing goes through - // the extension host via postMessage. - document.addEventListener('click', (e) => { - const anchor = e.target && e.target.closest && e.target.closest('a[href]'); - if (!anchor) return; - const href = anchor.getAttribute('href'); - if (!href || href === '#') return; - e.preventDefault(); - post({type: 'openLink', url: href}); - }); - - btnSkip.addEventListener('click', () => { - if (!currentState || !currentState.activeStepId) return; - post({type: 'skipStep', stepId: currentState.activeStepId}); - }); - btnPrev.addEventListener('click', () => post({type: 'goPrev'})); - btnNext.addEventListener('click', () => post({type: 'goNext'})); - if (btnPrevTop) btnPrevTop.addEventListener('click', () => post({type: 'goPrev'})); - if (btnNextTop) btnNextTop.addEventListener('click', () => post({type: 'goNext'})); - btnChangePersona.addEventListener('click', () => post({type: 'changePersona'})); - btnReset.addEventListener('click', () => post({type: 'reset'})); - if (btnMarkAllDone) { - btnMarkAllDone.addEventListener('click', () => - post({type: 'runAction', command: 'b2c-dx.walkthrough.markAllDone'}), - ); - } - if (btnThemeToggle) { - btnThemeToggle.addEventListener('click', () => post({type: 'runAction', command: 'b2c-dx.theme.toggle'})); - } - if (btnCtaMarkAllDone) { - btnCtaMarkAllDone.addEventListener('click', () => - post({type: 'runAction', command: 'b2c-dx.walkthrough.markAllDone'}), - ); - } - if (btnStartOver) { - btnStartOver.addEventListener('click', () => post({type: 'runAction', command: 'b2c-dx.setup.resetSession'})); - } - - // Keyboard navigation: Alt+← / Alt+→ (and the usual PageUp/Down pattern). - document.addEventListener('keydown', (e) => { - if (!currentState || !currentState.persona) return; - if (e.altKey && e.key === 'ArrowRight') { e.preventDefault(); post({type: 'goNext'}); } - else if (e.altKey && e.key === 'ArrowLeft') { e.preventDefault(); post({type: 'goPrev'}); } - }); - - window.addEventListener('message', (event) => { - const msg = event.data; - if (msg && msg.type === 'state') render(msg.state); - }); - - post({type: 'ready'}); -}()); -`; diff --git a/packages/b2c-vs-extension/src/walkthrough/personas.ts b/packages/b2c-vs-extension/src/walkthrough/personas.ts deleted file mode 100644 index f7d1f6d95..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/personas.ts +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 - */ - -export type PersonaId = 'storefront' | 'api-integration' | 'devops-release' | 'ai-augmented'; - -export interface StepDefinition { - id: string; - title: string; - summary: string; - /** Path relative to the extension root. The onboarding panel resolves this via asWebviewUri. */ - markdown: string; - /** - * Commands the step can trigger from the UI. Surfaced as buttons in the panel header. - */ - actions?: StepAction[]; -} - -export interface StepAction { - label: string; - command: string; - args?: unknown[]; - /** Marks this as the primary call-to-action (rendered as a filled button). */ - primary?: boolean; - /** When true, the button renders disabled with a hint tooltip. */ - disabled?: boolean; - /** Tooltip / aria-label describing why the action is in its current state. */ - tooltip?: string; -} - -export interface PersonaDefinition { - id: PersonaId; - label: string; - tagline: string; - description: string; - stepIds: string[]; -} - -/** - * Catalog of every step that can appear in any persona flow. - * The markdown files are the existing walkthrough content — we render them inside the panel - * instead of the built-in VS Code walkthrough surface. - */ -export const STEP_CATALOG: Record = { - welcome: { - id: 'welcome', - title: 'Welcome to B2C Commerce Development', - summary: 'What the extension does and what you will learn.', - markdown: 'media/walkthrough/welcome.md', - }, - 'configure-dw-json': { - id: 'configure-dw-json', - title: 'Connect to Your B2C Instance', - summary: 'Connection-only: name the instance and pick its hostname / code-version.', - markdown: 'media/walkthrough/dw-json-setup.md', - actions: [ - {label: 'Set up connection', command: 'b2c-dx.setup.connection', primary: true}, - {label: 'Inspect resolved config', command: 'b2c-dx.walkthrough.inspectSetup'}, - {label: 'Open dw.json', command: 'workbench.action.quickOpen', args: ['dw.json']}, - ], - }, - 'setup-oauth': { - id: 'setup-oauth', - title: 'Set Up OAuth Credentials', - summary: 'Add `client-id` + `client-secret`. Pick where the secret lives.', - markdown: 'media/walkthrough/oauth-setup.md', - actions: [ - {label: 'Set up OAuth', command: 'b2c-dx.setup.oauth', primary: true}, - {label: 'Inspect resolved config', command: 'b2c-dx.walkthrough.inspectSetup'}, - ], - }, - 'explore-webdav': { - id: 'explore-webdav', - title: 'Browse Your Instance with WebDAV', - summary: 'Add `username` + `password`, then open the WebDAV browser.', - markdown: 'media/walkthrough/webdav-browser.md', - actions: [ - {label: 'Set up WebDAV credentials', command: 'b2c-dx.setup.webdav', primary: true}, - {label: 'Open WebDAV Browser', command: 'b2c-dx.listWebDav'}, - {label: 'Inspect resolved config', command: 'b2c-dx.walkthrough.inspectSetup'}, - ], - }, - 'setup-cartridges': { - id: 'setup-cartridges', - title: 'Set Up Cartridge Development', - summary: 'Detect or create cartridges. Add SCAPI fields here if you need the API Browser.', - markdown: 'media/walkthrough/cartridge-structure.md', - actions: [ - {label: 'Create New Cartridge', command: 'b2c-dx.scaffold.generate', primary: true}, - {label: 'Set up SCAPI (short-code, tenant-id)', command: 'b2c-dx.setup.scapi'}, - {label: 'Refresh Cartridge List', command: 'b2c-dx.codeSync.refreshCartridges'}, - {label: 'Inspect resolved config', command: 'b2c-dx.walkthrough.inspectSetup'}, - ], - }, - 'deploy-code': { - id: 'deploy-code', - title: 'Deploy Your First Cartridge', - summary: 'Upload cartridge code to your sandbox.', - markdown: 'media/walkthrough/deploy-cartridge.md', - actions: [ - {label: 'Deploy Recommended Cartridge', command: 'b2c-dx.codeSync.deployOne', primary: true}, - {label: 'Deploy All Cartridges', command: 'b2c-dx.codeSync.deploy'}, - {label: 'Refresh WebDAV Browser', command: 'b2c-dx.webdav.refresh'}, - ], - }, - 'manage-sandboxes': { - id: 'manage-sandboxes', - title: 'Work with Development Sandboxes', - summary: 'Create, start, stop, and extend sandboxes.', - markdown: 'media/walkthrough/sandbox-explorer.md', - actions: [{label: 'Open Sandbox Explorer', command: 'workbench.view.extension.b2c-dx-sandboxes', primary: true}], - }, - 'enable-code-sync': { - id: 'enable-code-sync', - title: 'Automate Deployment with Code Sync', - summary: 'Auto-upload cartridge changes as you save.', - markdown: 'media/walkthrough/code-sync.md', - actions: [ - {label: 'Start Code Sync', command: 'b2c-dx.codeSync.start', primary: true}, - {label: 'Stop Code Sync', command: 'b2c-dx.codeSync.stop'}, - ], - }, - 'next-steps': { - id: 'next-steps', - title: "You're Ready! Explore More Features", - summary: 'Where to go next.', - markdown: 'media/walkthrough/next-steps.md', - }, - 'install-cli': { - id: 'install-cli', - title: 'Install the B2C CLI', - summary: 'Optional, but unlocks deploys, log tailing, and sandbox commands from the terminal.', - markdown: 'media/walkthrough/install-cli.md', - actions: [ - {label: 'Verify CLI', command: 'b2c-dx.cli.verify', primary: true}, - {label: 'Update CLI', command: 'b2c-dx.cli.update'}, - ], - }, - 'ai-skills': { - id: 'ai-skills', - title: 'Set Up Agent Skills & MCP', - summary: - 'One-click install of B2C agent skills + MCP for Claude Code, Cursor, Copilot, Windsurf, Codex, OpenCode, and more.', - markdown: 'media/walkthrough/ai-skills.md', - }, -}; - -export const PERSONAS: Record = { - storefront: { - id: 'storefront', - label: 'Storefront developer', - tagline: 'Build SFRA / PWA Kit templates, controllers, and ISML.', - description: 'Cartridge authoring, fast iteration with Code Sync, and WebDAV.', - stepIds: [ - 'welcome', - 'install-cli', - 'configure-dw-json', - 'setup-cartridges', - 'deploy-code', - 'enable-code-sync', - 'explore-webdav', - 'next-steps', - ], - }, - 'api-integration': { - id: 'api-integration', - label: 'API / integration developer', - tagline: 'Work with SCAPI, OCAPI, jobs, and hooks.', - description: 'OAuth setup and the API Browser are first-class; Code Sync is optional.', - stepIds: [ - 'welcome', - 'install-cli', - 'configure-dw-json', - 'setup-oauth', - 'explore-webdav', - 'setup-cartridges', - 'deploy-code', - 'next-steps', - ], - }, - 'devops-release': { - id: 'devops-release', - label: 'DevOps / release engineer', - tagline: 'Manage sandbox lifecycle, code versions, and CAPs.', - description: 'OAuth + Sandbox Explorer front and center. Less time on cartridge authoring.', - stepIds: [ - 'welcome', - 'install-cli', - 'configure-dw-json', - 'setup-oauth', - 'manage-sandboxes', - 'deploy-code', - 'next-steps', - ], - }, - 'ai-augmented': { - id: 'ai-augmented', - label: 'AI-augmented developer', - tagline: 'Pair Cursor / Claude Code / Copilot with this extension.', - description: - 'AI-first onboarding: get your IDE wired up to B2C agent skills and MCP first, then connect to a sandbox and deploy.', - // AI setup leads — agent skills + MCP get installed before instance config - // so the IDE has B2C context while the user works through the rest. - stepIds: [ - 'welcome', - 'install-cli', - 'ai-skills', - 'configure-dw-json', - 'setup-cartridges', - 'deploy-code', - 'enable-code-sync', - 'next-steps', - ], - }, -}; - -export function getPersona(id: string | null | undefined): PersonaDefinition | null { - if (!id) return null; - return (PERSONAS as Record)[id] ?? null; -} - -export function listPersonas(): PersonaDefinition[] { - return Object.values(PERSONAS); -} - -export function resolveSteps(personaId: PersonaId): StepDefinition[] { - return PERSONAS[personaId].stepIds.map((id) => { - const def = STEP_CATALOG[id]; - if (!def) throw new Error(`Unknown step id in persona ${personaId}: ${id}`); - return def; - }); -} diff --git a/packages/b2c-vs-extension/src/walkthrough/state.ts b/packages/b2c-vs-extension/src/walkthrough/state.ts deleted file mode 100644 index e0d0bb260..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/state.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 vscode from 'vscode'; - -export type StepStatus = 'locked' | 'available' | 'in-progress' | 'done' | 'skipped'; - -export interface StepRecord { - status: StepStatus; - startedAt?: string; - completedAt?: string; - skippedAt?: string; -} - -export interface OnboardingSnapshot { - persona: string | null; - steps: Record; - schemaVersion: number; -} - -const STATE_KEY = 'b2c-dx.onboarding.state'; -const CURRENT_SCHEMA_VERSION = 1; - -function emptySnapshot(): OnboardingSnapshot { - return {persona: null, steps: {}, schemaVersion: CURRENT_SCHEMA_VERSION}; -} - -function stepKey(persona: string, stepId: string): string { - return `${persona}:${stepId}`; -} - -export class OnboardingStateStore { - private readonly memento: vscode.Memento; - private readonly emitter = new vscode.EventEmitter(); - - readonly onDidChange = this.emitter.event; - - constructor(context: vscode.ExtensionContext) { - // Per-workspace state: each workspace has its own onboarding lifecycle. - // A fresh workspace = a fresh onboarding flow. - this.memento = context.workspaceState; - } - - get(): OnboardingSnapshot { - const raw = this.memento.get(STATE_KEY); - if (!raw || typeof raw !== 'object') return emptySnapshot(); - if (raw.schemaVersion !== CURRENT_SCHEMA_VERSION) { - return {...emptySnapshot(), persona: raw.persona ?? null}; - } - return raw; - } - - getPersona(): string | null { - return this.get().persona; - } - - async setPersona(persona: string | null): Promise { - const current = this.get(); - await this.write({...current, persona}); - } - - getStep(persona: string, stepId: string): StepRecord | undefined { - return this.get().steps[stepKey(persona, stepId)]; - } - - async updateStep(persona: string, stepId: string, patch: Partial): Promise { - const current = this.get(); - const key = stepKey(persona, stepId); - const existing: StepRecord = current.steps[key] ?? {status: 'available'}; - const next: StepRecord = {...existing, ...patch}; - await this.write({...current, steps: {...current.steps, [key]: next}}); - return next; - } - - async markStarted(persona: string, stepId: string): Promise { - const existing = this.getStep(persona, stepId); - if (existing?.status === 'done') return; - await this.updateStep(persona, stepId, { - status: 'in-progress', - startedAt: existing?.startedAt ?? new Date().toISOString(), - }); - } - - async markCompleted(persona: string, stepId: string): Promise { - await this.updateStep(persona, stepId, { - status: 'done', - completedAt: new Date().toISOString(), - }); - } - - async markSkipped(persona: string, stepId: string): Promise { - await this.updateStep(persona, stepId, { - status: 'skipped', - skippedAt: new Date().toISOString(), - }); - } - - async reset(): Promise { - await this.write(emptySnapshot()); - } - - private async write(next: OnboardingSnapshot): Promise { - await this.memento.update(STATE_KEY, next); - this.emitter.fire(next); - } - - dispose(): void { - this.emitter.dispose(); - } -} diff --git a/packages/b2c-vs-extension/src/walkthrough/stepDetection.ts b/packages/b2c-vs-extension/src/walkthrough/stepDetection.ts deleted file mode 100644 index b48716283..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/stepDetection.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 fs from 'fs/promises'; -import * as path from 'path'; - -/** - * Lightweight per-step config detection. Reads dw.json directly from the - * workspace root so the panel can show "1 similar configuration detected" - * style hints without depending on the b2c CLI being installed. - */ - -export interface StepDetection { - /** Number of matching configurations detected (e.g., instances with OAuth). */ - matchCount: number; - /** Total instance entries scanned. */ - totalInstances: number; - /** Short label to display in the chip (e.g., "1 similar configuration detected"). */ - label?: string; - /** Names of matched instances, when applicable. */ - matchedNames?: string[]; -} - -interface DwJsonInstance { - name?: string; - hostname?: string; - 'code-version'?: string; - codeVersion?: string; - username?: string; - password?: string; - 'client-id'?: string; - clientId?: string; - 'client-secret'?: string; - clientSecret?: string; - 'short-code'?: string; - shortCode?: string; - 'tenant-id'?: string; - tenantId?: string; - cartridge?: unknown; - cartridgesPath?: string; - active?: boolean; -} - -interface DwJsonShape extends DwJsonInstance { - configs?: DwJsonInstance[]; -} - -async function readDwJson(workspaceRoot: string): Promise { - try { - const raw = await fs.readFile(path.join(workspaceRoot, 'dw.json'), 'utf-8'); - return JSON.parse(raw) as DwJsonShape; - } catch { - return null; - } -} - -/** Flatten a dw.json into a list of instance config blocks. */ -function flattenInstances(dw: DwJsonShape | null): DwJsonInstance[] { - if (!dw) return []; - if (Array.isArray(dw.configs) && dw.configs.length > 0) { - return dw.configs; - } - // Top-level shape: dw.json itself describes one instance. - return [dw]; -} - -const has = (v: unknown): boolean => typeof v === 'string' && v.trim().length > 0; - -function pluralize(n: number, sing: string, plural: string): string { - return n === 1 ? sing : plural; -} - -/** Check what's configured on each instance and tally per category. */ -export interface DetectionSummary { - connection: StepDetection; - oauth: StepDetection; - webdav: StepDetection; - scapi: StepDetection; - cartridges: StepDetection; -} - -export async function detectStepConfigurations(workspaceRoot: string | undefined): Promise { - const empty: StepDetection = {matchCount: 0, totalInstances: 0}; - if (!workspaceRoot) { - return { - connection: {...empty}, - oauth: {...empty}, - webdav: {...empty}, - scapi: {...empty}, - cartridges: {...empty}, - }; - } - - const dw = await readDwJson(workspaceRoot); - const instances = flattenInstances(dw); - const total = instances.length; - - const namesWith = (predicate: (i: DwJsonInstance) => boolean): string[] => - instances - .filter(predicate) - .map((i) => i.name) - .filter((n): n is string => typeof n === 'string' && n.length > 0); - - const connectionNames = namesWith((i) => has(i.hostname)); - const oauthNames = namesWith((i) => has(i['client-id'] ?? i.clientId)); - const webdavNames = namesWith((i) => has(i.username) && has(i.password)); - const scapiNames = namesWith((i) => has(i['short-code'] ?? i.shortCode) && has(i['tenant-id'] ?? i.tenantId)); - const cartridgeNames = namesWith((i) => has(i.cartridgesPath) || Array.isArray(i.cartridge)); - - const make = (names: string[]): StepDetection => { - if (names.length === 0) return {matchCount: 0, totalInstances: total}; - return { - matchCount: names.length, - totalInstances: total, - matchedNames: names, - label: `${names.length} ${pluralize(names.length, 'configuration', 'configurations')} detected`, - }; - }; - - return { - connection: make(connectionNames), - oauth: make(oauthNames), - webdav: make(webdavNames), - scapi: make(scapiNames), - cartridges: make(cartridgeNames), - }; -} - -/** Pulled from dw.json for the deploy-code step's "what will be deployed" preview. */ -export interface DeployContext { - hostname?: string; - codeVersion?: string; - instanceName?: string; -} - -/** Read the active instance's deploy-relevant fields from dw.json. */ -export async function readDeployContext(workspaceRoot: string | undefined): Promise { - if (!workspaceRoot) return {}; - const dw = await readDwJson(workspaceRoot); - if (!dw) return {}; - const instances = flattenInstances(dw); - // Prefer the active instance; fall back to the first one. - const active = instances.find((i) => i.active === true) ?? instances[0]; - if (!active) return {}; - return { - hostname: active.hostname, - codeVersion: active['code-version'] ?? active.codeVersion, - instanceName: active.name, - }; -} - -/** Map a step id to the relevant detection bucket. */ -export function getDetectionForStep(stepId: string, summary: DetectionSummary): StepDetection | null { - switch (stepId) { - case 'configure-dw-json': - return summary.connection; - case 'setup-oauth': - return summary.oauth; - case 'explore-webdav': - return summary.webdav; - case 'setup-cartridges': - // Cartridges step also covers SCAPI; report whichever has more matches, - // preferring cartridges when tied. - return summary.cartridges.matchCount >= summary.scapi.matchCount ? summary.cartridges : summary.scapi; - default: - return null; - } -} diff --git a/packages/b2c-vs-extension/src/walkthrough/telemetry.ts b/packages/b2c-vs-extension/src/walkthrough/telemetry.ts deleted file mode 100644 index 4680ea51d..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/telemetry.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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 vscode from 'vscode'; - -/** - * Walkthrough telemetry and performance tracking. - * Note: This is a basic implementation. For production, consider using - * a proper telemetry service like Application Insights or VS Code's telemetry API. - */ - -interface WalkthroughMetrics { - commandExecutions: Map; - commandDurations: Map; - stepCompletions: Map; - errors: Array<{command: string; error: string; timestamp: Date}>; -} - -class WalkthroughTelemetry { - private metrics: WalkthroughMetrics = { - commandExecutions: new Map(), - commandDurations: new Map(), - stepCompletions: new Map(), - errors: [], - }; - - private log: vscode.OutputChannel; - - constructor(log: vscode.OutputChannel) { - this.log = log; - } - - /** - * Track command execution start time - */ - startCommand(commandId: string): () => void { - const startTime = Date.now(); - - // Increment execution count - const count = this.metrics.commandExecutions.get(commandId) || 0; - this.metrics.commandExecutions.set(commandId, count + 1); - - // Return a function to call when command completes - return () => { - const duration = Date.now() - startTime; - - // Store duration - const durations = this.metrics.commandDurations.get(commandId) || []; - durations.push(duration); - this.metrics.commandDurations.set(commandId, durations); - - this.log.appendLine(`[Telemetry] Command '${commandId}' completed in ${duration}ms`); - }; - } - - /** - * Track step completion - */ - trackStepCompletion(stepId: string): void { - this.metrics.stepCompletions.set(stepId, new Date()); - this.log.appendLine(`[Telemetry] Step '${stepId}' completed`); - } - - /** - * Track error - */ - trackError(commandId: string, error: Error | string): void { - const errorMessage = error instanceof Error ? error.message : error; - this.metrics.errors.push({ - command: commandId, - error: errorMessage, - timestamp: new Date(), - }); - this.log.appendLine(`[Telemetry] Error in '${commandId}': ${errorMessage}`); - } - - /** - * Get average duration for a command - */ - getAverageDuration(commandId: string): number | null { - const durations = this.metrics.commandDurations.get(commandId); - if (!durations || durations.length === 0) { - return null; - } - - const sum = durations.reduce((a, b) => a + b, 0); - return sum / durations.length; - } - - /** - * Get metrics summary - */ - getSummary(): string { - const lines: string[] = ['=== Walkthrough Telemetry Summary ===', '', 'Command Executions:']; - - for (const [command, count] of this.metrics.commandExecutions) { - const avgDuration = this.getAverageDuration(command); - const avgStr = avgDuration ? `avg: ${avgDuration.toFixed(2)}ms` : 'no timing data'; - lines.push(` ${command}: ${count} executions (${avgStr})`); - } - - lines.push('', 'Step Completions:'); - for (const [step, date] of this.metrics.stepCompletions) { - lines.push(` ${step}: ${date.toISOString()}`); - } - - if (this.metrics.errors.length > 0) { - lines.push('', 'Errors:'); - for (const error of this.metrics.errors) { - lines.push(` [${error.timestamp.toISOString()}] ${error.command}: ${error.error}`); - } - } - - return lines.join('\n'); - } - - /** - * Log summary to output channel - */ - logSummary(): void { - this.log.appendLine(this.getSummary()); - } - - /** - * Reset all metrics - */ - reset(): void { - this.metrics = { - commandExecutions: new Map(), - commandDurations: new Map(), - stepCompletions: new Map(), - errors: [], - }; - this.log.appendLine('[Telemetry] Metrics reset'); - } -} - -let telemetryInstance: WalkthroughTelemetry | null = null; - -/** - * Initialize telemetry - */ -export function initializeTelemetry(log: vscode.OutputChannel): WalkthroughTelemetry { - telemetryInstance = new WalkthroughTelemetry(log); - return telemetryInstance; -} - -/** - * Get telemetry instance - */ -export function getTelemetry(): WalkthroughTelemetry | null { - return telemetryInstance; -} - -/** - * Decorator for tracking command execution time - */ -export function trackCommand(commandId: string): MethodDecorator { - return function ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, - ) { - const originalMethod = descriptor.value; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - descriptor.value = async function (...args: any[]) { - const telemetry = getTelemetry(); - if (!telemetry) { - return originalMethod.apply(this, args); - } - - const endTracking = telemetry.startCommand(commandId); - - try { - const result = await originalMethod.apply(this, args); - endTracking(); - return result; - } catch (error) { - telemetry.trackError(commandId, error as Error); - endTracking(); - throw error; - } - }; - - return descriptor; - }; -} diff --git a/packages/b2c-vs-extension/src/walkthrough/test/commands.test.ts b/packages/b2c-vs-extension/src/walkthrough/test/commands.test.ts deleted file mode 100644 index b50f0c8cc..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/test/commands.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 vscode from 'vscode'; -import * as path from 'path'; -import * as fs from 'fs/promises'; - -suite('Walkthrough Commands Test Suite', () => { - let testWorkspaceUri: vscode.Uri; - let testDwJsonPath: string; - - suiteSetup(async () => { - // Get the test workspace folder - const workspaceFolders = vscode.workspace.workspaceFolders; - assert.ok(workspaceFolders && workspaceFolders.length > 0, 'Test workspace should be open'); - - testWorkspaceUri = workspaceFolders[0].uri; - testDwJsonPath = path.join(testWorkspaceUri.fsPath, 'dw.json'); - }); - - setup(async () => { - // Clean up any existing dw.json before each test - try { - await fs.unlink(testDwJsonPath); - } catch { - // File doesn't exist, that's fine - } - }); - - teardown(async () => { - // Clean up after each test - try { - await fs.unlink(testDwJsonPath); - } catch { - // File doesn't exist, that's fine - } - }); - - suite('b2c-dx.walkthrough.open', () => { - test('should open walkthrough without errors', async () => { - // Execute the command - await vscode.commands.executeCommand('b2c-dx.walkthrough.open'); - - // Give it a moment to execute - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // If we got here without throwing, the command executed - assert.ok(true, 'Walkthrough open command executed'); - }); - - test('should be registered in command palette', async () => { - const commands = await vscode.commands.getCommands(); - assert.ok(commands.includes('b2c-dx.walkthrough.open'), 'Command should be registered'); - }); - }); - - suite('b2c-dx.walkthrough.createDwJson', () => { - test('should be registered in command palette', async () => { - const commands = await vscode.commands.getCommands(); - assert.ok(commands.includes('b2c-dx.walkthrough.createDwJson'), 'Command should be registered'); - }); - - // Note: Full integration testing of createDwJson requires user interaction - // (QuickPick dialogs), so we test the command registration here. - // Manual testing covers the full user interaction flow. - }); - - suite('dw.json file operations', () => { - test('should detect when dw.json exists', async () => { - // Create a test dw.json - const testContent = JSON.stringify( - { - hostname: 'test.demandware.net', - username: 'test', - password: 'test', - }, - null, - 2, - ); - - await fs.writeFile(testDwJsonPath, testContent, 'utf-8'); - - // Verify file exists - try { - await fs.access(testDwJsonPath); - assert.ok(true, 'dw.json file was created'); - } catch { - assert.fail('dw.json file should exist'); - } - - // Verify content - const content = await fs.readFile(testDwJsonPath, 'utf-8'); - const parsed = JSON.parse(content); - assert.strictEqual(parsed.hostname, 'test.demandware.net'); - }); - - test('should handle missing dw.json gracefully', async () => { - // Ensure file doesn't exist - try { - await fs.unlink(testDwJsonPath); - } catch { - // Already doesn't exist - } - - // Try to access - try { - await fs.access(testDwJsonPath); - assert.fail('dw.json should not exist'); - } catch { - assert.ok(true, 'Correctly detected missing dw.json'); - } - }); - }); - - suite('Walkthrough completion events', () => { - test('should complete Step 2 when dw.json exists', async () => { - // Create dw.json - const testContent = JSON.stringify( - { - hostname: 'test.demandware.net', - username: 'test', - password: 'test', - }, - null, - 2, - ); - - await fs.writeFile(testDwJsonPath, testContent, 'utf-8'); - - // Trigger workspace file change event - await vscode.commands.executeCommand('workbench.action.reloadWindow'); - - // Note: Actual completion tracking is handled by VS Code's walkthrough API - // This test verifies the file exists, which is the completion condition - const exists = await fs - .access(testDwJsonPath) - .then(() => true) - .catch(() => false); - - assert.ok(exists, 'dw.json exists, Step 2 should be completable'); - }); - }); - - suite('Error handling', () => { - test('should handle command execution errors gracefully', async () => { - try { - // Try to execute a non-existent command - await vscode.commands.executeCommand('b2c-dx.nonexistent.command'); - assert.fail('Should have thrown an error'); - } catch (error) { - assert.ok(error, 'Error should be thrown for non-existent command'); - } - }); - }); - - suite('Extension activation', () => { - test('should activate extension in test workspace', async () => { - const extension = vscode.extensions.getExtension('Salesforce.b2c-vs-extension'); - assert.ok(extension, 'Extension should be installed'); - - if (!extension.isActive) { - await extension.activate(); - } - - assert.ok(extension.isActive, 'Extension should be active'); - }); - - test('should have walkthrough commands after activation', async () => { - const extension = vscode.extensions.getExtension('Salesforce.b2c-vs-extension'); - assert.ok(extension, 'Extension should be installed'); - - if (!extension.isActive) { - await extension.activate(); - } - - const commands = await vscode.commands.getCommands(); - assert.ok(commands.includes('b2c-dx.walkthrough.open'), 'Walkthrough open command should be available'); - assert.ok(commands.includes('b2c-dx.walkthrough.createDwJson'), 'Create dw.json command should be available'); - assert.ok(commands.includes('b2c-dx.walkthrough.markAllDone'), 'Mark all done command should be available'); - assert.ok(commands.includes('b2c-dx.cli.verify'), 'CLI verify command should be available'); - }); - }); - - suite('Personas', () => { - test('should expose the four current personas', async () => { - // Lazy import to avoid a hard module load before activation. - const personas = await import('../personas.js'); - const ids = personas.listPersonas().map((p) => p.id); - assert.deepStrictEqual( - ids.sort(), - ['ai-augmented', 'api-integration', 'devops-release', 'storefront'], - 'Persona ids should match the documented set', - ); - }); - }); -}); diff --git a/packages/b2c-vs-extension/src/walkthrough/toolDetection.ts b/packages/b2c-vs-extension/src/walkthrough/toolDetection.ts deleted file mode 100644 index 1793eca6d..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/toolDetection.ts +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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 cp from 'child_process'; - -export interface ToolStatus { - name: string; - installed: boolean; - version?: string; - label: string; -} - -export interface ToolDetectionResult { - node: ToolStatus; - npm: ToolStatus; - homebrew: ToolStatus; - npx: ToolStatus; - b2cCli: ToolStatus; - b2cCliLatest?: string; - b2cCliOutdated?: boolean; -} - -function execVersion(command: string, args: string[]): Promise { - return new Promise((resolve) => { - cp.execFile(command, args, {timeout: 5000}, (err, stdout) => { - if (err) { - resolve(undefined); - return; - } - const output = stdout.toString().trim(); - resolve(output || undefined); - }); - }); -} - -function extractVersion(raw: string | undefined): string | undefined { - if (!raw) return undefined; - const match = raw.match(/(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/); - return match ? match[1] : raw; -} - -export function compareSemver(a: string, b: string): number { - const norm = (v: string) => - v - .split(/[-+]/)[0] - .split('.') - .map((n) => parseInt(n, 10) || 0); - const [aA, aB] = [norm(a), norm(b)]; - for (let i = 0; i < 3; i++) { - if ((aA[i] ?? 0) !== (aB[i] ?? 0)) return (aA[i] ?? 0) - (aB[i] ?? 0); - } - const aPre = a.includes('-'); - const bPre = b.includes('-'); - if (aPre !== bPre) return aPre ? -1 : 1; - return 0; -} - -export async function detectTools(latestCliVersion?: string): Promise { - const [nodeRaw, npmRaw, brewRaw, npxRaw, b2cRaw] = await Promise.all([ - execVersion('node', ['--version']), - execVersion('npm', ['--version']), - execVersion('brew', ['--version']), - execVersion('npx', ['--version']), - execVersion('b2c', ['--version']), - ]); - - const nodeVersion = extractVersion(nodeRaw); - const npmVersion = extractVersion(npmRaw); - const brewVersion = extractVersion(brewRaw); - const npxVersion = extractVersion(npxRaw); - const b2cVersion = extractVersion(b2cRaw); - - let b2cCliOutdated: boolean | undefined; - let b2cCliLatest: string | undefined; - if (b2cVersion && latestCliVersion) { - b2cCliOutdated = compareSemver(b2cVersion, latestCliVersion) < 0; - b2cCliLatest = latestCliVersion; - } - - return { - node: { - name: 'node', - installed: !!nodeVersion, - version: nodeVersion, - label: 'Node.js', - }, - npm: { - name: 'npm', - installed: !!npmVersion, - version: npmVersion, - label: 'npm', - }, - homebrew: { - name: 'homebrew', - installed: !!brewVersion, - version: brewVersion, - label: 'Homebrew', - }, - npx: { - name: 'npx', - installed: !!npxVersion, - version: npxVersion, - label: 'npx', - }, - b2cCli: { - name: 'b2c-cli', - installed: !!b2cVersion, - version: b2cVersion, - label: 'B2C CLI', - }, - b2cCliLatest: b2cCliLatest, - b2cCliOutdated: b2cCliOutdated, - }; -} - -function toolRowHtml(tool: ToolStatus, note?: string): string { - if (tool.installed) { - const extra = note ? `${note}` : ''; - return [ - `
    `, - ``, - `${tool.label}`, - `v${tool.version}`, - extra, - `
    `, - ].join(''); - } - const extra = note ? `${note}` : ''; - return [ - `
    `, - ``, - `${tool.label}`, - `not found`, - extra, - `
    `, - ].join(''); -} - -/** - * Generates styled HTML for the install-cli step. This bypasses the markdown - * renderer to allow colored status indicators and version badges. - */ -export function generateInstallCliHtml(result: ToolDetectionResult): string { - const parts: string[] = []; - - // Scoped styles for tool detection UI - parts.push(``); - - // Intro (title is already shown in the step header — skip h1 to avoid duplication) - parts.push( - `

    The B2C CLI (b2c) drives deploys, log tailing, sandbox management, and more from the terminal. The VS Code extension uses it under the hood for some commands.

    `, - ); - parts.push( - `

    Optional. You can use the extension's Cartridges, WebDAV, and Sandbox views without the CLI. Install it when you want to script the same operations from the terminal or CI.

    `, - ); - - // Prerequisites grid - parts.push(`

    Prerequisites

    `); - parts.push(`
    `); - parts.push(toolRowHtml(result.node, result.node.installed ? undefined : 'required, v22.0.0+')); - parts.push(toolRowHtml(result.npm, result.npm.installed ? 'for global install' : undefined)); - parts.push(toolRowHtml(result.npx, result.npx.installed ? 'for one-off runs' : undefined)); - parts.push(toolRowHtml(result.homebrew, result.homebrew.installed ? 'alt install method' : 'optional')); - parts.push(`
    `); - - // B2C CLI status - parts.push(`

    B2C CLI

    `); - - if (result.b2cCli.installed) { - const ver = result.b2cCli.version ?? 'unknown'; - if (result.b2cCliOutdated && result.b2cCliLatest) { - parts.push(`
    `); - parts.push(`Update available`); - parts.push( - `

    Installed: ${ver} → Latest: ${result.b2cCliLatest}

    `, - ); - parts.push(`

    Run the Update CLI action above to upgrade.

    `); - parts.push(`
    `); - } else { - parts.push(`
    `); - parts.push(`✔ Installed & up to date`); - parts.push(`

    ${ver}${result.b2cCliLatest ? ' (latest)' : ''}

    `); - parts.push( - `

    The CLI is on your PATH and ready to use. Move to the next step or run b2c --version in the terminal to confirm.

    `, - ); - parts.push(`
    `); - } - } else { - parts.push(`
    `); - parts.push(`✗ Not found on PATH`); - parts.push(`

    Install using one of the methods below, then click Re-check above.

    `); - parts.push(`
    `); - - parts.push(`

    Install

    `); - parts.push(`

    Pick whichever fits your toolchain:

    `); - parts.push(`
      `); - parts.push(`
    • npmnpm install -g @salesforce/b2c-cli
    • `); - parts.push( - `
    • Homebrewbrew install salesforcecommercecloud/tools/b2c-cli
    • `, - ); - parts.push(`
    • npx (no install)npx @salesforce/b2c-cli --help
    • `); - parts.push(`
    `); - parts.push(`

    After installing, click Re-check above to confirm detection.

    `); - } - - // What it unlocks - parts.push(`

    What it unlocks

    `); - parts.push(`
      `); - parts.push(`
    • b2c code:deploy — same flow the Cartridges view uses, scriptable from CI.
    • `); - parts.push(`
    • b2c sandbox:* — create/start/stop/delete sandboxes from the terminal.
    • `); - parts.push(`
    • b2c log:tail — stream instance logs.
    • `); - parts.push(`
    • b2c auth:* — non-interactive OAuth client login for pipelines.
    • `); - parts.push(`
    `); - - // Troubleshooting - parts.push(`

    Troubleshooting

    `); - parts.push(`
      `); - parts.push( - `
    • Command not found after npm install -g — your global npm prefix isn't on PATH. Run npm config get prefix and add <prefix>/bin to PATH.
    • `, - ); - parts.push( - `
    • EACCES on install — use a Node version manager (nvm, fnm, volta) instead of sudo npm. Avoid sudo.
    • `, - ); - parts.push( - `
    • Old version behaves oddly — run Update CLI (or npm install -g @salesforce/b2c-cli@latest) to upgrade.
    • `, - ); - parts.push(`
    `); - - parts.push( - `

    Full installation guide on the docs site

    `, - ); - - return parts.join('\n'); -} diff --git a/packages/b2c-vs-extension/src/walkthrough/validator.ts b/packages/b2c-vs-extension/src/walkthrough/validator.ts deleted file mode 100644 index bac4dbe11..000000000 --- a/packages/b2c-vs-extension/src/walkthrough/validator.ts +++ /dev/null @@ -1,348 +0,0 @@ -/* - * 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 vscode from 'vscode'; -import * as fs from 'fs/promises'; -import * as path from 'path'; - -/** - * Validation utilities for walkthrough configuration. - * Ensures package.json walkthrough configuration is correct. - */ - -interface ValidationResult { - valid: boolean; - errors: string[]; - warnings: string[]; - info: string[]; -} - -interface WalkthroughConfig { - id?: string; - title?: string; - description?: string; - steps?: StepConfig[]; - [key: string]: unknown; -} - -type ThemedImage = {light?: string; dark?: string; hc?: string; hcLight?: string}; - -interface StepConfig { - id?: string; - title?: string; - description?: string; - media?: { - markdown?: string; - image?: string | ThemedImage; - svg?: string; - altText?: string; - [key: string]: unknown; - }; - completionEvents?: string[]; - [key: string]: unknown; -} - -interface PackageJsonConfig { - contributes?: { - walkthroughs?: WalkthroughConfig[]; - commands?: Array<{id?: string; title?: string; [key: string]: unknown}>; - [key: string]: unknown; - }; - [key: string]: unknown; -} - -/** - * Validate walkthrough configuration in package.json - */ -export async function validateWalkthroughConfiguration(extensionPath: string): Promise { - const result: ValidationResult = { - valid: true, - errors: [], - warnings: [], - info: [], - }; - - try { - // Read package.json - const packageJsonPath = path.join(extensionPath, 'package.json'); - const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8'); - const packageJson = JSON.parse(packageJsonContent); - - // Check if walkthroughs exist - if (!packageJson.contributes?.walkthroughs) { - result.valid = false; - result.errors.push('No walkthroughs defined in package.json'); - return result; - } - - const walkthroughs = packageJson.contributes.walkthroughs; - - // Validate each walkthrough - for (const walkthrough of walkthroughs) { - await validateWalkthrough(walkthrough, extensionPath, result); - } - - // Validate commands exist - await validateCommands(packageJson, result); - } catch (error) { - result.valid = false; - const message = error instanceof Error ? error.message : String(error); - result.errors.push(`Failed to validate: ${message}`); - } - - return result; -} - -/** - * Validate individual walkthrough - */ -async function validateWalkthrough( - walkthrough: WalkthroughConfig, - extensionPath: string, - result: ValidationResult, -): Promise { - const walkthroughId = walkthrough.id || 'unknown'; - - // Check required fields - if (!walkthrough.id) { - result.errors.push('Walkthrough missing required field: id'); - result.valid = false; - } - - if (!walkthrough.title) { - result.errors.push(`Walkthrough "${walkthroughId}" missing required field: title`); - result.valid = false; - } - - if (!walkthrough.description) { - result.warnings.push(`Walkthrough "${walkthroughId}" missing description`); - } - - // Check steps - if (!walkthrough.steps || walkthrough.steps.length === 0) { - result.errors.push(`Walkthrough "${walkthroughId}" has no steps`); - result.valid = false; - return; - } - - result.info.push(`Walkthrough "${walkthroughId}" has ${walkthrough.steps.length} steps`); - - // Validate each step - for (let i = 0; i < walkthrough.steps.length; i++) { - const step = walkthrough.steps[i]; - await validateStep(step, i + 1, walkthroughId, extensionPath, result); - } -} - -/** - * Validate individual step - */ -async function validateStep( - step: StepConfig, - stepNumber: number, - walkthroughId: string, - extensionPath: string, - result: ValidationResult, -): Promise { - const stepId = step.id || `step-${stepNumber}`; - - // Check required fields - if (!step.id) { - result.errors.push(`Step ${stepNumber} in "${walkthroughId}" missing id`); - result.valid = false; - } - - if (!step.title) { - result.errors.push(`Step "${stepId}" missing title`); - result.valid = false; - } - - if (!step.description) { - result.warnings.push(`Step "${stepId}" missing description`); - } - - // Check media - if (!step.media) { - result.warnings.push(`Step "${stepId}" has no media (markdown or image)`); - } else { - // Validate media files exist - if (step.media.markdown) { - const mediaPath = path.join(extensionPath, step.media.markdown); - try { - await fs.access(mediaPath); - result.info.push(`✓ Step "${stepId}": markdown file exists`); - } catch { - result.errors.push(`Step "${stepId}": markdown file not found: ${step.media.markdown}`); - result.valid = false; - } - } - - if (step.media.image) { - const imagePaths = - typeof step.media.image === 'string' - ? [step.media.image] - : Object.values(step.media.image).filter((v): v is string => typeof v === 'string'); - for (const rel of imagePaths) { - const imagePath = path.join(extensionPath, rel); - try { - await fs.access(imagePath); - result.info.push(`✓ Step "${stepId}": image file exists (${rel})`); - } catch { - result.warnings.push(`Step "${stepId}": image file not found: ${rel}`); - } - } - if (!step.media.altText) { - result.warnings.push(`Step "${stepId}": image has no altText (accessibility issue)`); - } - } - - if (step.media.svg) { - const svgPath = path.join(extensionPath, step.media.svg); - try { - await fs.access(svgPath); - result.info.push(`✓ Step "${stepId}": svg file exists`); - if (!step.media.altText) { - result.warnings.push(`Step "${stepId}": svg has no altText (accessibility issue)`); - } - } catch { - result.errors.push(`Step "${stepId}": svg file not found: ${step.media.svg}`); - result.valid = false; - } - } - } - - // Validate completion events - if (step.completionEvents && step.completionEvents.length > 0) { - result.info.push(`Step "${stepId}" has ${step.completionEvents.length} completion event(s)`); - - for (const event of step.completionEvents) { - validateCompletionEvent(event, stepId, result); - } - } else { - result.info.push(`Step "${stepId}" has no completion events (manual completion)`); - } - - // Check description for command links - if (step.description) { - const commandLinks = step.description.match(/command:[\w.-]+/g) || []; - if (commandLinks.length > 0) { - result.info.push(`Step "${stepId}" has ${commandLinks.length} command link(s)`); - } - } -} - -/** - * Validate completion event syntax - */ -function validateCompletionEvent(event: string, stepId: string, result: ValidationResult): void { - const validPrefixes = ['onCommand:', 'onView:', 'onContext:', 'onLink:']; - const hasValidPrefix = validPrefixes.some((prefix) => event.startsWith(prefix)); - - if (!hasValidPrefix) { - result.warnings.push( - `Step "${stepId}": completion event "${event}" doesn't use recognized prefix (${validPrefixes.join(', ')})`, - ); - } - - // Check specific event types - if (event.startsWith('onCommand:')) { - const commandId = event.substring('onCommand:'.length); - result.info.push(`Step "${stepId}" completes on command: ${commandId}`); - } else if (event.startsWith('onView:')) { - const viewId = event.substring('onView:'.length); - result.info.push(`Step "${stepId}" completes on view open: ${viewId}`); - } else if (event.startsWith('onContext:')) { - const contextKey = event.substring('onContext:'.length); - result.info.push(`Step "${stepId}" completes on context: ${contextKey}`); - } -} - -/** - * Validate walkthrough commands are registered - */ -async function validateCommands(packageJson: PackageJsonConfig, result: ValidationResult): Promise { - const commands = packageJson.contributes?.commands || []; - const commandIds = commands.map((cmd) => cmd.id); - - const expectedCommands = [ - 'b2c-dx.walkthrough.open', - 'b2c-dx.walkthrough.createDwJson', - 'b2c-dx.walkthrough.markAllDone', - 'b2c-dx.cli.verify', - 'b2c-dx.cli.update', - 'b2c-dx.walkthrough.chooseCredentialStorage', - 'b2c-dx.walkthrough.inspectSetup', - 'b2c-dx.setup.connection', - 'b2c-dx.setup.oauth', - 'b2c-dx.setup.webdav', - 'b2c-dx.setup.scapi', - 'b2c-dx.setup.resetSession', - ]; - - for (const expectedCommand of expectedCommands) { - if (commandIds.includes(expectedCommand)) { - result.info.push(`✓ Command registered: ${expectedCommand}`); - } else { - result.errors.push(`Command not registered: ${expectedCommand}`); - result.valid = false; - } - } -} - -/** - * Format validation result for display - */ -export function formatValidationResult(result: ValidationResult): string { - const lines: string[] = ['=== Walkthrough Configuration Validation ===', '']; - - if (result.valid) { - lines.push('✅ Configuration is valid!'); - } else { - lines.push('❌ Configuration has errors'); - } - - lines.push(''); - - if (result.errors.length > 0) { - lines.push('Errors:'); - result.errors.forEach((error) => lines.push(` ❌ ${error}`)); - lines.push(''); - } - - if (result.warnings.length > 0) { - lines.push('Warnings:'); - result.warnings.forEach((warning) => lines.push(` ⚠️ ${warning}`)); - lines.push(''); - } - - if (result.info.length > 0) { - lines.push('Info:'); - result.info.forEach((info) => lines.push(` ℹ️ ${info}`)); - } - - return lines.join('\n'); -} - -/** - * VS Code command to validate walkthrough configuration - */ -export async function validateWalkthroughCommand(extensionPath: string, log: vscode.OutputChannel): Promise { - log.appendLine('Validating walkthrough configuration...'); - - const result = await validateWalkthroughConfiguration(extensionPath); - const report = formatValidationResult(result); - - log.appendLine(report); - log.show(); - - if (result.valid) { - vscode.window.showInformationMessage('✅ Walkthrough configuration is valid!'); - } else { - vscode.window.showErrorMessage( - `Walkthrough configuration has ${result.errors.length} error(s). Check Output > B2C DX for details.`, - ); - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c763c5c9..e1a616c45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -415,6 +415,9 @@ importers: mocha: specifier: 'catalog:' version: 11.7.5 + msw: + specifier: 'catalog:' + version: 2.12.4(@types/node@22.19.0)(typescript@5.9.3) oclif: specifier: 'catalog:' version: 4.22.44(@types/node@22.19.0) From 1d484bf1f1cf435d3a1cea23ec9db304b99e20c9 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 18 Sep 2026 12:32:49 -0400 Subject: [PATCH 2/3] docs: explain extension-managed MCP installation --- docs/_partials/mcp-setup-cursor.md | 23 ++++++++++++++++++++--- docs/_partials/mcp-setup-vscode.md | 22 ++++++++++++++++++++-- docs/mcp/configuration.md | 11 +++++++++-- docs/mcp/index.md | 10 +++++++--- docs/public/llms.txt | 6 ++++-- 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/docs/_partials/mcp-setup-cursor.md b/docs/_partials/mcp-setup-cursor.md index 1ac52961e..c0ce4e6cd 100644 --- a/docs/_partials/mcp-setup-cursor.md +++ b/docs/_partials/mcp-setup-cursor.md @@ -1,9 +1,23 @@ - +**Use the IDE Extension** Recommended + +Install the [B2C IDE Extension](/vscode-extension/), then open your project in a +trusted workspace. The extension automatically registers **salesforce-b2c-commerce** +with Cursor. Enable the server and its tools in Cursor's MCP settings if prompted. +The default launcher needs Node.js 22 or later and `npx` on the extension host. + +This one server includes the B2C Commerce tools and live IDE context: your selected +instance and code-sync status. No `.cursor/mcp.json` entry or context URL is needed. +See [AI Chat settings](/vscode-extension/configuration#ai-chat). -Reload the MCP server in Cursor after installation. +If you already installed the B2C MCP through a plugin or configuration file, +disable or remove that duplicate entry when using the extension-managed server. +To keep your existing installation instead, set `b2c-dx.mcp.enabled` to `false`; +that also disables Cursor's live IDE-context connection.
    -Manual MCP setup +Install MCP without the IDE Extension + + Add this to `.cursor/mcp.json` in your project: @@ -11,6 +25,9 @@ Add this to `.cursor/mcp.json` in your project: For all projects, use `~/.cursor/mcp.json` instead. +Reload the MCP server in Cursor after installation. This standalone installation +does not receive the extension's live instance selection or code-sync status. +
    See [Cursor's MCP documentation](https://cursor.com/docs/context/mcp). diff --git a/docs/_partials/mcp-setup-vscode.md b/docs/_partials/mcp-setup-vscode.md index 327f7ce19..2ff59a620 100644 --- a/docs/_partials/mcp-setup-vscode.md +++ b/docs/_partials/mcp-setup-vscode.md @@ -1,15 +1,33 @@ - +**Use the IDE Extension** Recommended + +Install the [B2C IDE Extension](/vscode-extension/), then open your project in a +trusted workspace. The extension provides the **B2C Commerce** MCP server to +VS Code. Enable the server and its tools in chat; no `.vscode/mcp.json` entry is +needed. The default launcher needs Node.js 22 or later and `npx` on the extension host. + +Attach **#b2cContext** in chat to include the selected instance and live code-sync +status. See [AI Chat settings](/vscode-extension/configuration#ai-chat). + +If you already installed the B2C MCP through a plugin or configuration file, +disable or remove that duplicate entry when using the extension-managed server. +To keep your existing installation instead, set `b2c-dx.mcp.enabled` to `false`; +the native **#b2cContext** tool remains available. -**Install the plugin** Recommended +
    +Install the plugin without the IDE Extension 1. Open the Command Palette (`Cmd/Ctrl+Shift+P`) and run **Chat: Install Plugin from Source**. 2. Enter `SalesforceCommerceCloud/b2c-developer-tooling`. 3. Select **b2c-dx-mcp** and follow the installation prompts. 4. Start a new chat in GitHub Copilot. +
    +
    Manual MCP setup + + Add this to `.vscode/mcp.json` in your workspace: ```json diff --git a/docs/mcp/configuration.md b/docs/mcp/configuration.md index 56a441b7e..646e4b239 100644 --- a/docs/mcp/configuration.md +++ b/docs/mcp/configuration.md @@ -41,8 +41,15 @@ connection for reading the selected instance and live code-sync status. VS Code provides the same context through the extension's native chat tool. The optional `--ide-context-url` launch flag and `SFCC_IDE_CONTEXT_TOKEN` -environment variable are supplied together by the extension. They identify one -running editor window and are not project configuration to save or share. The +environment variable are supplied together by the extension. It starts a private +HTTP endpoint on an OS-assigned loopback port inside the extension host, then +passes that endpoint's URL and a generated authentication token when registering +the Commerce MCP process with Cursor. This is one MCP server with a connection +back to the extension, not a second MCP registration. You do not generate the URL +or add it to your MCP configuration yourself. + +The URL and token identify one running editor window and are not project +configuration to save or share. The MCP process must run on the same host as the extension. Without that connection, the MCP server does not expose IDE context. Restarting the editor requires a new connection; a failed connection never substitutes the shared default instance. diff --git a/docs/mcp/index.md b/docs/mcp/index.md index f518bb3aa..e824ef74e 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -16,8 +16,10 @@ connected tasks use your existing [B2C configuration](../guide/configuration). ## Set up your assistant {#setup} -Choose your assistant. **Plugin installation is recommended where supported**; -manual setup includes the same tools, documentation, and skills. +Choose your assistant. In VS Code and Cursor, the **IDE Extension provides the MCP +registration** and live editor context. For other compatible clients, plugin +installation is recommended. Manual setup includes the same B2C Commerce tools, +documentation, and skills, without the extension's live context. Our plugins use the open [Agent Plugins standard](https://agent-plugins.org/). @@ -211,7 +213,9 @@ For clients with a command array, such as OpenCode, use ## Updates and customization Use your client's plugin update controls to update a plugin installation, then -start a new session. For direct installations, `@latest` follows the current npm +start a new session. Extension-managed installations use the MCP version matched +to the IDE Extension; update the extension to update that version. +For direct installations, `@latest` follows the current npm release; use a specific version when your team needs a fixed version. The default installation includes all toolsets. If you want a smaller selection diff --git a/docs/public/llms.txt b/docs/public/llms.txt index 6facd3d0c..4e747e493 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -25,8 +25,10 @@ version constraints; use `@latest` for a new installation rather than a memorize ## Connect the MCP -Use the existing B2C MCP connection when available. For a new connection, prefer -the plugin on compatible clients: +Use the existing B2C MCP connection when available. In VS Code and Cursor, the +B2C IDE Extension registers the server and provides live editor context; do not +add a duplicate plugin or manual MCP entry. See [AI Chat settings](vscode-extension/configuration.md#ai-chat). +For other compatible clients, prefer the plugin: - Marketplace source: `SalesforceCommerceCloud/b2c-developer-tooling` - Marketplace name: `b2c-developer-tooling` From 43b676e072cd5eca827391cf6c9d8c43e6bb81c0 Mon Sep 17 00:00:00 2001 From: Charles Lavery Date: Fri, 18 Sep 2026 13:09:58 -0400 Subject: [PATCH 3/3] docs: clarify chat behavior across editors --- docs/vscode-extension/configuration.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/vscode-extension/configuration.md b/docs/vscode-extension/configuration.md index 0cc6d03a0..236a940fa 100644 --- a/docs/vscode-extension/configuration.md +++ b/docs/vscode-extension/configuration.md @@ -111,7 +111,7 @@ For named entries, setting the default writes `active: true`; a root configurati The extension makes the **B2C Commerce MCP server** available in VS Code and Cursor without creating an MCP configuration file. In a trusted workspace, enable the server and its tools in your editor's chat settings. The default launcher requires Node.js 22 or later and `npx` on the extension host's PATH; it downloads the MCP version matched to the extension. Remote workspaces need these prerequisites on the remote host. -In VS Code, attach **#b2cContext** to ask about the selected instance or code-sync status. In Cursor, the registered **salesforce-b2c-commerce** server exposes the same context through an optional connection to the extension. Context is read live from the editor window and includes connection metadata, never credentials. +Chat can check the selected instance and live code-sync status in your editor window. Shared context includes connection details, never credentials. @@ -121,7 +121,12 @@ In VS Code, attach **#b2cContext** to ask about the selected instance or code-sy Assistants can use the current IDE selection unless you specify another target. The context includes the project root, configuration file, instance name, hostname, configured code version, and whether code sync is actually running. When active, code sync reports its upload hostname and code version separately. -This is guidance for the assistant, not an enforced binding of every operation to the status bar. Server launch defaults reflect the IDE selection at discovery/startup. After switching instances, the context tool reports the new selection; refresh/restart the Commerce MCP server if you need its launch defaults refreshed. Updating registration in Cursor can restart its server. Existing debug and log sessions do not move to the newly selected instance; a server restart ends those sessions. Environment overrides and credentials available only in the editor can also cause MCP resolution to differ. +Both editors let your assistant check the current selection and code-sync status: + +- **VS Code:** attach **#b2cContext** to your chat. +- **Cursor:** ask your assistant to check the selected B2C instance; the extension-provided MCP server includes this capability without additional setup. + +After switching instances, ask the assistant to check your selection again. Explicit targets in your request take precedence. Existing debug and log sessions stay on their original instance; restarting MCP ends those sessions. In VS Code, restart MCP if it still uses the previous default instance. Cursor may restart it when you change instances. The native context tool is specific to VS Code chat integrations that consume extension tools. Cursor uses its own MCP registration API. Other assistants sharing the directory do not automatically inherit editor context. In remote workspaces, the MCP process and extension host must run on the same host.