diff --git a/.changeset/prophet-ide-integration.md b/.changeset/prophet-ide-integration.md new file mode 100644 index 00000000..4cc2baae --- /dev/null +++ b/.changeset/prophet-ide-integration.md @@ -0,0 +1,5 @@ +--- +'@salesforce/b2c-cli': minor +--- + +Add `b2c setup ide prophet` to generate a Prophet-compatible `dw.js` script from resolved CLI configuration (including plugin-resolved values), plus new IDE integration docs and setup command reference. diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 75eccd0b..39f546a7 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -49,6 +49,7 @@ const guideSidebar = [ items: [ {text: 'Authentication Setup', link: '/guide/authentication'}, {text: 'Account Manager', link: '/guide/account-manager'}, + {text: 'IDE Integration', link: '/guide/ide-integration'}, {text: 'Scaffolding', link: '/guide/scaffolding'}, {text: 'Security', link: '/guide/security'}, {text: 'Storefront Next', link: '/guide/storefront-next'}, diff --git a/docs/cli/setup.md b/docs/cli/setup.md index 81885e4f..007a261e 100644 --- a/docs/cli/setup.md +++ b/docs/cli/setup.md @@ -1,10 +1,10 @@ --- -description: Commands for viewing configuration, installing AI agent skills, and setting up the development environment. +description: Commands for viewing configuration, managing instances, installing AI agent skills, and generating IDE integration scripts. --- # Setup Commands -Commands for viewing configuration and setting up the development environment. +Commands for viewing configuration, setting up the development environment, and generating IDE integration scripts. ## b2c setup inspect @@ -20,10 +20,10 @@ b2c setup inspect [FLAGS] ### Flags -| Flag | Description | Default | -|------|-------------|---------| +| Flag | Description | Default | +| ---------- | ------------------------------------------------------------- | ------- | | `--unmask` | Show sensitive values unmasked (passwords, secrets, API keys) | `false` | -| `--json` | Output results as JSON | `false` | +| `--json` | Output results as JSON | `false` | ### Examples @@ -105,6 +105,76 @@ Use `--unmask` to reveal the actual values when needed for debugging. - [Configuration Guide](/guide/configuration) - How to configure the CLI +## b2c setup ide + +Show help for IDE integration setup commands. + +### Usage + +```bash +b2c setup ide +``` + +### Examples + +```bash +# Show setup ide subcommands +b2c setup ide --help + +# Generate Prophet integration script +b2c setup ide prophet +``` + +## b2c setup ide prophet + +Generate a `dw.js` script for the [Prophet VS Code extension](https://marketplace.visualstudio.com/items?itemName=SqrTT.prophet). + +The script runs `b2c setup inspect --json --unmask` at runtime and maps the resolved configuration into a `dw.json`-compatible structure that Prophet can consume. + +### Usage + +```bash +b2c setup ide prophet [FLAGS] +``` + +### Flags + +| Flag | Description | Default | +| ---------------- | ------------------------------------------ | ------- | +| `--output`, `-o` | Path for generated script file | `dw.js` | +| `--force`, `-f` | Overwrite output file if it already exists | `false` | +| `--json` | Output results as JSON | `false` | + +### Examples + +```bash +# Generate ./dw.js +b2c setup ide prophet + +# Overwrite existing dw.js +b2c setup ide prophet --force + +# Generate into .vscode folder +b2c setup ide prophet --output .vscode/dw.js + +# Pin generated script to a specific instance context +b2c setup ide prophet --instance staging +``` + +### Output + +The command creates a JavaScript file that: + +1. Executes `setup inspect --json --unmask` +2. Reads resolved config values (including plugin-provided sources) +3. Falls back to loading `dw.json` from `SFCC_CONFIG` or the `dw.js` directory if inspect cannot run +4. Exports the final object via `module.exports = dwJson` +5. Emits Prophet-compatible keys such as: + - `hostname`, `username`, `password` + - `code-version` + - `cartridgesPath`, `siteID`, `storefrontPassword` (when present) +6. Logs diagnostics to both stdout and stderr when resolution fails + ## b2c setup instance list List all configured B2C Commerce instances from dw.json. @@ -117,8 +187,8 @@ b2c setup instance list [FLAGS] ### Flags -| Flag | Description | Default | -|------|-------------|---------| +| Flag | Description | Default | +| -------- | ---------------------- | ------- | | `--json` | Output results as JSON | `false` | ### Examples @@ -156,23 +226,23 @@ b2c setup instance create [NAME] [FLAGS] ### Arguments -| Argument | Description | Required | -|----------|-------------|----------| -| `NAME` | Instance name | Yes (or prompted) | +| Argument | Description | Required | +| -------- | ------------- | ----------------- | +| `NAME` | Instance name | Yes (or prompted) | ### Flags -| Flag | Description | Default | -|------|-------------|---------| -| `--hostname`, `-s` | B2C instance hostname | Prompted | -| `--username` | WebDAV username | | -| `--password` | WebDAV password | Prompted if username set | -| `--client-id` | OAuth client ID | | -| `--client-secret` | OAuth client secret | Prompted if client-id set | -| `--code-version` | Code version | | -| `--active` | Set as active instance | `false` | -| `--force` | Non-interactive mode | `false` | -| `--json` | Output results as JSON | `false` | +| Flag | Description | Default | +| ------------------ | ---------------------- | ------------------------- | +| `--hostname`, `-s` | B2C instance hostname | Prompted | +| `--username` | WebDAV username | | +| `--password` | WebDAV password | Prompted if username set | +| `--client-id` | OAuth client ID | | +| `--client-secret` | OAuth client secret | Prompted if client-id set | +| `--code-version` | Code version | | +| `--active` | Set as active instance | `false` | +| `--force` | Non-interactive mode | `false` | +| `--json` | Output results as JSON | `false` | ### Examples @@ -213,16 +283,16 @@ b2c setup instance remove NAME [FLAGS] ### Arguments -| Argument | Description | Required | -|----------|-------------|----------| -| `NAME` | Instance name to remove | Yes | +| Argument | Description | Required | +| -------- | ----------------------- | -------- | +| `NAME` | Instance name to remove | Yes | ### Flags -| Flag | Description | Default | -|------|-------------|---------| +| Flag | Description | Default | +| --------- | ------------------------ | ------- | | `--force` | Skip confirmation prompt | `false` | -| `--json` | Output results as JSON | `false` | +| `--json` | Output results as JSON | `false` | ### Examples @@ -246,14 +316,14 @@ b2c setup instance set-active NAME [FLAGS] ### Arguments -| Argument | Description | Required | -|----------|-------------|----------| -| `NAME` | Instance name to set as active | Yes | +| Argument | Description | Required | +| -------- | ------------------------------ | -------- | +| `NAME` | Instance name to set as active | Yes | ### Flags -| Flag | Description | Default | -|------|-------------|---------| +| Flag | Description | Default | +| -------- | ---------------------- | ------- | | `--json` | Output results as JSON | `false` | ### Examples @@ -299,34 +369,34 @@ b2c setup skills [SKILLSET] ### Arguments -| Argument | Description | Default | -|----------|-------------|---------| +| Argument | Description | Default | +| ---------- | ---------------------------------------- | ---------------------- | | `SKILLSET` | Skill set to install: `b2c` or `b2c-cli` | Prompted interactively | ### Flags -| Flag | Description | Default | -|------|-------------|---------| -| `--list`, `-l` | List available skills without installing | `false` | -| `--skill` | Install specific skill(s) (can be repeated) | | -| `--ide` | Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, manual | Auto-detect | -| `--global`, `-g` | Install to user home directory (global scope) | `false` | -| `--update`, `-u` | Update existing skills (overwrite) | `false` | -| `--version` | Specific release version | `latest` | -| `--force` | Skip confirmation prompts (non-interactive) | `false` | -| `--json` | Output results as JSON | `false` | +| Flag | Description | Default | +| ---------------- | ----------------------------------------------------------------------------- | ----------- | +| `--list`, `-l` | List available skills without installing | `false` | +| `--skill` | Install specific skill(s) (can be repeated) | | +| `--ide` | Target IDE(s): claude-code, cursor, windsurf, vscode, codex, opencode, manual | Auto-detect | +| `--global`, `-g` | Install to user home directory (global scope) | `false` | +| `--update`, `-u` | Update existing skills (overwrite) | `false` | +| `--version` | Specific release version | `latest` | +| `--force` | Skip confirmation prompts (non-interactive) | `false` | +| `--json` | Output results as JSON | `false` | ### Supported IDEs -| IDE Value | IDE Name | Project Path | Global Path | -|-----------|----------|--------------|-------------| -| `claude-code` | Claude Code | `.claude/skills/` | `~/.claude/skills/` | -| `cursor` | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | -| `windsurf` | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | -| `vscode` | VS Code / GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | -| `codex` | OpenAI Codex CLI | `.codex/skills/` | `~/.codex/skills/` | -| `opencode` | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | -| `manual` | Manual | `.claude/skills/` | `~/.claude/skills/` | +| IDE Value | IDE Name | Project Path | Global Path | +| ------------- | ------------------------ | ------------------- | ----------------------------- | +| `claude-code` | Claude Code | `.claude/skills/` | `~/.claude/skills/` | +| `cursor` | Cursor | `.cursor/skills/` | `~/.cursor/skills/` | +| `windsurf` | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` | +| `vscode` | VS Code / GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` | +| `codex` | OpenAI Codex CLI | `.codex/skills/` | `~/.codex/skills/` | +| `opencode` | OpenCode | `.opencode/skills/` | `~/.config/opencode/skills/` | +| `manual` | Manual | `.claude/skills/` | `~/.claude/skills/` | Use `manual` when you want to install to the Claude Code paths without marketplace recommendations. @@ -390,6 +460,7 @@ claude plugin install b2c ``` The marketplace provides: + - Automatic updates when new versions are released - Centralized plugin management - Version tracking @@ -398,14 +469,15 @@ Use `--ide manual` if you prefer manual installation to the same paths. ### Skill Sets -| Skill Set | Description | -|-----------|-------------| -| `b2c` | B2C Commerce development patterns and practices | -| `b2c-cli` | B2C CLI commands and operations | +| Skill Set | Description | +| --------- | ----------------------------------------------- | +| `b2c` | B2C Commerce development patterns and practices | +| `b2c-cli` | B2C CLI commands and operations | ### Output When installing, the command reports: + - Successfully installed skills with paths - Skipped skills (already exist, use `--update` to overwrite) - Errors encountered during installation @@ -427,10 +499,10 @@ Successfully installed 12 skill(s): Skills are downloaded from the GitHub releases of the [b2c-developer-tooling](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling) repository: -| Artifact | Contents | -|----------|----------| -| `b2c-cli-skills.zip` | Skills for B2C CLI commands and operations | -| `b2c-skills.zip` | Skills for B2C Commerce development patterns | +| Artifact | Contents | +| -------------------- | -------------------------------------------- | +| `b2c-cli-skills.zip` | Skills for B2C CLI commands and operations | +| `b2c-skills.zip` | Skills for B2C Commerce development patterns | Downloaded artifacts are cached locally at: `~/.cache/b2c-cli/skills/{version}/{skillset}/` diff --git a/docs/guide/ide-integration.md b/docs/guide/ide-integration.md new file mode 100644 index 00000000..b5ac4e4e --- /dev/null +++ b/docs/guide/ide-integration.md @@ -0,0 +1,124 @@ +--- +description: Configure IDE tooling like Prophet VS Code extension to consume resolved B2C CLI configuration via a generated dw.js bridge script. +--- + +# IDE Integration + +This guide explains how to connect IDE extensions to your B2C CLI configuration. + +## Prophet VS Code Extension + +[Prophet](https://marketplace.visualstudio.com/items?itemName=SqrTT.prophet) can load `dw.json`-compatible configuration by executing a local `dw.js` script in your working directory. + +### Why Use `dw.js` with B2C CLI + +Using `dw.js` lets Prophet consume the same resolved configuration used by `b2c` commands, including values resolved from: + +- `dw.json` +- environment variables and `.env` +- active instance selection +- configuration plugins registered with the CLI + +The script resolves configuration at runtime by running: + +```bash +b2c setup inspect --json --unmask +``` + +Then it maps the resolved config into Prophet-friendly `dw.json`-style keys. + +If `setup inspect` cannot be executed in the extension runtime, the script falls back to loading `dw.json` from `SFCC_CONFIG` or the `dw.js` directory. + +### Generate `dw.js` Automatically (Recommended) + +```bash +# Generate ./dw.js +b2c setup ide prophet + +# Overwrite an existing file +b2c setup ide prophet --force + +# Write to a custom location +b2c setup ide prophet --output .vscode/dw.js +``` + +### Manual `dw.js` Example + +If you want to author the file yourself, match the same pattern as the generated script: + +```js +var childProcess = require('node:child_process'); +var path = require('node:path'); +var dwJson = {}; + +function toProphetConfig(config) { + if (!config || typeof config !== 'object') return {}; + var codeVersion = config['code-version'] || config.codeVersion || config.version; + return { + hostname: config.hostname || config.server, + username: config.username, + password: config.password, + 'code-version': codeVersion, + version: codeVersion, + cartridgesPath: config.cartridgesPath, + siteID: config.siteID || config.siteId, + storefrontPassword: config.storefrontPassword, + }; +} + +function loadDwConfig() { + try { + var workingDirectory = process.env.SFCC_WORKING_DIRECTORY || __dirname || process.cwd(); + var stdout = childProcess.execFileSync( + 'b2c', + ['setup', 'inspect', '--json', '--unmask', '--working-directory', workingDirectory], + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + cwd: workingDirectory, + }, + ); + + var parsed = JSON.parse(stdout); + var root = parsed && parsed.result && typeof parsed.result === 'object' ? parsed.result : parsed; + var resolved = root && root.config && typeof root.config === 'object' ? root.config : root; + dwJson = toProphetConfig(resolved); + } catch (inspectError) { + try { + var dwJsonPath = process.env.SFCC_CONFIG || path.join(__dirname || process.cwd(), 'dw.json'); + var fallback = require(dwJsonPath); + dwJson = toProphetConfig(fallback); + } catch (fallbackError) { + dwJson = {}; + } + } + + return dwJson; +} + +dwJson = loadDwConfig(); +module.exports = dwJson; +``` + +### Fields Written by `setup ide prophet` + +The generated script maps common CLI fields: + +- `hostname` +- `username` +- `password` +- `code-version` + +It also passes through Prophet-specific fields when available: + +- `cartridgesPath` +- `siteID` (or `siteId`) +- `storefrontPassword` + +### Notes + +- The generated script uses `--unmask` at runtime, so secrets are exposed to Prophet as needed for connection. +- You can regenerate the file any time with `b2c setup ide prophet --force`. +- Dynamic cartridge path discovery can be layered on later. For now, `cartridgesPath` is passed through when available. +- When inspect/fallback resolution fails, the script logs diagnostic messages to both stdout and stderr and exports `{}`. +- The generated script resolves the project root from `SFCC_WORKING_DIRECTORY`, `SFCC_CONFIG`, or the script directory (`__dirname`) before falling back to `process.cwd()`. diff --git a/docs/guide/index.md b/docs/guide/index.md index f634f5da..0390db29 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -30,5 +30,6 @@ See [Installation](./installation) for more options. - [Authentication Setup](./authentication) - Set up Account Manager, OCAPI, and WebDAV - [Configuration](./configuration) - Configure instances and credentials +- [IDE Integration](./ide-integration) - Connect Prophet VS Code to B2C CLI configuration - [CLI Reference](/cli/) - Browse available commands - [API Reference](/api/) - Explore the SDK API diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index 03b8caa0..0c08cfab 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -281,7 +281,12 @@ } }, "setup": { - "description": "Setup commands for development environment\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/setup.html" + "description": "Setup commands for development environment\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/setup.html", + "subtopics": { + "ide": { + "description": "Set up IDE integrations for B2C CLI configuration\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/guide/ide-integration.html" + } + } }, "scaffold": { "description": "Generate project scaffolds for cartridges, APIs, and components\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/scaffold.html" diff --git a/packages/b2c-cli/src/commands/setup/ide/index.ts b/packages/b2c-cli/src/commands/setup/ide/index.ts new file mode 100644 index 00000000..e618d856 --- /dev/null +++ b/packages/b2c-cli/src/commands/setup/ide/index.ts @@ -0,0 +1,23 @@ +/* + * 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 {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {withDocs} from '../../../i18n/index.js'; + +/** + * Default setup ide command - shows topic help for IDE integration subcommands. + */ +export default class SetupIdeIndex extends BaseCommand { + static description = withDocs( + 'Set up IDE integrations that consume B2C CLI configuration', + '/cli/setup.html#b2c-setup-ide', + ); + + static examples = ['<%= config.bin %> setup ide prophet']; + + async run(): Promise { + await this.config.runCommand('help', ['setup', 'ide']); + } +} diff --git a/packages/b2c-cli/src/commands/setup/ide/prophet.ts b/packages/b2c-cli/src/commands/setup/ide/prophet.ts new file mode 100644 index 00000000..2397ad40 --- /dev/null +++ b/packages/b2c-cli/src/commands/setup/ide/prophet.ts @@ -0,0 +1,341 @@ +/* + * 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 'node:fs/promises'; +import {existsSync} from 'node:fs'; +import path from 'node:path'; +import {Flags, ux} from '@oclif/core'; +import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli'; +import {withDocs} from '../../../i18n/index.js'; + +/** + * JSON output structure for setup ide prophet command. + */ +interface SetupIdeProphetResponse { + overwritten: boolean; + path: string; +} + +/** + * Build the dw.js script for Prophet integration. + */ +function buildDwJsScript(inspectArgs: string[]): string { + return `/** + * Auto-generated by "b2c setup ide prophet". + * Purpose: Provide Prophet VS Code extension with dw.json-compatible configuration + * by exporting the resolved B2C CLI config as module.exports. + * support dwJson multi-config in prophet and other tools that support dw.js loading + * when this is present prophet will ignore dw.json and instead load the configuration exported + * This script shells out to: b2c setup inspect --json --unmask + */ +var childProcess = require('node:child_process'); +var path = require('node:path'); +var INSPECT_ARGS = ${JSON.stringify(inspectArgs)}; +var dwJson = {}; + +function logProphetDw(message, error) { + var suffix = error && error.message ? ': ' + String(error.message) : ''; + var line = '[b2c setup ide prophet] ' + message + suffix; + + try { + console.error(line); + } catch (logError) {} + + try { + console.log(line); + } catch (logError) {} +} + +function loadDotEnv() { + try { + require('dotenv').config({override: true}); + } catch (error) { + // optional dependency + } +} + +function getWorkspaceRoot() { + if (process.env.SFCC_WORKING_DIRECTORY && process.env.SFCC_WORKING_DIRECTORY.trim()) { + return process.env.SFCC_WORKING_DIRECTORY.trim(); + } + + if (process.env.SFCC_CONFIG && process.env.SFCC_CONFIG.trim()) { + return path.dirname(process.env.SFCC_CONFIG.trim()); + } + + if (typeof __dirname === 'string' && __dirname) { + return __dirname; + } + + if (typeof module !== 'undefined' && module && module.filename) { + return path.dirname(module.filename); + } + + if (typeof workspace !== 'undefined' && workspace && Array.isArray(workspace.workspaceFolders)) { + for (var i = 0; i < workspace.workspaceFolders.length; i += 1) { + var folder = workspace.workspaceFolders[i]; + if (folder && folder.uri && typeof folder.uri.fsPath === 'string' && folder.uri.fsPath) { + return folder.uri.fsPath; + } + } + } + + return process.cwd(); +} + +function withWorkingDirectory(args, workingDirectory) { + if (!workingDirectory || args.indexOf('--working-directory') !== -1) { + return args.slice(); + } + + return args.concat(['--working-directory', workingDirectory]); +} + +function pickInspectConfig(parsed) { + if (!parsed || typeof parsed !== 'object') { + return {}; + } + + var root = parsed.result && typeof parsed.result === 'object' ? parsed.result : parsed; + + if (root.config && typeof root.config === 'object') { + return root.config; + } + + if (root.result && root.result.config && typeof root.result.config === 'object') { + return root.result.config; + } + + return root; +} + +function runSetupInspect(workingDirectory) { + var inspectArgs = withWorkingDirectory(INSPECT_ARGS, workingDirectory); + var candidates = []; + + if (process.env.B2C_CLI_BIN && process.env.B2C_CLI_BIN.trim()) { + candidates.push({cmd: process.env.B2C_CLI_BIN.trim(), args: inspectArgs}); + } + + candidates.push({cmd: 'b2c', args: inspectArgs}); + candidates.push({cmd: 'npx', args: ['--yes', '@salesforce/b2c-cli'].concat(inspectArgs)}); + + var lastError; + + for (var i = 0; i < candidates.length; i += 1) { + var candidate = candidates[i]; + try { + var execOptions = { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }; + if (workingDirectory) { + execOptions.cwd = workingDirectory; + } + + var stdout = childProcess.execFileSync(candidate.cmd, candidate.args, execOptions); + return pickInspectConfig(JSON.parse(stdout)); + } catch (error) { + logProphetDw('setup inspect candidate failed (' + candidate.cmd + ')', error); + lastError = error; + } + } + + throw lastError || new Error('No setup inspect candidate succeeded'); +} + +function resolveDwJsonConfig(raw) { + if (!raw || typeof raw !== 'object') { + return {}; + } + + var instanceName = process.env.SFCC_INSTANCE; + + if (!instanceName || raw.name !== instanceName) { + if (instanceName && Array.isArray(raw.configs)) { + return raw.configs.find(function (item) { + return item && item.name === instanceName; + }) || raw; + } + + if (Array.isArray(raw.configs) && raw.active !== true) { + return raw.configs.find(function (item) { + return item && item.active === true; + }) || raw; + } + } + + return raw; +} + +function loadDwJsonFallback(workingDirectory) { + try { + var dwJsonPath = process.env.SFCC_CONFIG ? process.env.SFCC_CONFIG : path.join(workingDirectory, 'dw.json'); + if (!path.isAbsolute(dwJsonPath)) { + dwJsonPath = path.resolve(workingDirectory || process.cwd(), dwJsonPath); + } + + return resolveDwJsonConfig(require(dwJsonPath)); + } catch (error) { + logProphetDw('dw.json fallback failed', error); + return {}; + } +} + +function toProphetConfig(config) { + if (!config || typeof config !== 'object') { + return {}; + } + + var result = {}; + var codeVersion = config['code-version'] || config.codeVersion || config.version; + + if (config.hostname || config.server) { + result.hostname = config.hostname || config.server; + } + if (config.username) { + result.username = config.username; + } + if (config.password) { + result.password = config.password; + } + if (codeVersion) { + result['code-version'] = codeVersion; + result.version = codeVersion; + } + if (config.cartridgesPath !== undefined) { + result.cartridgesPath = config.cartridgesPath; + } + if (config.siteID !== undefined || config.siteId !== undefined) { + result.siteID = config.siteID || config.siteId; + } + if (config.storefrontPassword !== undefined) { + result.storefrontPassword = config.storefrontPassword; + } + if (config.cartridge !== undefined) { + result.cartridge = config.cartridge; + } + + return result; +} + +function loadDwConfig() { + loadDotEnv(); + var workingDirectory = getWorkspaceRoot(); + + try { + var inspectConfig = runSetupInspect(workingDirectory); + var inspectMapped = toProphetConfig(inspectConfig); + if (inspectMapped.hostname) { + return inspectMapped; + } + + logProphetDw('setup inspect returned no hostname; falling back to dw.json'); + } catch (error) { + logProphetDw('setup inspect failed; falling back to dw.json', error); + } + + try { + var fallbackMapped = toProphetConfig(loadDwJsonFallback(workingDirectory)); + if (!fallbackMapped.hostname) { + logProphetDw('dw.json fallback returned no hostname'); + } + + return fallbackMapped; + } catch (error) { + logProphetDw('dw.json mapping failed; returning empty config', error); + return {}; + } +} + +try { + dwJson = loadDwConfig(); +} catch (error) { + logProphetDw('unexpected dw.js error; returning empty config', error); + dwJson = {}; +} + +module.exports = dwJson; +`; +} + +/** + * Create dw.js integration script for Prophet VS Code extension. + */ +export default class SetupIdeProphet extends BaseCommand { + static description = withDocs( + 'Generate a dw.js script that exposes B2C CLI config for Prophet VS Code', + '/cli/setup.html#b2c-setup-ide-prophet', + ); + + static enableJsonFlag = true; + + static examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --force', + '<%= config.bin %> <%= command.id %> --output .vscode/dw.js', + '<%= config.bin %> <%= command.id %> --instance staging', + ]; + + static flags = { + ...BaseCommand.baseFlags, + output: Flags.string({ + char: 'o', + description: 'Path for generated script file', + default: 'dw.js', + }), + force: Flags.boolean({ + char: 'f', + description: 'Overwrite output file if it already exists', + default: false, + }), + }; + + async run(): Promise { + const outputPath = path.resolve(this.flags.output); + const alreadyExists = existsSync(outputPath); + + if (alreadyExists && !this.flags.force) { + this.error(`File already exists at ${outputPath}. Use --force to overwrite.`); + } + + const inspectArgs = this.buildInspectArgs(); + const script = buildDwJsScript(inspectArgs); + + await fs.mkdir(path.dirname(outputPath), {recursive: true}); + await fs.writeFile(outputPath, script, 'utf8'); + + const result: SetupIdeProphetResponse = { + path: outputPath, + overwritten: alreadyExists, + }; + + if (!this.jsonEnabled()) { + ux.stdout(`Created ${outputPath}`); + } + + return result; + } + + /** + * Build setup inspect arguments for the generated script. + * Includes setup context flags so runtime resolution matches command intent. + */ + private buildInspectArgs(): string[] { + const args = ['setup', 'inspect', '--json', '--unmask']; + + if (this.flags.instance) { + args.push('--instance', this.flags.instance); + } + if (this.flags.config) { + args.push('--config', this.flags.config); + } + if (this.flags['working-directory']) { + args.push('--working-directory', this.flags['working-directory']); + } + + return args; + } +} diff --git a/packages/b2c-cli/src/commands/setup/index.ts b/packages/b2c-cli/src/commands/setup/index.ts index 2522d015..b3ba3de7 100644 --- a/packages/b2c-cli/src/commands/setup/index.ts +++ b/packages/b2c-cli/src/commands/setup/index.ts @@ -14,9 +14,16 @@ import {withDocs} from '../../i18n/index.js'; * - `b2c setup` with instance configured or non-TTY: shows topic help */ export default class SetupIndex extends BaseCommand { - static description = withDocs('Manage instances, view configuration, and install agent skills', '/cli/setup.html'); + static description = withDocs( + 'Manage instances, inspect configuration, install skills, and configure IDE integrations', + '/cli/setup.html', + ); - static examples = ['<%= config.bin %> setup --help', '<%= config.bin %> setup instance create']; + static examples = [ + '<%= config.bin %> setup --help', + '<%= config.bin %> setup instance create', + '<%= config.bin %> setup ide prophet', + ]; async run(): Promise { const hasInstance = this.resolvedConfig.hasB2CInstanceConfig(); diff --git a/packages/b2c-cli/test/commands/setup/ide/index.test.ts b/packages/b2c-cli/test/commands/setup/ide/index.test.ts new file mode 100644 index 00000000..fd39e0eb --- /dev/null +++ b/packages/b2c-cli/test/commands/setup/ide/index.test.ts @@ -0,0 +1,33 @@ +/* + * 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 sinon from 'sinon'; +import SetupIdeIndex from '../../../../src/commands/setup/ide/index.js'; + +describe('setup ide', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should have a description', () => { + expect(SetupIdeIndex.description).to.be.a('string'); + expect(SetupIdeIndex.description).to.include('IDE'); + }); + + it('should show setup ide help when run', async () => { + const command = new SetupIdeIndex([], {} as any); + const runCommand = sinon.stub().resolves(undefined); + + Object.defineProperty(command, 'config', { + value: {runCommand}, + configurable: true, + }); + + await command.run(); + + expect(runCommand.calledOnceWithExactly('help', ['setup', 'ide'])).to.equal(true); + }); +}); diff --git a/packages/b2c-cli/test/commands/setup/ide/prophet.test.ts b/packages/b2c-cli/test/commands/setup/ide/prophet.test.ts new file mode 100644 index 00000000..3bb5387c --- /dev/null +++ b/packages/b2c-cli/test/commands/setup/ide/prophet.test.ts @@ -0,0 +1,154 @@ +/* + * 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 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import {expect} from 'chai'; +import sinon from 'sinon'; +import SetupIdeProphet from '../../../../src/commands/setup/ide/prophet.js'; +import { + runSilent, + stubCommandConfigAndLogger, + stubJsonEnabled, + makeCommandThrowOnError, +} from '../../../helpers/test-setup.js'; +import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils'; + +describe('setup ide prophet', () => { + let tempDir: string; + + beforeEach(async () => { + isolateConfig(); + tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'b2c-setup-ide-prophet-')); + }); + + afterEach(async () => { + sinon.restore(); + restoreConfig(); + await fs.rm(tempDir, {recursive: true, force: true}); + }); + + function createCommand(flags: Record = {}): SetupIdeProphet { + const command = new SetupIdeProphet([], {} as any); + + (command as any).flags = { + output: path.join(tempDir, 'dw.js'), + force: false, + instance: undefined, + config: undefined, + 'working-directory': undefined, + ...flags, + }; + + stubJsonEnabled(command, true); + stubCommandConfigAndLogger(command); + + return command; + } + + describe('command structure', () => { + it('should have expected metadata', () => { + expect(SetupIdeProphet.description).to.be.a('string'); + expect(SetupIdeProphet.description).to.include('Prophet'); + expect(SetupIdeProphet.enableJsonFlag).to.equal(true); + expect(SetupIdeProphet.flags).to.have.property('output'); + expect(SetupIdeProphet.flags).to.have.property('force'); + }); + }); + + describe('script generation', () => { + it('should create dw.js when no file exists', async () => { + const outputPath = path.join(tempDir, 'dw.js'); + const command = createCommand({output: outputPath}); + + const result = await runSilent(() => command.run()); + const content = await fs.readFile(outputPath, 'utf8'); + + expect(result.path).to.equal(path.resolve(outputPath)); + expect(result.overwritten).to.equal(false); + expect(content).to.not.match(/^#!\/usr\/bin\/env node/m); + expect(content).to.include('b2c setup inspect --json --unmask'); + expect(content).to.include('Purpose: Provide Prophet VS Code extension'); + expect(content).to.include("'code-version'"); + expect(content).to.not.include("'client-id'"); + expect(content).to.not.include("'client-secret'"); + expect(content).to.not.include("'short-code'"); + expect(content).to.not.include("'tenant-id'"); + expect(content).to.include('cartridgesPath'); + expect(content).to.include('siteID'); + expect(content).to.include('storefrontPassword'); + expect(content).to.not.include("'oauth-scopes'"); + expect(content).to.include('module.exports = dwJson;'); + expect(content).to.not.include('process.stdout.write'); + expect(content).to.include('function logProphetDw(message, error)'); + expect(content).to.include('console.error(line);'); + expect(content).to.include('console.log(line);'); + expect(content).to.include('support dwJson multi-config in prophet'); + expect(content).to.include("typeof workspace !== 'undefined'"); + expect(content).to.include("if (typeof __dirname === 'string' && __dirname)"); + expect(content).to.include('process.env.SFCC_WORKING_DIRECTORY'); + expect(content).to.include('try {'); + expect(content).to.include('return {};'); + expect(content).to.include('execOptions.cwd = workingDirectory;'); + expect(content).to.include("path.join(workingDirectory, 'dw.json')"); + expect(content).to.include('path.resolve(workingDirectory || process.cwd(), dwJsonPath);'); + expect(content).to.include('return resolveDwJsonConfig(require(dwJsonPath));'); + expect(content).to.include('setup inspect returned no hostname; falling back to dw.json'); + expect(content).to.include('dw.json fallback returned no hostname'); + }); + + it('should fail if output exists without --force', async () => { + const outputPath = path.join(tempDir, 'dw.js'); + await fs.writeFile(outputPath, 'existing', 'utf8'); + + const command = createCommand({output: outputPath}); + makeCommandThrowOnError(command); + let error: unknown; + try { + await command.run(); + } catch (error_) { + error = error_; + } + + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Use --force to overwrite'); + }); + + it('should overwrite output when --force is used', async () => { + const outputPath = path.join(tempDir, 'dw.js'); + await fs.writeFile(outputPath, 'old-content', 'utf8'); + + const command = createCommand({output: outputPath, force: true}); + + const result = await runSilent(() => command.run()); + const content = await fs.readFile(outputPath, 'utf8'); + + expect(result.overwritten).to.equal(true); + expect(content).to.not.equal('old-content'); + expect(content).to.include('function toProphetConfig(config)'); + }); + + it('should include setup context flags in generated inspect args', async () => { + const outputPath = path.join(tempDir, 'dw.js'); + const command = createCommand({ + output: outputPath, + instance: 'staging', + config: '/tmp/config/dw.json', + 'working-directory': '/tmp/workspace', + }); + + await runSilent(() => command.run()); + const content = await fs.readFile(outputPath, 'utf8'); + + expect(content).to.include('--instance'); + expect(content).to.include('staging'); + expect(content).to.include('--config'); + expect(content).to.include('/tmp/config/dw.json'); + expect(content).to.include('--working-directory'); + expect(content).to.include('/tmp/workspace'); + }); + }); +}); diff --git a/skills/b2c-cli/skills/b2c-config/SKILL.md b/skills/b2c-cli/skills/b2c-config/SKILL.md index 9fa38c7d..f75a0b66 100644 --- a/skills/b2c-cli/skills/b2c-config/SKILL.md +++ b/skills/b2c-cli/skills/b2c-config/SKILL.md @@ -62,6 +62,23 @@ b2c setup inspect --json | jq '.config' b2c setup inspect --json | jq '.sources' ``` +## IDE Integration (Prophet) + +Use `b2c setup ide prophet` to generate a `dw.js` bridge script for the Prophet VS Code extension. + +```bash +# Generate ./dw.js in the current project +b2c setup ide prophet + +# Overwrite existing file +b2c setup ide prophet --force + +# Custom path +b2c setup ide prophet --output .vscode/dw.js +``` + +The generated script runs `b2c setup inspect --json --unmask` at runtime, so Prophet sees the same resolved config as CLI commands, including configuration plugins. It maps values to `dw.json`-style keys and passes through Prophet fields like `cartridgesPath`, `siteID`, and `storefrontPassword` when present. + ## Managing Instances ### List Configured Instances @@ -128,6 +145,7 @@ The `setup inspect` command displays configuration organized by category: - **Sources**: List of all configuration sources that were loaded Each value shows its source in brackets: + - `[DwJsonSource]` - Value from dw.json file - `[MobifySource]` - Value from ~/.mobify file - `[SFCC_*]` - Value from environment variable @@ -151,6 +169,7 @@ When troubleshooting, check the source column to understand which configuration ### Missing Values If a value shows `-`, it means no source provided that configuration. Check: + - Is the field spelled correctly in dw.json? - Is the environment variable set? - Does the plugin provide that value? @@ -158,6 +177,7 @@ If a value shows `-`, it means no source provided that configuration. Check: ### Wrong Source Taking Precedence If a value comes from an unexpected source: + - Higher priority sources override lower ones - Credential groups (username+password, clientId+clientSecret) are atomic - Hostname mismatch protection may discard values