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
424 changes: 424 additions & 0 deletions docs/cli/sandbox.md

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions packages/b2c-cli/src/commands/sandbox/alias/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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 {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t, withDocs} from '../../../i18n/index.js';
import open from 'open';

type SandboxAliasModel = OdsComponents['schemas']['SandboxAliasModel'];

/**
* Command to create a sandbox alias.
*/
export default class SandboxAliasCreate extends OdsCommand<typeof SandboxAliasCreate> {
static aliases = ['ods:alias:create'];

static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
hostname: Args.string({
description: 'Hostname alias to register (e.g., my-store.example.com)',
required: true,
}),
};

static description = withDocs(
t('commands.sandbox.alias.create.description', 'Create a hostname alias for a sandbox'),
'/cli/sandbox.html#b2c-sandbox-alias-create',
);

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %> zzzv-123 my-store.example.com',
'<%= config.bin %> <%= command.id %> zzzv-123 secure-store.example.com --unique',
'<%= config.bin %> <%= command.id %> zzzv-123 secure-store.example.com --unique --letsencrypt',
'<%= config.bin %> <%= command.id %> zzzv-123 my-store.example.com --json',
];

static flags = {
unique: Flags.boolean({
char: 'u',
description: "Make the alias unique (required for Let's Encrypt certificates)",
default: false,
}),
letsencrypt: Flags.boolean({
description: "Request a Let's Encrypt certificate for this alias (requires --unique)",
default: false,
dependsOn: ['unique'],
}),
'no-open': Flags.boolean({
description: 'Do not open registration URL in browser (for non-unique aliases)',
default: false,
}),
};

async run(): Promise<SandboxAliasModel> {
const {sandboxId, hostname} = this.args;
const {unique, letsencrypt, 'no-open': noOpen} = this.flags;

const resolvedSandboxId = await this.resolveSandboxId(sandboxId);

this.log(
t('commands.sandbox.alias.create.creating', 'Creating alias {{hostname}} for sandbox {{sandboxId}}...', {
hostname,
sandboxId,
}),
);

const result = await this.odsClient.POST('/sandboxes/{sandboxId}/aliases', {
params: {
path: {sandboxId: resolvedSandboxId},
},
body: {
name: hostname,
unique,
requestLetsEncryptCertificate: letsencrypt,
},
});

if (!result.data?.data) {
const message = getApiErrorMessage(result.error, result.response);
this.error(
t('commands.sandbox.alias.create.error', 'Failed to create alias: {{message}}', {
message,
}),
);
}

const alias = result.data.data as SandboxAliasModel;

if (!this.jsonEnabled()) {
this.log(t('commands.sandbox.alias.create.success', 'Alias created successfully'));
this.log('');
this.log(`ID: ${alias.id}`);
this.log(`Hostname: ${alias.name}`);
this.log(`Status: ${alias.status}`);

if (unique && alias.domainVerificationRecord) {
this.log('');
this.log(t('commands.sandbox.alias.create.verification', '⚠️ DNS Verification Required:'));
this.log(
t(
'commands.sandbox.alias.create.verification_instructions',
'Add this TXT record to your DNS configuration:',
),
);
this.log(` ${alias.domainVerificationRecord}`);
this.log('');
this.log(t('commands.sandbox.alias.create.verification_wait', 'The alias will activate after DNS propagation'));
}

if (!unique && alias.registration && !noOpen) {
this.log('');
this.log(t('commands.sandbox.alias.create.registration', 'Opening alias registration in browser...'));
await open(alias.registration);
}
}

return alias;
}
}
96 changes: 96 additions & 0 deletions packages/b2c-cli/src/commands/sandbox/alias/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {getApiErrorMessage} from '@salesforce/b2c-tooling-sdk';
import {t, withDocs} from '../../../i18n/index.js';
import {confirm} from '@inquirer/prompts';

/**
* Command to delete a sandbox alias.
*/
export default class SandboxAliasDelete extends OdsCommand<typeof SandboxAliasDelete> {
static aliases = ['ods:alias:delete'];

static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
aliasId: Args.string({
description: 'Alias ID to delete',
required: true,
}),
};

static description = withDocs(
t('commands.sandbox.alias.delete.description', 'Delete a hostname alias from a sandbox'),
'/cli/sandbox.html#b2c-sandbox-alias-delete',
);

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %> zzzv-123 alias-uuid-here',
'<%= config.bin %> <%= command.id %> zzzv-123 alias-uuid-here --force',
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 alias-uuid-here --json',
];

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

async run(): Promise<{success: boolean; message: string}> {
const {sandboxId, aliasId} = this.args;
const {force} = this.flags;

const resolvedSandboxId = await this.resolveSandboxId(sandboxId);

// Confirmation prompt (skip if --force or --json)
if (!force && !this.jsonEnabled()) {
const confirmed = await confirm({
message: t('commands.sandbox.alias.delete.confirm', 'Delete alias {{aliasId}}?', {aliasId}),
default: false,
});

if (!confirmed) {
this.log(t('commands.sandbox.alias.delete.cancelled', 'Delete cancelled'));
return {success: false, message: 'Cancelled by user'};
}
}

this.log(
t('commands.sandbox.alias.delete.deleting', 'Deleting alias {{aliasId}} from sandbox {{sandboxId}}...', {
aliasId,
sandboxId,
}),
);

const result = await this.odsClient.DELETE('/sandboxes/{sandboxId}/aliases/{sandboxAliasId}', {
params: {
path: {sandboxId: resolvedSandboxId, sandboxAliasId: aliasId},
},
});

if (result.response?.status !== 404 && result.error) {
const message = getApiErrorMessage(result.error, result.response);
this.error(
t('commands.sandbox.alias.delete.error', 'Failed to delete alias: {{message}}', {
message,
}),
);
}

const message = t('commands.sandbox.alias.delete.success', 'Alias deleted successfully');
this.log(message);

return {success: true, message};
}
}
164 changes: 164 additions & 0 deletions packages/b2c-cli/src/commands/sandbox/alias/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* 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 {OdsCommand, TableRenderer} from '@salesforce/b2c-tooling-sdk/cli';
import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
import {t, withDocs} from '../../../i18n/index.js';

type SandboxAliasModel = OdsComponents['schemas']['SandboxAliasModel'];

/**
* Command to list sandbox aliases.
*/
export default class SandboxAliasList extends OdsCommand<typeof SandboxAliasList> {
static aliases = ['ods:alias:list'];

static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};

static description = withDocs(
t('commands.sandbox.alias.list.description', 'List all hostname aliases for a sandbox'),
'/cli/sandbox.html#b2c-sandbox-alias-list',
);

static enableJsonFlag = true;

static examples = [
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
'<%= config.bin %> <%= command.id %> zzzv-123',
'<%= config.bin %> <%= command.id %> zzzv-123 --alias-id some-alias-uuid',
'<%= config.bin %> <%= command.id %> zzzv-123 --json',
];

static flags = {
'alias-id': Flags.string({
description: 'Specific alias ID to retrieve (if omitted, lists all aliases)',
required: false,
}),
};

async run(): Promise<SandboxAliasModel | SandboxAliasModel[]> {
const {sandboxId} = this.args;
const {'alias-id': aliasId} = this.flags;

const resolvedSandboxId = await this.resolveSandboxId(sandboxId);

// If alias ID provided, get specific alias; otherwise list all
if (aliasId) {
return this.showAlias(resolvedSandboxId, aliasId);
}
return this.listAllAliases(resolvedSandboxId);
}

private async listAllAliases(sandboxId: string): Promise<SandboxAliasModel[]> {
this.log(
t('commands.sandbox.alias.list.fetching', 'Fetching aliases for sandbox {{sandboxId}}...', {
sandboxId: this.args.sandboxId,
}),
);

const result = await this.odsClient.GET('/sandboxes/{sandboxId}/aliases', {
params: {
path: {sandboxId},
},
});

if (!result.data?.data) {
const message = getApiErrorMessage(result.error, result.response);
this.error(
t('commands.sandbox.alias.list.error', 'Failed to fetch aliases: {{message}}', {
message,
}),
);
}

const aliases = (result.data?.data ?? []) as SandboxAliasModel[];

if (!this.jsonEnabled()) {
if (aliases.length === 0) {
this.log(t('commands.sandbox.alias.list.no_aliases', 'No aliases found'));
} else {
this.log(t('commands.sandbox.alias.list.count', 'Found {{count}} alias(es):', {count: aliases.length}));
const columns = {
id: {
header: 'Alias ID',
get: (row: SandboxAliasModel) => row.id || '-',
},
name: {
header: 'Hostname',
get: (row: SandboxAliasModel) => row.name,
},
status: {
header: 'Status',
get: (row: SandboxAliasModel) => row.status || '-',
},
unique: {
header: 'Unique',
get: (row: SandboxAliasModel) => (row.unique ? 'Yes' : 'No'),
},
domainVerificationRecord: {
header: 'Verification Record',
get: (row: SandboxAliasModel) => row.domainVerificationRecord || '-',
},
};
const table = new TableRenderer(columns);
table.render(aliases, ['id', 'name', 'status', 'unique', 'domainVerificationRecord']);
}
}

return aliases;
}

private async showAlias(sandboxId: string, aliasId: string): Promise<SandboxAliasModel> {
this.log(
t('commands.sandbox.alias.list.fetching_one', 'Fetching alias {{aliasId}} for sandbox {{sandboxId}}...', {
aliasId,
sandboxId,
}),
);

const result = await this.odsClient.GET('/sandboxes/{sandboxId}/aliases/{sandboxAliasId}', {
params: {
path: {sandboxId, sandboxAliasId: aliasId},
},
});

if (!result.data?.data) {
const message = getApiErrorMessage(result.error, result.response);
this.error(
t('commands.sandbox.alias.list.error_one', 'Failed to fetch alias: {{message}}', {
message,
}),
);
}

const alias = result.data.data as SandboxAliasModel;

if (!this.jsonEnabled()) {
this.log('');
this.log(t('commands.sandbox.alias.list.alias_details', 'Alias Details:'));
this.log('─'.repeat(60));
this.log(`ID: ${alias.id}`);
this.log(`Name: ${alias.name}`);
this.log(`Status: ${alias.status}`);
if (alias.unique) {
this.log(`Unique: ${alias.unique}`);
}
if (alias.domainVerificationRecord) {
this.log(`Verification Record: ${alias.domainVerificationRecord}`);
}
if (alias.registration) {
this.log(`Registration URL: ${alias.registration}`);
}
}

return alias;
}
}
Loading
Loading