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
5 changes: 5 additions & 0 deletions .changeset/mrt-schema-refresh-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@salesforce/b2c-agent-plugins': patch
---

Document new MRT environment clone, bundle delete, organization member, and organization certificate commands in the `b2c-mrt` skill.
11 changes: 11 additions & 0 deletions .changeset/mrt-schema-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@salesforce/b2c-cli': minor
'@salesforce/b2c-tooling-sdk': minor
---

Refresh the MRT admin API schema and add new commands:

- `b2c mrt env clone` — clone an environment from an existing source, optionally copying redirects, environment variables, and B2C target info
- `b2c mrt bundle delete` — delete one or more bundles (uses bulk-delete when more than one ID is supplied)
- `b2c mrt org member list|add|get|update|remove` — manage organization-level members
- `b2c mrt org cert list|get|create|delete|restart-validation` — manage custom domain certificates referenced by environments
128 changes: 128 additions & 0 deletions docs/cli/mrt.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,35 @@ b2c mrt env delete staging --project my-storefront
b2c mrt env delete old-env -p my-storefront --force
```

### b2c mrt env clone

Clone an environment from an existing source environment. The new target receives the source's configuration (excluding proxies and the production flag) and is automatically deployed with the source target's current bundle (if any). Optionally clones redirects, environment variables, and B2C target info.

The source environment is the one selected by `--environment` / `-e` (or `MRT_ENVIRONMENT` / `mrtEnvironment` in `dw.json`). The positional argument is the **new** environment's slug.

```bash
# Clone the configured environment into a new slug
b2c mrt env clone staging-copy -p my-storefront -e staging

# Clone with redirects and environment variables
b2c mrt env clone qa -p my-storefront -e staging --clone-redirects --clone-env-vars

# Clone using a custom domain certificate
b2c mrt env clone qa -p my-storefront -e staging \
--external-hostname qa.example.com --certificate-id 123 --wait
```

| Flag | Description |
|------|-------------|
| `--environment`, `-e` | Source environment slug (defaults to `mrtEnvironment` / `MRT_ENVIRONMENT`) |
| `--external-hostname` | Full external hostname (required for non-MRT-managed certificates) |
| `--external-domain` | External domain for Universal PWA SSR |
| `--certificate-id` | Certificate ID for custom domain (use `b2c mrt org cert list` to find) |
| `--clone-redirects` | Clone redirects from the source environment |
| `--clone-env-vars` | Clone environment variables from the source environment |
| `--clone-b2c-info` | Clone B2C target info from the source environment |
| `--wait`, `-w` | Wait for the new environment to reach a terminal state |

### b2c mrt env invalidate

Invalidate CDN cache for an environment.
Expand Down Expand Up @@ -508,6 +537,105 @@ b2c mrt bundle download 12345 -p my-storefront -o ./artifacts/bundle.tgz
b2c mrt bundle download 12345 -p my-storefront --url-only
```

### b2c mrt bundle delete

Delete one or more bundles. Bundles are deleted asynchronously by the server and only project admins can run this command. With more than one bundle ID the CLI uses the bulk-delete endpoint and reports any rejected bundles (e.g. bundles in use by an active deployment).

```bash
# Delete a single bundle
b2c mrt bundle delete 12345 -p my-storefront

# Delete several at once
b2c mrt bundle delete 12345 12346 12347 -p my-storefront

# Skip the confirmation prompt
b2c mrt bundle delete 12345 -p my-storefront --force
```

---

## Organization Member Commands

Organization members are distinct from project members: they hold a role at the organization level and can optionally be granted permission to view all projects and manage custom domain certificates.

### b2c mrt org member list

```bash
b2c mrt org member list --org my-org
b2c mrt org member list --org my-org --search alice
```

### b2c mrt org member add

Roles: `owner` or `member`.

```bash
b2c mrt org member add alice@example.com --org my-org --role member
b2c mrt org member add bob@example.com --org my-org --role owner --view-all-projects
```

### b2c mrt org member get

```bash
b2c mrt org member get alice@example.com --org my-org
```

### b2c mrt org member update

```bash
b2c mrt org member update alice@example.com --org my-org --view-all-projects
b2c mrt org member update alice@example.com --org my-org --no-cert-permission
```

### b2c mrt org member remove

```bash
b2c mrt org member remove alice@example.com --org my-org
```

---

## Organization Certificate Commands

Manage custom domain certificates for environments that use a non-MRT-managed hostname. Certificates are organization-scoped; reference a certificate from `env create`, `env update`, or `env clone` via `--certificate-id`.

### b2c mrt org cert list

```bash
b2c mrt org cert list --org my-org
b2c mrt org cert list --org my-org --custom-only
```

### b2c mrt org cert get

Returns the validation record (the DNS entry the customer must add to validate the certificate).

```bash
b2c mrt org cert get 123 --org my-org
```

### b2c mrt org cert create

```bash
b2c mrt org cert create shop.example.com --org my-org
```

The output includes the validation record. Add it to your DNS to complete validation.

### b2c mrt org cert delete

```bash
b2c mrt org cert delete 123 --org my-org
```

### b2c mrt org cert restart-validation

Restart validation for a certificate that has not yet been validated. The response includes a fresh validation record.

```bash
b2c mrt org cert restart-validation 123 --org my-org
```

---

## Tail Logs
Expand Down
6 changes: 6 additions & 0 deletions packages/b2c-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@
"mrt:org": {
"description": "List organizations and view B2C Commerce instance connections\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-commands"
},
"mrt:org:member": {
"description": "Manage organization-level members and their permissions\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-member-commands"
},
"mrt:org:cert": {
"description": "Manage custom domain certificates for organization environments\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#organization-certificate-commands"
},
"mrt:project": {
"description": "Create, update, delete, and configure MRT projects\n\nDocs: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/mrt.html#project-commands"
},
Expand Down
150 changes: 150 additions & 0 deletions packages/b2c-cli/src/commands/mrt/bundle/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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 {Args, Flags} from '@oclif/core';
import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {
bulkDeleteBundles,
deleteBundle,
type BulkDeleteBundlesResult,
} from '@salesforce/b2c-tooling-sdk/operations/mrt';
import {t, withDocs} from '../../../i18n/index.js';
import {confirm} from '../../../prompts.js';

interface DeleteBundleResult {
queued: number[];
rejected: BulkDeleteBundlesResult['rejected'];
}

export default class MrtBundleDelete extends MrtCommand<typeof MrtBundleDelete> {
static args = {
bundleId: Args.integer({
description: 'Bundle ID to delete (additional IDs may follow)',
required: true,
}),
};

static description = withDocs(
t('commands.mrt.bundle.delete.description', 'Delete one or more bundles from a Managed Runtime project'),
'/cli/mrt.html#b2c-mrt-bundle-delete',
);

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %> 12345 --project my-storefront',
'<%= config.bin %> <%= command.id %> 12345 12346 12347 -p my-storefront',
'<%= config.bin %> <%= command.id %> 12345 -p my-storefront --force',
];

static flags = {
...MrtCommand.baseFlags,
force: Flags.boolean({
char: 'f',
description: 'Skip confirmation prompt',
default: false,
}),
};

// Allow multiple positional bundle IDs
static strict = false;

protected operations = {
deleteBundle,
bulkDeleteBundles,
};

async run(): Promise<DeleteBundleResult> {
this.requireMrtCredentials();

const {argv, flags} = await this.parse(MrtBundleDelete);
const {mrtProject: project} = this.resolvedConfig.values;

if (!project) {
this.error('MRT project is required. Provide --project flag, set MRT_PROJECT, or set mrtProject in dw.json.');
}

const bundleIds: number[] = [];
for (const arg of argv as Array<number | string>) {
const n = typeof arg === 'number' ? arg : Number.parseInt(String(arg), 10);
if (!Number.isInteger(n) || n <= 0) {
this.error(t('commands.mrt.bundle.delete.invalidId', 'Invalid bundle ID: {{arg}}', {arg: String(arg)}));
}
bundleIds.push(n);
}

if (!flags.force && !this.jsonEnabled()) {
const message =
bundleIds.length === 1
? t('commands.mrt.bundle.delete.confirmOne', 'Delete bundle {{id}} from {{project}}?', {
id: String(bundleIds[0]),
project,
})
: t('commands.mrt.bundle.delete.confirmMany', 'Delete {{n}} bundles from {{project}}?', {
n: String(bundleIds.length),
project,
});
const confirmed = await confirm(message);
if (!confirmed) {
this.log(t('commands.mrt.bundle.delete.cancelled', 'Delete cancelled.'));
return {queued: [], rejected: []};
}
}

try {
if (bundleIds.length === 1) {
const [bundleId] = bundleIds;
await this.operations.deleteBundle(
{
projectSlug: project,
bundleId,
origin: this.resolvedConfig.values.mrtOrigin,
},
this.getMrtAuth(),
);

if (!this.jsonEnabled()) {
this.log(
t('commands.mrt.bundle.delete.queuedOne', 'Bundle {{id}} queued for deletion.', {id: String(bundleId)}),
);
}

return {queued: [bundleId], rejected: []};
}

const result = await this.operations.bulkDeleteBundles(
{
projectSlug: project,
bundleIds,
origin: this.resolvedConfig.values.mrtOrigin,
},
this.getMrtAuth(),
);

if (!this.jsonEnabled()) {
this.log(
t('commands.mrt.bundle.delete.queuedMany', '{{n}} bundle(s) queued for deletion.', {
n: String(result.queued.length),
}),
);
if (result.rejected.length > 0) {
this.log(t('commands.mrt.bundle.delete.rejectedHeader', 'Rejected bundles:'));
for (const r of result.rejected) {
this.log(` - ${r.bundleId ?? '?'}: ${r.reason}`);
}
}
}

return result;
} catch (error) {
if (error instanceof Error) {
this.error(
t('commands.mrt.bundle.delete.failed', 'Failed to delete bundle(s): {{message}}', {message: error.message}),
);
}
throw error;
}
}
}
Loading
Loading