Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/scapi-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-cli': minor
'@salesforce/b2c-tooling-sdk': minor
---

Migrate `job`, `code`, `bm users`, and `bm roles` commands to support SCAPI alongside OCAPI. In auto mode (the default), the CLI prefers SCAPI when `shortCode` and `tenantId` are configured and silently falls back to OCAPI if the SCAPI scopes aren't granted. Use `--api-backend ocapi|scapi|auto` or `apiBackend` in dw.json to control explicitly. SCAPI scopes: `sfcc.jobs.rw`, `sfcc.scripts.rw`, `sfcc.users.rw`, `sfcc.roles.rw`. New `job execution delete` command (SCAPI only).
30 changes: 29 additions & 1 deletion docs/cli/bm.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,35 @@ description: Commands for administering Business Manager resources on a B2C Comm

# Business Manager Commands

Commands for administering instance-level Business Manager resources via the OCAPI Data API. These are distinct from [Account Manager commands](/cli/account-manager) which manage cross-instance identity.
Commands for administering instance-level Business Manager resources. These are distinct from [Account Manager commands](/cli/account-manager) which manage cross-instance identity.

## API Backend

Most `bm users` and `bm roles` commands support both the OCAPI Data API and the SCAPI Merchant Users / Merchant Roles APIs. By default (`auto` mode), SCAPI is preferred when `shortCode` and `tenantId` are configured. If the SCAPI scopes aren't granted on your API client, the CLI silently falls back to OCAPI.

```bash
# Force SCAPI backend
b2c bm users list --api-backend scapi

# Force OCAPI backend
b2c bm roles get Administrator --api-backend ocapi
```

Or set in `dw.json`: `"api-backend": "scapi"`. Or `SFCC_API_BACKEND=scapi` env var.

| Command | SCAPI | OCAPI |
|---|---|---|
| `bm users list/get/update/delete` | ✓ (`sfcc.users.rw`) | ✓ |
| `bm users search` | ✗ — OCAPI only | ✓ |
| `bm whoami` | ✗ — OCAPI only | ✓ |
| `bm access-key *` | ✗ — OCAPI only | ✓ |
| `bm roles list/get/create/delete` | ✓ (`sfcc.roles.rw`) | ✓ |
| `bm roles grant/revoke` | ✓ (`sfcc.roles.rw`) | ✓ |
| `bm roles permissions get/set` | ✓ (`sfcc.roles.rw`) | ✓ |

::: warning
The SCAPI Users PATCH endpoint does not support changing the `disabled` flag. `bm users update --disabled` falls back to OCAPI in auto mode; with `--api-backend scapi` it errors with a clear message.
:::

## Authentication

Expand Down
31 changes: 27 additions & 4 deletions docs/cli/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,37 @@ description: Commands for deploying, downloading, activating code versions, and

Commands for managing cartridge code on B2C Commerce instances.

## API Backend

The `code list`, `code activate`, and `code delete` commands support both OCAPI and SCAPI backends. By default (`auto` mode), SCAPI is preferred when `shortCode` and `tenantId` are configured. If SCAPI scopes are unavailable, the CLI falls back to OCAPI transparently.

```bash
# Force SCAPI
b2c code list --api-backend scapi

# Force OCAPI
b2c code list --api-backend ocapi
```

Or set in `dw.json`: `"api-backend": "scapi"`. Or `SFCC_API_BACKEND=scapi` env var.

::: tip
The `code activate --reload` flag forces an OCAPI call regardless of `--api-backend`, since SCAPI does not expose the cache-rebuild operation.
:::

::: tip
The `code deploy`, `code download`, and `code watch` commands always use WebDAV (no SCAPI equivalent for cartridge file transfer).
:::

## Authentication

Code commands use different authentication depending on the operation:

| Operation | Auth Required |
|-----------|--------------|
| `code deploy`, `code download`, `code watch` | WebDAV (Basic Auth or OAuth) |
| `code list`, `code activate`, `code delete` | OAuth + OCAPI |
| `code list`, `code activate`, `code delete` (SCAPI) | OAuth + `sfcc.scripts` (read) or `sfcc.scripts.rw` (write) + tenant scope |
| `code list`, `code activate`, `code delete` (OCAPI) | OAuth + OCAPI permissions for `/code_versions` |

### WebDAV Operations (deploy, download, watch)

Expand All @@ -24,16 +47,16 @@ export SFCC_USERNAME=your-bm-username
export SFCC_PASSWORD=your-webdav-access-key
```

### OCAPI Operations (list, activate, delete)
### SCAPI / OCAPI Operations (list, activate, delete)

These commands require OAuth authentication with OCAPI permissions for the `/code_versions` resource configured in Business Manager.
These commands require OAuth authentication. For SCAPI, configure the `sfcc.scripts.rw` scope on your API client in Account Manager. For OCAPI, configure permissions for the `/code_versions` resource in Business Manager.

```bash
export SFCC_CLIENT_ID=your-client-id
export SFCC_CLIENT_SECRET=your-client-secret
```

For complete setup instructions including OCAPI configuration, see the [Authentication Guide](/guide/authentication).
For complete setup instructions, see the [Authentication Guide](/guide/authentication).

---

Expand Down
75 changes: 73 additions & 2 deletions docs/cli/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,51 @@ description: Commands for executing jobs, importing and exporting site archives,

Commands for executing and monitoring jobs on B2C Commerce instances.

## API Backend

Job commands support both OCAPI and SCAPI backends. By default (`auto` mode), SCAPI is preferred when `shortCode` and `tenantId` are configured. If SCAPI scopes are unavailable, the CLI falls back to OCAPI transparently.

Use `--api-backend` to control explicitly:

```bash
# Force SCAPI
b2c job run my-job --api-backend scapi

# Force OCAPI
b2c job run my-job --api-backend ocapi

# Auto-detect (default)
b2c job run my-job --api-backend auto
```

Or set in `dw.json`:

```json
{
"api-backend": "scapi"
}
```

Or via environment variable: `SFCC_API_BACKEND=scapi`.

::: tip
The `job import` and `job export` commands currently use OCAPI only, regardless of the `--api-backend` setting.
:::

## Authentication

Job commands require OAuth authentication with OCAPI permissions.
### SCAPI (recommended)

When using SCAPI, your API client needs the appropriate scopes in Account Manager:

| Scope | Operations |
|-------|------------|
| `sfcc.jobs.rw` | Execute, delete, search, and get job executions (recommended) |
| `sfcc.jobs` | Search and get job executions (read-only) |

### Required OCAPI Permissions
You also need `shortCode` and `tenantId` configured (in `dw.json` or via flags).

### OCAPI

Configure these resources in Business Manager under **Administration** > **Site Development** > **Open Commerce API Settings**:

Expand Down Expand Up @@ -253,6 +293,37 @@ b2c job log my-custom-job > job.log

---

## b2c job execution delete

Delete a job execution record. This command requires the SCAPI backend (`sfcc.jobs.rw` scope).

### Usage

```bash
b2c job execution delete JOBID EXECUTIONID
```

### Arguments

| Argument | Description | Required |
|----------|-------------|----------|
| `JOBID` | Job ID | Yes |
| `EXECUTIONID` | Execution ID to delete | Yes |

### Examples

```bash
# Delete a specific execution
b2c job execution delete my-job abc123-def456
```

### Notes

- Requires SCAPI backend — not available via OCAPI.
- Requires the `sfcc.jobs.rw` scope on your API client.

---

## b2c job import

Import a site archive to a B2C Commerce instance using the `sfcc-site-archive-import` system job.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ For the full command reference with all flags, see [Setup Commands](/cli/setup).
| `certificate` | Path to PKCS12 certificate for two-factor auth (mTLS) |
| `certificate-passphrase` | Passphrase for the certificate. Also accepts `passphrase`. |
| `self-signed` | Allow self-signed server certificates. Also accepts `selfsigned`. |
| `api-backend` | API backend for `job`, `code`, `bm users`, and `bm roles` commands: `ocapi`, `scapi`, or `auto` (default). Auto prefers SCAPI when `shortCode` and `tenant-id` are set, falling back to OCAPI on missing scopes. |

### Two-Factor Authentication (mTLS)

Expand Down
13 changes: 8 additions & 5 deletions packages/b2c-cli/src/commands/bm/roles/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Args, Flags} from '@oclif/core';
import {InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {createBmRole, type BmRole} from '@salesforce/b2c-tooling-sdk/operations/bm-roles';
import {BmCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {type RoleInfo} from '@salesforce/b2c-tooling-sdk/operations/bm-roles';
import {t} from '../../../i18n/index.js';

export default class BmRolesCreate extends InstanceCommand<typeof BmRolesCreate> {
export default class BmRolesCreate extends BmCommand<typeof BmRolesCreate> {
static args = {
role: Args.string({
description: 'Role ID to create',
Expand All @@ -33,16 +33,19 @@ export default class BmRolesCreate extends InstanceCommand<typeof BmRolesCreate>
}),
};

async run(): Promise<BmRole> {
async run(): Promise<RoleInfo> {
this.requireOAuthCredentials();

const {role: roleId} = this.args;
const {description} = this.flags;
const hostname = this.resolvedConfig.values.hostname!;

const backend = this.createRolesBackend();
this.logger.debug(`Using ${backend.name} backend for roles create`);

this.log(t('commands.bm.roles.create.creating', 'Creating role {{roleId}} on {{hostname}}...', {roleId, hostname}));

const role = await createBmRole(this.instance, roleId, {description});
const role = await backend.createRole(roleId, {description});

if (this.jsonEnabled()) {
return role;
Expand Down
10 changes: 6 additions & 4 deletions packages/b2c-cli/src/commands/bm/roles/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Args} from '@oclif/core';
import {InstanceCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {deleteBmRole} from '@salesforce/b2c-tooling-sdk/operations/bm-roles';
import {BmCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {t} from '../../../i18n/index.js';

interface DeleteResult {
Expand All @@ -14,7 +13,7 @@ interface DeleteResult {
hostname: string;
}

export default class BmRolesDelete extends InstanceCommand<typeof BmRolesDelete> {
export default class BmRolesDelete extends BmCommand<typeof BmRolesDelete> {
static args = {
role: Args.string({
description: 'Role ID to delete',
Expand All @@ -37,11 +36,14 @@ export default class BmRolesDelete extends InstanceCommand<typeof BmRolesDelete>
const {role: roleId} = this.args;
const hostname = this.resolvedConfig.values.hostname!;

const backend = this.createRolesBackend();
this.logger.debug(`Using ${backend.name} backend for roles delete`);

this.log(
t('commands.bm.roles.delete.deleting', 'Deleting role {{roleId}} from {{hostname}}...', {roleId, hostname}),
);

await deleteBmRole(this.instance, roleId);
await backend.deleteRole(roleId);

const result = {success: true, role: roleId, hostname};

Expand Down
41 changes: 28 additions & 13 deletions packages/b2c-cli/src/commands/bm/roles/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/
import {Args, Flags} from '@oclif/core';
import {InstanceCommand, printFieldsBlock, type DetailSection} from '@salesforce/b2c-tooling-sdk/cli';
import {getBmRole, type BmRole} from '@salesforce/b2c-tooling-sdk/operations/bm-roles';
import {BmCommand, printFieldsBlock, type DetailSection} from '@salesforce/b2c-tooling-sdk/cli';
import {type RoleInfo} from '@salesforce/b2c-tooling-sdk/operations/bm-roles';
import {t} from '../../../i18n/index.js';

export default class BmRolesGet extends InstanceCommand<typeof BmRolesGet> {
interface ExpandedUser {
login?: string;
first_name?: string;
last_name?: string;
firstName?: string;
lastName?: string;
}

export default class BmRolesGet extends BmCommand<typeof BmRolesGet> {
static args = {
role: Args.string({
description: 'Role ID (e.g. "Administrator")',
Expand All @@ -29,33 +37,42 @@ export default class BmRolesGet extends InstanceCommand<typeof BmRolesGet> {
static flags = {
expand: Flags.string({
char: 'e',
description: 'Expansions to apply (e.g. users, permissions)',
description: 'Expansions to apply (users, permissions)',
multiple: true,
options: ['users', 'permissions'],
}),
};

async run(): Promise<BmRole> {
async run(): Promise<RoleInfo> {
this.requireOAuthCredentials();

const {role: roleId} = this.args;
const {expand} = this.flags;
const hostname = this.resolvedConfig.values.hostname!;

const backend = this.createRolesBackend();
this.logger.debug(`Using ${backend.name} backend for roles get`);

this.log(t('commands.bm.roles.get.fetching', 'Fetching role {{roleId}} from {{hostname}}...', {roleId, hostname}));

const role = await getBmRole(this.instance, roleId, {expand});
const role = await backend.getRole(roleId, {expand: expand as ('permissions' | 'users')[] | undefined});

if (this.jsonEnabled()) {
return role;
}

const sections: DetailSection[] = [];
if (role.users && role.users.length > 0) {
// Users may be present on _raw (both OCAPI and SCAPI return them under role.users when --expand users).
const raw = role._raw as undefined | {users?: ExpandedUser[]};
const users = raw?.users;
if (users && users.length > 0) {
sections.push({
title: 'Assigned Users',
lines: role.users.map((user) => {
lines: users.map((user) => {
const login = user.login || '-';
const name = [user.first_name, user.last_name].filter(Boolean).join(' ');
const first = user.firstName ?? user.first_name;
const last = user.lastName ?? user.last_name;
const name = [first, last].filter(Boolean).join(' ');
return name ? `${login} ${name}` : login;
}),
});
Expand All @@ -66,10 +83,8 @@ export default class BmRolesGet extends InstanceCommand<typeof BmRolesGet> {
[
['ID', role.id],
['Description', role.description],
['User Count', role.user_count?.toString()],
['User Manager', role.user_manager?.toString()],
['Created', role.creation_date],
['Last Modified', role.last_modified],
['User Count', role.userCount?.toString()],
['User Manager', role.userManager?.toString()],
],
{sections},
);
Expand Down
Loading
Loading