Skip to content

Commit 9390557

Browse files
committed
Improve instance create and set-active interactive experience
- Accept URL or hostname in create command (auto-extracts hostname) - Rename auth choices: WebDAV, API Client; show access key URL - Make client secret optional (supports user auth flow) - Auto-detect active code version via OCAPI when OAuth is configured - Add searchable instance picker to set-active when name omitted
1 parent c3dd768 commit 9390557

2 files changed

Lines changed: 83 additions & 22 deletions

File tree

packages/b2c-cli/src/commands/setup/instance/create.ts

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import {Args, Flags, ux} from '@oclif/core';
77
import {input, password, confirm, select} from '@inquirer/prompts';
88
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
9-
import {DwJsonSource, type NormalizedConfig} from '@salesforce/b2c-tooling-sdk/config';
9+
import {DwJsonSource, createInstanceFromConfig, type NormalizedConfig} from '@salesforce/b2c-tooling-sdk/config';
10+
import {getActiveCodeVersion} from '@salesforce/b2c-tooling-sdk/operations/code';
1011
import {withDocs} from '../../../i18n/index.js';
1112

1213
/**
@@ -24,6 +25,19 @@ interface InstanceCreateResponse {
2425
*/
2526
type AuthType = 'basic' | 'both' | 'none' | 'oauth';
2627

28+
/**
29+
* Extract the hostname from a URL or return the input as-is if it's already a hostname.
30+
*/
31+
function parseHostname(input: string): string {
32+
const trimmed = input.trim();
33+
try {
34+
const url = new URL(trimmed);
35+
return url.hostname;
36+
} catch {
37+
return trimmed;
38+
}
39+
}
40+
2741
/**
2842
* Create a new B2C Commerce instance configuration.
2943
*/
@@ -101,28 +115,24 @@ export default class SetupInstanceCreate extends BaseCommand<typeof SetupInstanc
101115
this.error(`Instance "${name}" already exists. Use a different name.`);
102116
}
103117

104-
// Get or prompt for hostname
118+
// Get or prompt for hostname (accepts a URL or plain hostname)
105119
let hostname = this.flags.hostname;
106120
if (!hostname) {
107121
if (force) {
108122
this.error('Hostname is required in non-interactive mode. Use --hostname flag.');
109123
}
110124
hostname = await input({
111-
message: 'Enter B2C instance hostname:',
125+
message: 'Enter B2C instance hostname or URL:',
112126
validate: (v) => (v.trim() ? true : 'Hostname is required'),
113127
});
114128
}
129+
hostname = parseHostname(hostname);
115130

116131
// Build config
117132
const config: Partial<NormalizedConfig> = {
118133
hostname,
119134
};
120135

121-
// Code version
122-
if (this.flags['code-version']) {
123-
config.codeVersion = this.flags['code-version'];
124-
}
125-
126136
// Handle authentication - in non-interactive mode, use provided flags
127137
if (force) {
128138
// Basic auth
@@ -137,18 +147,17 @@ export default class SetupInstanceCreate extends BaseCommand<typeof SetupInstanc
137147
// OAuth
138148
if (this.flags['client-id']) {
139149
config.clientId = this.flags['client-id'];
140-
if (!this.flags['client-secret']) {
141-
this.error('Client secret is required when client ID is provided in non-interactive mode.');
150+
if (this.flags['client-secret']) {
151+
config.clientSecret = this.flags['client-secret'];
142152
}
143-
config.clientSecret = this.flags['client-secret'];
144153
}
145154
} else {
146155
// Interactive mode - prompt for auth type and credentials
147156
const authType = await select<AuthType>({
148157
message: 'Configure authentication:',
149158
choices: [
150-
{name: 'Basic (username/password)', value: 'basic'},
151-
{name: 'OAuth (client credentials)', value: 'oauth'},
159+
{name: 'WebDAV (username/password or access key)', value: 'basic'},
160+
{name: 'API Client (OAuth client credentials)', value: 'oauth'},
152161
{name: 'Both', value: 'both'},
153162
{name: 'Skip for now', value: 'none'},
154163
],
@@ -163,11 +172,12 @@ export default class SetupInstanceCreate extends BaseCommand<typeof SetupInstanc
163172
validate: (v) => (v.trim() ? true : 'Username is required'),
164173
}));
165174

175+
const accessKeyUrl = `https://${hostname}/on/demandware.store/Sites-Site/default/ViewAccessKeys-List`;
166176
config.password =
167177
this.flags.password ||
168178
(await password({
169-
message: 'Enter WebDAV password:',
170-
validate: (v) => (v.trim() ? true : 'Password is required'),
179+
message: `Enter WebDAV password or access key (${accessKeyUrl}):`,
180+
validate: (v) => (v.trim() ? true : 'Password or access key is required'),
171181
}));
172182
}
173183

@@ -180,12 +190,45 @@ export default class SetupInstanceCreate extends BaseCommand<typeof SetupInstanc
180190
validate: (v) => (v.trim() ? true : 'Client ID is required'),
181191
}));
182192

183-
config.clientSecret =
193+
const clientSecret =
184194
this.flags['client-secret'] ||
185195
(await password({
186-
message: 'Enter OAuth client secret:',
187-
validate: (v) => (v.trim() ? true : 'Client secret is required'),
196+
message: 'Enter OAuth client secret (leave blank for user auth):',
188197
}));
198+
199+
if (clientSecret.trim()) {
200+
config.clientSecret = clientSecret.trim();
201+
}
202+
}
203+
}
204+
205+
// Code version - use flag, or try to detect via OCAPI if OAuth credentials are available
206+
if (this.flags['code-version']) {
207+
config.codeVersion = this.flags['code-version'];
208+
} else if (!force) {
209+
let detectedVersion: string | undefined;
210+
211+
if (config.clientId) {
212+
try {
213+
const tempInstance = createInstanceFromConfig({
214+
hostname,
215+
clientId: config.clientId,
216+
clientSecret: config.clientSecret,
217+
});
218+
const activeVersion = await getActiveCodeVersion(tempInstance);
219+
detectedVersion = activeVersion?.id;
220+
} catch {
221+
// Detection failed - continue without a default
222+
}
223+
}
224+
225+
const codeVersion = await input({
226+
message: 'Enter code version:',
227+
default: detectedVersion,
228+
});
229+
230+
if (codeVersion.trim()) {
231+
config.codeVersion = codeVersion.trim();
189232
}
190233
}
191234

packages/b2c-cli/src/commands/setup/instance/set-active.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
55
*/
66
import {Args, ux} from '@oclif/core';
7+
import {search} from '@inquirer/prompts';
78
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
89
import {DwJsonSource} from '@salesforce/b2c-tooling-sdk/config';
910
import {withDocs} from '../../../i18n/index.js';
@@ -23,7 +24,6 @@ export default class SetupInstanceSetActive extends BaseCommand<typeof SetupInst
2324
static args = {
2425
name: Args.string({
2526
description: 'Instance name to set as active',
26-
required: true,
2727
}),
2828
};
2929

@@ -42,10 +42,28 @@ export default class SetupInstanceSetActive extends BaseCommand<typeof SetupInst
4242

4343
async run(): Promise<InstanceSetActiveResponse> {
4444
const source = new DwJsonSource();
45-
const name = this.args.name;
46-
47-
// Check if instance exists
4845
const instances = source.listInstances({configPath: this.flags.config});
46+
47+
let name = this.args.name;
48+
49+
if (!name) {
50+
if (instances.length === 0) {
51+
this.error('No instances are configured. Use `b2c setup instance create` to add one.');
52+
}
53+
54+
name = await search({
55+
message: 'Select instance:',
56+
source: (term) => {
57+
const filtered = term ? instances.filter((i) => i.name.includes(term)) : instances;
58+
const sorted = [...filtered].sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1));
59+
return sorted.map((i) => ({
60+
name: `${i.name}${i.hostname ? ` (${i.hostname})` : ''}${i.active ? ' [active]' : ''}`,
61+
value: i.name,
62+
}));
63+
},
64+
});
65+
}
66+
4967
const instance = instances.find((i) => i.name === name);
5068

5169
if (!instance) {

0 commit comments

Comments
 (0)