Skip to content

Commit 269de20

Browse files
authored
Add setup config command (#62)
* docs on setup * fix: update test to use new helper pattern after main merge
1 parent d6056c2 commit 269de20

File tree

6 files changed

+857
-2
lines changed

6 files changed

+857
-2
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@salesforce/b2c-cli': minor
3+
---
4+
5+
Add `setup config` command to display resolved configuration with source tracking.
6+
7+
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.

docs/cli/setup.md

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,107 @@
11
---
2-
description: Commands for installing AI agent skills for Claude Code, Cursor, Windsurf, and other agentic IDEs.
2+
description: Commands for viewing configuration, installing AI agent skills, and setting up the development environment.
33
---
44

55
# Setup Commands
66

7-
Commands for setting up the development environment with AI agent skills.
7+
Commands for viewing configuration and setting up the development environment.
8+
9+
## b2c setup config
10+
11+
Display the resolved configuration from all sources, showing which values are set and where they came from. Useful for debugging configuration issues.
12+
13+
### Usage
14+
15+
```bash
16+
b2c setup config [FLAGS]
17+
```
18+
19+
### Flags
20+
21+
| Flag | Description | Default |
22+
|------|-------------|---------|
23+
| `--unmask` | Show sensitive values unmasked (passwords, secrets, API keys) | `false` |
24+
| `--json` | Output results as JSON | `false` |
25+
26+
### Examples
27+
28+
```bash
29+
# Display resolved configuration (sensitive values masked)
30+
b2c setup config
31+
32+
# Display configuration with sensitive values unmasked
33+
b2c setup config --unmask
34+
35+
# Output as JSON for scripting
36+
b2c setup config --json
37+
38+
# Debug configuration with a specific instance
39+
b2c setup config -i staging
40+
```
41+
42+
### Output
43+
44+
The command displays configuration organized by category:
45+
46+
- **Instance**: hostname, webdavHostname, codeVersion
47+
- **Authentication (Basic)**: username, password
48+
- **Authentication (OAuth)**: clientId, clientSecret, scopes, authMethods, accountManagerHost
49+
- **SCAPI**: shortCode
50+
- **Managed Runtime (MRT)**: mrtProject, mrtEnvironment, mrtApiKey, mrtOrigin
51+
- **Metadata**: instanceName
52+
- **Sources**: List of configuration sources that contributed values
53+
54+
Each value shows its source in brackets (e.g., `[dw.json]`, `[SFCC_CLIENT_ID]`, `[~/.mobify]`).
55+
56+
Example output:
57+
58+
```
59+
Configuration
60+
────────────────────────────────────────────────────────────
61+
62+
Instance
63+
hostname my-sandbox.dx.commercecloud.salesforce.com [DwJsonSource]
64+
webdavHostname -
65+
codeVersion version1 [DwJsonSource]
66+
67+
Authentication (Basic)
68+
username admin [DwJsonSource]
69+
password admi...REDACTED [DwJsonSource]
70+
71+
Authentication (OAuth)
72+
clientId my-client-id [password-store]
73+
clientSecret my-c...REDACTED [password-store]
74+
scopes -
75+
authMethods -
76+
accountManagerHost -
77+
78+
SCAPI
79+
shortCode abc123 [DwJsonSource]
80+
81+
Managed Runtime (MRT)
82+
mrtProject my-project [MobifySource]
83+
mrtApiKey mrtk...REDACTED [MobifySource]
84+
85+
Sources
86+
────────────────────────────────────────────────────────────
87+
1. DwJsonSource /path/to/project/dw.json
88+
2. MobifySource /Users/user/.mobify
89+
3. password-store pass:b2c-cli/_default
90+
```
91+
92+
### Sensitive Values
93+
94+
By default, sensitive fields are masked to prevent accidental exposure:
95+
96+
- `password` - Basic auth access key
97+
- `clientSecret` - OAuth client secret
98+
- `mrtApiKey` - MRT API key
99+
100+
Use `--unmask` to reveal the actual values when needed for debugging.
101+
102+
### See Also
103+
104+
- [Configuration Guide](/guide/configuration) - How to configure the CLI
8105

9106
## b2c setup skills
10107

docs/guide/configuration.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,29 @@ SFCC_AUTH_METHODS=client-credentials,implicit b2c code deploy
264264

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

267+
## Debugging Configuration
268+
269+
Use `b2c setup config` to view the resolved configuration and see which source provided each value:
270+
271+
```bash
272+
# Display resolved configuration (sensitive values masked)
273+
b2c setup config
274+
275+
# Show actual sensitive values
276+
b2c setup config --unmask
277+
278+
# Output as JSON
279+
b2c setup config --json
280+
```
281+
282+
This command helps troubleshoot issues like:
283+
- Verifying which configuration file is being used
284+
- Checking if environment variables are being read
285+
- Understanding credential source priority
286+
- Identifying hostname mismatch protection triggers
287+
288+
See [setup config](/cli/setup#b2c-setup-config) for full documentation.
289+
267290
## Next Steps
268291

269292
- [CLI Reference](/cli/) - Browse available commands
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
import {Flags, ux} from '@oclif/core';
7+
import cliui from 'cliui';
8+
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
9+
import type {NormalizedConfig, ConfigSourceInfo} from '@salesforce/b2c-tooling-sdk/config';
10+
11+
/**
12+
* Sensitive fields that should be masked by default.
13+
*/
14+
const SENSITIVE_FIELDS = new Set<keyof NormalizedConfig>(['clientSecret', 'mrtApiKey', 'password']);
15+
16+
/**
17+
* JSON output structure for the config command.
18+
*/
19+
interface SetupConfigResponse {
20+
config: Record<string, unknown>;
21+
sources: ConfigSourceInfo[];
22+
warnings?: string[];
23+
}
24+
25+
/**
26+
* Mask a sensitive value, showing first 4 characters.
27+
* Matches the pattern used in the logger for consistency.
28+
*/
29+
function maskValue(value: string): string {
30+
if (value.length > 10) {
31+
return `${value.slice(0, 4)}...REDACTED`;
32+
}
33+
return 'REDACTED';
34+
}
35+
36+
/**
37+
* Check if a field is sensitive and should be masked.
38+
*/
39+
function isSensitiveField(field: string): boolean {
40+
return SENSITIVE_FIELDS.has(field as keyof NormalizedConfig);
41+
}
42+
43+
/**
44+
* Get the display value for a config field, applying masking if needed.
45+
*/
46+
function getDisplayValue(field: string, value: unknown, unmask: boolean): string {
47+
if (value === undefined || value === null) {
48+
return '-';
49+
}
50+
51+
if (Array.isArray(value)) {
52+
return value.length > 0 ? value.join(', ') : '-';
53+
}
54+
55+
const strValue = String(value);
56+
57+
if (!unmask && isSensitiveField(field)) {
58+
return maskValue(strValue);
59+
}
60+
61+
return strValue;
62+
}
63+
64+
/**
65+
* Command to display resolved configuration.
66+
*/
67+
export default class SetupConfig extends BaseCommand<typeof SetupConfig> {
68+
static description = 'Display resolved configuration';
69+
70+
static enableJsonFlag = true;
71+
72+
static examples = [
73+
'<%= config.bin %> <%= command.id %>',
74+
'<%= config.bin %> <%= command.id %> --unmask',
75+
'<%= config.bin %> <%= command.id %> --json',
76+
];
77+
78+
static flags = {
79+
...BaseCommand.baseFlags,
80+
unmask: Flags.boolean({
81+
description: 'Show sensitive values unmasked (passwords, secrets, API keys)',
82+
default: false,
83+
}),
84+
};
85+
86+
async run(): Promise<SetupConfigResponse> {
87+
const {values, sources, warnings} = this.resolvedConfig;
88+
const unmask = this.flags.unmask;
89+
90+
// Build output config with masking applied
91+
const outputConfig: Record<string, unknown> = {};
92+
for (const [key, value] of Object.entries(values)) {
93+
if (value !== undefined) {
94+
outputConfig[key] = isSensitiveField(key) && !unmask ? maskValue(String(value)) : value;
95+
}
96+
}
97+
98+
const result: SetupConfigResponse = {
99+
config: outputConfig,
100+
sources,
101+
warnings: warnings.length > 0 ? warnings.map((w) => w.message) : undefined,
102+
};
103+
104+
// JSON mode - just return the data
105+
if (this.jsonEnabled()) {
106+
return result;
107+
}
108+
109+
// Human-readable output
110+
if (unmask) {
111+
this.warn('Sensitive values are displayed unmasked.');
112+
}
113+
114+
this.printConfig(values, sources, unmask);
115+
116+
// Show warnings
117+
for (const warning of warnings) {
118+
this.warn(warning.message);
119+
}
120+
121+
return result;
122+
}
123+
124+
/**
125+
* Build a map of field -> source name for display.
126+
*/
127+
private buildFieldSourceMap(sources: ConfigSourceInfo[]): Map<string, string> {
128+
const resultMap = new Map<string, string>();
129+
130+
// Process sources in order - first source with a field (not ignored) wins
131+
for (const source of sources) {
132+
for (const field of source.fields) {
133+
if (!source.fieldsIgnored?.includes(field) && !resultMap.has(field)) {
134+
resultMap.set(field, source.name);
135+
}
136+
}
137+
}
138+
139+
return resultMap;
140+
}
141+
142+
/**
143+
* Print the configuration in human-readable format.
144+
*/
145+
private printConfig(config: NormalizedConfig, sources: ConfigSourceInfo[], unmask: boolean): void {
146+
const ui = cliui({width: process.stdout.columns || 80});
147+
const fieldSources = this.buildFieldSourceMap(sources);
148+
149+
// Header
150+
ui.div({text: 'Configuration', padding: [1, 0, 0, 0]});
151+
ui.div({text: '─'.repeat(60), padding: [0, 0, 0, 0]});
152+
153+
// Instance section
154+
this.renderSection(
155+
ui,
156+
'Instance',
157+
[
158+
['hostname', config.hostname],
159+
['webdavHostname', config.webdavHostname],
160+
['codeVersion', config.codeVersion],
161+
],
162+
fieldSources,
163+
unmask,
164+
);
165+
166+
// Auth (Basic) section
167+
this.renderSection(
168+
ui,
169+
'Authentication (Basic)',
170+
[
171+
['username', config.username],
172+
['password', config.password],
173+
],
174+
fieldSources,
175+
unmask,
176+
);
177+
178+
// Auth (OAuth) section
179+
this.renderSection(
180+
ui,
181+
'Authentication (OAuth)',
182+
[
183+
['clientId', config.clientId],
184+
['clientSecret', config.clientSecret],
185+
['scopes', config.scopes],
186+
['authMethods', config.authMethods],
187+
['accountManagerHost', config.accountManagerHost],
188+
],
189+
fieldSources,
190+
unmask,
191+
);
192+
193+
// SCAPI section
194+
this.renderSection(ui, 'SCAPI', [['shortCode', config.shortCode]], fieldSources, unmask);
195+
196+
// MRT section
197+
this.renderSection(
198+
ui,
199+
'Managed Runtime (MRT)',
200+
[
201+
['mrtProject', config.mrtProject],
202+
['mrtEnvironment', config.mrtEnvironment],
203+
['mrtApiKey', config.mrtApiKey],
204+
['mrtOrigin', config.mrtOrigin],
205+
],
206+
fieldSources,
207+
unmask,
208+
);
209+
210+
// Metadata section
211+
this.renderSection(ui, 'Metadata', [['instanceName', config.instanceName]], fieldSources, unmask);
212+
213+
// Sources section
214+
if (sources.length > 0) {
215+
ui.div({text: '', padding: [0, 0, 0, 0]});
216+
ui.div({text: 'Sources', padding: [1, 0, 0, 0]});
217+
ui.div({text: '─'.repeat(60), padding: [0, 0, 0, 0]});
218+
219+
for (const [index, source] of sources.entries()) {
220+
ui.div({text: ` ${index + 1}. ${source.name}`, width: 24}, {text: source.location || '-'});
221+
}
222+
}
223+
224+
ux.stdout(ui.toString());
225+
}
226+
227+
/**
228+
* Render a configuration section with fields.
229+
*/
230+
private renderSection(
231+
ui: ReturnType<typeof cliui>,
232+
title: string,
233+
fields: [string, unknown][],
234+
fieldSources: Map<string, string>,
235+
unmask: boolean,
236+
): void {
237+
ui.div({text: '', padding: [0, 0, 0, 0]});
238+
ui.div({text: title, padding: [0, 0, 0, 0]});
239+
240+
for (const [field, value] of fields) {
241+
const displayValue = getDisplayValue(field, value, unmask);
242+
const source = fieldSources.get(field);
243+
244+
ui.div(
245+
{text: ` ${field}`, width: 22},
246+
{text: displayValue, width: 40},
247+
{text: source ? `[${source}]` : '', padding: [0, 0, 0, 2]},
248+
);
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)