Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/add-setup-config-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@salesforce/b2c-cli': minor
---

Add `setup config` command to display resolved configuration with source tracking.

Shows all configuration values organized by category (Instance, Authentication, SCAPI, MRT) and indicates which source file or environment variable provided each value. Sensitive values are masked by default; use `--unmask` to reveal them.
101 changes: 99 additions & 2 deletions docs/cli/setup.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,107 @@
---
description: Commands for installing AI agent skills for Claude Code, Cursor, Windsurf, and other agentic IDEs.
description: Commands for viewing configuration, installing AI agent skills, and setting up the development environment.
---

# Setup Commands

Commands for setting up the development environment with AI agent skills.
Commands for viewing configuration and setting up the development environment.

## b2c setup config

Display the resolved configuration from all sources, showing which values are set and where they came from. Useful for debugging configuration issues.

### Usage

```bash
b2c setup config [FLAGS]
```

### Flags

| Flag | Description | Default |
|------|-------------|---------|
| `--unmask` | Show sensitive values unmasked (passwords, secrets, API keys) | `false` |
| `--json` | Output results as JSON | `false` |

### Examples

```bash
# Display resolved configuration (sensitive values masked)
b2c setup config

# Display configuration with sensitive values unmasked
b2c setup config --unmask

# Output as JSON for scripting
b2c setup config --json

# Debug configuration with a specific instance
b2c setup config -i staging
```

### Output

The command displays configuration organized by category:

- **Instance**: hostname, webdavHostname, codeVersion
- **Authentication (Basic)**: username, password
- **Authentication (OAuth)**: clientId, clientSecret, scopes, authMethods, accountManagerHost
- **SCAPI**: shortCode
- **Managed Runtime (MRT)**: mrtProject, mrtEnvironment, mrtApiKey, mrtOrigin
- **Metadata**: instanceName
- **Sources**: List of configuration sources that contributed values

Each value shows its source in brackets (e.g., `[dw.json]`, `[SFCC_CLIENT_ID]`, `[~/.mobify]`).

Example output:

```
Configuration
────────────────────────────────────────────────────────────

Instance
hostname my-sandbox.dx.commercecloud.salesforce.com [DwJsonSource]
webdavHostname -
codeVersion version1 [DwJsonSource]

Authentication (Basic)
username admin [DwJsonSource]
password admi...REDACTED [DwJsonSource]

Authentication (OAuth)
clientId my-client-id [password-store]
clientSecret my-c...REDACTED [password-store]
scopes -
authMethods -
accountManagerHost -

SCAPI
shortCode abc123 [DwJsonSource]

Managed Runtime (MRT)
mrtProject my-project [MobifySource]
mrtApiKey mrtk...REDACTED [MobifySource]

Sources
────────────────────────────────────────────────────────────
1. DwJsonSource /path/to/project/dw.json
2. MobifySource /Users/user/.mobify
3. password-store pass:b2c-cli/_default
```

### Sensitive Values

By default, sensitive fields are masked to prevent accidental exposure:

- `password` - Basic auth access key
- `clientSecret` - OAuth client secret
- `mrtApiKey` - MRT API key

Use `--unmask` to reveal the actual values when needed for debugging.

### See Also

- [Configuration Guide](/guide/configuration) - How to configure the CLI

## b2c setup skills

Expand Down
23 changes: 23 additions & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,29 @@ SFCC_AUTH_METHODS=client-credentials,implicit b2c code deploy

The CLI will try each method in order until one succeeds.

## Debugging Configuration

Use `b2c setup config` to view the resolved configuration and see which source provided each value:

```bash
# Display resolved configuration (sensitive values masked)
b2c setup config

# Show actual sensitive values
b2c setup config --unmask

# Output as JSON
b2c setup config --json
```

This command helps troubleshoot issues like:
- Verifying which configuration file is being used
- Checking if environment variables are being read
- Understanding credential source priority
- Identifying hostname mismatch protection triggers

See [setup config](/cli/setup#b2c-setup-config) for full documentation.

## Next Steps

- [CLI Reference](/cli/) - Browse available commands
Expand Down
251 changes: 251 additions & 0 deletions packages/b2c-cli/src/commands/setup/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
* 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 {Flags, ux} from '@oclif/core';
import cliui from 'cliui';
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
import type {NormalizedConfig, ConfigSourceInfo} from '@salesforce/b2c-tooling-sdk/config';

/**
* Sensitive fields that should be masked by default.
*/
const SENSITIVE_FIELDS = new Set<keyof NormalizedConfig>(['clientSecret', 'mrtApiKey', 'password']);

/**
* JSON output structure for the config command.
*/
interface SetupConfigResponse {
config: Record<string, unknown>;
sources: ConfigSourceInfo[];
warnings?: string[];
}

/**
* Mask a sensitive value, showing first 4 characters.
* Matches the pattern used in the logger for consistency.
*/
function maskValue(value: string): string {
if (value.length > 10) {
return `${value.slice(0, 4)}...REDACTED`;
}
return 'REDACTED';
}

/**
* Check if a field is sensitive and should be masked.
*/
function isSensitiveField(field: string): boolean {
return SENSITIVE_FIELDS.has(field as keyof NormalizedConfig);
}

/**
* Get the display value for a config field, applying masking if needed.
*/
function getDisplayValue(field: string, value: unknown, unmask: boolean): string {
if (value === undefined || value === null) {
return '-';
}

if (Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '-';
}

const strValue = String(value);

if (!unmask && isSensitiveField(field)) {
return maskValue(strValue);
}

return strValue;
}

/**
* Command to display resolved configuration.
*/
export default class SetupConfig extends BaseCommand<typeof SetupConfig> {
static description = 'Display resolved configuration';

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --unmask',
'<%= config.bin %> <%= command.id %> --json',
];

static flags = {
...BaseCommand.baseFlags,
unmask: Flags.boolean({
description: 'Show sensitive values unmasked (passwords, secrets, API keys)',
default: false,
}),
};

async run(): Promise<SetupConfigResponse> {
const {values, sources, warnings} = this.resolvedConfig;
const unmask = this.flags.unmask;

// Build output config with masking applied
const outputConfig: Record<string, unknown> = {};
for (const [key, value] of Object.entries(values)) {
if (value !== undefined) {
outputConfig[key] = isSensitiveField(key) && !unmask ? maskValue(String(value)) : value;
}
}

const result: SetupConfigResponse = {
config: outputConfig,
sources,
warnings: warnings.length > 0 ? warnings.map((w) => w.message) : undefined,
};

// JSON mode - just return the data
if (this.jsonEnabled()) {
return result;
}

// Human-readable output
if (unmask) {
this.warn('Sensitive values are displayed unmasked.');
}

this.printConfig(values, sources, unmask);

// Show warnings
for (const warning of warnings) {
this.warn(warning.message);
}

return result;
}

/**
* Build a map of field -> source name for display.
*/
private buildFieldSourceMap(sources: ConfigSourceInfo[]): Map<string, string> {
const resultMap = new Map<string, string>();

// Process sources in order - first source with a field (not ignored) wins
for (const source of sources) {
for (const field of source.fields) {
if (!source.fieldsIgnored?.includes(field) && !resultMap.has(field)) {
resultMap.set(field, source.name);
}
}
}

return resultMap;
}

/**
* Print the configuration in human-readable format.
*/
private printConfig(config: NormalizedConfig, sources: ConfigSourceInfo[], unmask: boolean): void {
const ui = cliui({width: process.stdout.columns || 80});
const fieldSources = this.buildFieldSourceMap(sources);

// Header
ui.div({text: 'Configuration', padding: [1, 0, 0, 0]});
ui.div({text: '─'.repeat(60), padding: [0, 0, 0, 0]});

// Instance section
this.renderSection(
ui,
'Instance',
[
['hostname', config.hostname],
['webdavHostname', config.webdavHostname],
['codeVersion', config.codeVersion],
],
fieldSources,
unmask,
);

// Auth (Basic) section
this.renderSection(
ui,
'Authentication (Basic)',
[
['username', config.username],
['password', config.password],
],
fieldSources,
unmask,
);

// Auth (OAuth) section
this.renderSection(
ui,
'Authentication (OAuth)',
[
['clientId', config.clientId],
['clientSecret', config.clientSecret],
['scopes', config.scopes],
['authMethods', config.authMethods],
['accountManagerHost', config.accountManagerHost],
],
fieldSources,
unmask,
);

// SCAPI section
this.renderSection(ui, 'SCAPI', [['shortCode', config.shortCode]], fieldSources, unmask);

// MRT section
this.renderSection(
ui,
'Managed Runtime (MRT)',
[
['mrtProject', config.mrtProject],
['mrtEnvironment', config.mrtEnvironment],
['mrtApiKey', config.mrtApiKey],
['mrtOrigin', config.mrtOrigin],
],
fieldSources,
unmask,
);

// Metadata section
this.renderSection(ui, 'Metadata', [['instanceName', config.instanceName]], fieldSources, unmask);

// Sources section
if (sources.length > 0) {
ui.div({text: '', padding: [0, 0, 0, 0]});
ui.div({text: 'Sources', padding: [1, 0, 0, 0]});
ui.div({text: '─'.repeat(60), padding: [0, 0, 0, 0]});

for (const [index, source] of sources.entries()) {
ui.div({text: ` ${index + 1}. ${source.name}`, width: 24}, {text: source.location || '-'});
}
}

ux.stdout(ui.toString());
}

/**
* Render a configuration section with fields.
*/
private renderSection(

Check warning on line 230 in packages/b2c-cli/src/commands/setup/config.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

Method 'renderSection' has too many parameters (5). Maximum allowed is 4

Check warning on line 230 in packages/b2c-cli/src/commands/setup/config.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Method 'renderSection' has too many parameters (5). Maximum allowed is 4
ui: ReturnType<typeof cliui>,
title: string,
fields: [string, unknown][],
fieldSources: Map<string, string>,
unmask: boolean,
): void {
ui.div({text: '', padding: [0, 0, 0, 0]});
ui.div({text: title, padding: [0, 0, 0, 0]});

for (const [field, value] of fields) {
const displayValue = getDisplayValue(field, value, unmask);
const source = fieldSources.get(field);

ui.div(
{text: ` ${field}`, width: 22},
{text: displayValue, width: 40},
{text: source ? `[${source}]` : '', padding: [0, 0, 0, 2]},
);
}
}
}
Loading
Loading