Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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/friendly-sandbox-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@salesforce/b2c-cli': minor
'@salesforce/b2c-tooling-sdk': minor
---

Add support for friendly sandbox ID format (realm-instance) in ODS commands. You can now use `zzzv-123` or `zzzv_123` instead of full UUIDs for `ods get`, `ods start`, `ods stop`, `ods restart`, and `ods delete` commands.
62 changes: 47 additions & 15 deletions docs/cli/ods.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ description: Commands for creating, managing, starting, stopping, and deleting O

Commands for managing On-Demand Sandboxes (ODS).

## Sandbox ID Formats

Commands that operate on a specific sandbox (`get`, `start`, `stop`, `restart`, `delete`) accept two ID formats:

| Format | Example | Description |
|--------|---------|-------------|
| UUID | `abc12345-1234-1234-1234-abc123456789` | Full sandbox UUID |
| Friendly ID | `zzzv-123` or `zzzv_123` | Realm-instance format |

The friendly ID format uses the 4-character realm code followed by a dash (`-`) or underscore (`_`) and the instance identifier. When using a friendly ID, the CLI automatically looks up the corresponding sandbox UUID.

```bash
# These are equivalent (assuming zzzv-123 resolves to the UUID)
b2c ods get abc12345-1234-1234-1234-abc123456789
b2c ods get zzzv-123
```

## Global ODS Flags

These flags are available on all ODS commands:
Expand Down Expand Up @@ -176,16 +193,19 @@ b2c ods get <SANDBOXID>

| Argument | Description | Required |
|----------|-------------|----------|
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
Comment thread
clavery marked this conversation as resolved.

### Examples

```bash
# Get sandbox details
# Get sandbox details using UUID
b2c ods get abc12345-1234-1234-1234-abc123456789

# Get sandbox details using friendly ID
b2c ods get zzzv-123

# Output as JSON
b2c ods get abc12345-1234-1234-1234-abc123456789 --json
b2c ods get zzzv_123 --json
```

### Output
Expand Down Expand Up @@ -244,16 +264,19 @@ b2c ods start <SANDBOXID>

| Argument | Description | Required |
|----------|-------------|----------|
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |

### Examples

```bash
# Start a sandbox
# Start a sandbox using UUID
b2c ods start abc12345-1234-1234-1234-abc123456789

# Start a sandbox using friendly ID
b2c ods start zzzv-123

# Output as JSON
b2c ods start abc12345-1234-1234-1234-abc123456789 --json
b2c ods start zzzv_123 --json
```

---
Expand All @@ -272,16 +295,19 @@ b2c ods stop <SANDBOXID>

| Argument | Description | Required |
|----------|-------------|----------|
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |

### Examples

```bash
# Stop a sandbox
# Stop a sandbox using UUID
b2c ods stop abc12345-1234-1234-1234-abc123456789

# Stop a sandbox using friendly ID
b2c ods stop zzzv-123

# Output as JSON
b2c ods stop abc12345-1234-1234-1234-abc123456789 --json
b2c ods stop zzzv_123 --json
```

---
Expand All @@ -300,16 +326,19 @@ b2c ods restart <SANDBOXID>

| Argument | Description | Required |
|----------|-------------|----------|
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |

### Examples

```bash
# Restart a sandbox
# Restart a sandbox using UUID
b2c ods restart abc12345-1234-1234-1234-abc123456789

# Restart a sandbox using friendly ID
b2c ods restart zzzv-123

# Output as JSON
b2c ods restart abc12345-1234-1234-1234-abc123456789 --json
b2c ods restart zzzv_123 --json
```

---
Expand All @@ -328,7 +357,7 @@ b2c ods delete <SANDBOXID>

| Argument | Description | Required |
|----------|-------------|----------|
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |

### Flags

Expand All @@ -339,11 +368,14 @@ b2c ods delete <SANDBOXID>
### Examples

```bash
# Delete a sandbox (with confirmation prompt)
# Delete a sandbox using UUID (with confirmation prompt)
b2c ods delete abc12345-1234-1234-1234-abc123456789

# Delete a sandbox using friendly ID
b2c ods delete zzzv-123

# Delete without confirmation
b2c ods delete abc12345-1234-1234-1234-abc123456789 --force
b2c ods delete zzzv_123 --force
```

### Notes
Expand Down
7 changes: 4 additions & 3 deletions packages/b2c-cli/src/commands/ods/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function confirm(message: string): Promise<boolean> {
export default class OdsDelete extends OdsCommand<typeof OdsDelete> {
static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID)',
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};
Expand All @@ -44,7 +44,8 @@ export default class OdsDelete extends OdsCommand<typeof OdsDelete> {

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

static flags = {
Expand All @@ -56,7 +57,7 @@ export default class OdsDelete extends OdsCommand<typeof OdsDelete> {
};

async run(): Promise<void> {
const sandboxId = this.args.sandboxId;
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);

// Get sandbox details first to show in confirmation
const getResult = await this.odsClient.GET('/sandboxes/{sandboxId}', {
Expand Down
7 changes: 4 additions & 3 deletions packages/b2c-cli/src/commands/ods/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type SandboxModel = OdsComponents['schemas']['SandboxModel'];
export default class OdsGet extends OdsCommand<typeof OdsGet> {
static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID)',
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};
Expand All @@ -31,11 +31,12 @@ export default class OdsGet extends OdsCommand<typeof OdsGet> {

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

async run(): Promise<SandboxModel> {
const sandboxId = this.args.sandboxId;
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);

this.log(t('commands.ods.get.fetching', 'Fetching sandbox {{sandboxId}}...', {sandboxId}));

Expand Down
7 changes: 4 additions & 3 deletions packages/b2c-cli/src/commands/ods/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
export default class OdsRestart extends OdsCommand<typeof OdsRestart> {
static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID)',
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};
Expand All @@ -30,11 +30,12 @@ export default class OdsRestart extends OdsCommand<typeof OdsRestart> {

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

async run(): Promise<SandboxOperationModel> {
const sandboxId = this.args.sandboxId;
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);

this.log(t('commands.ods.restart.restarting', 'Restarting sandbox {{sandboxId}}...', {sandboxId}));

Expand Down
7 changes: 4 additions & 3 deletions packages/b2c-cli/src/commands/ods/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
export default class OdsStart extends OdsCommand<typeof OdsStart> {
static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID)',
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};
Expand All @@ -30,11 +30,12 @@ export default class OdsStart extends OdsCommand<typeof OdsStart> {

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

async run(): Promise<SandboxOperationModel> {
const sandboxId = this.args.sandboxId;
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);

this.log(t('commands.ods.start.starting', 'Starting sandbox {{sandboxId}}...', {sandboxId}));

Expand Down
7 changes: 4 additions & 3 deletions packages/b2c-cli/src/commands/ods/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
export default class OdsStop extends OdsCommand<typeof OdsStop> {
static args = {
sandboxId: Args.string({
description: 'Sandbox ID (UUID)',
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
required: true,
}),
};
Expand All @@ -30,11 +30,12 @@ export default class OdsStop extends OdsCommand<typeof OdsStop> {

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

async run(): Promise<SandboxOperationModel> {
const sandboxId = this.args.sandboxId;
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);

this.log(t('commands.ods.stop.stopping', 'Stopping sandbox {{sandboxId}}...', {sandboxId}));

Expand Down
59 changes: 59 additions & 0 deletions packages/b2c-tooling-sdk/src/cli/ods-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Command, Flags} from '@oclif/core';
import {OAuthCommand} from './oauth-command.js';
import {createOdsClient, type OdsClient} from '../clients/ods.js';
import {DEFAULT_ODS_HOST} from '../defaults.js';
import {isUuid, parseFriendlySandboxId, SandboxNotFoundError} from '../operations/ods/sandbox-lookup.js';

/**
* Base command for ODS (On-Demand Sandbox) operations.
Expand Down Expand Up @@ -82,4 +83,62 @@ export abstract class OdsCommand<T extends typeof Command> extends OAuthCommand<
protected get odsHost(): string {
return this.flags['sandbox-api-host'] ?? DEFAULT_ODS_HOST;
}

/**
* Resolves a sandbox identifier to a UUID.
*
* Supports both UUID format and friendly format (realm-instance, e.g., "abcd-123" or "abcd_123").
* If given a UUID, returns it directly. If given a friendly format, queries the API to find
* the matching sandbox and logs the resolution.
*
* @param identifier - Sandbox identifier (UUID or friendly format)
* @returns The sandbox UUID
* @throws Error if the sandbox cannot be found (friendly ID not resolved)
*
* @example
* ```typescript
* // In a command's run() method:
* const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
* ```
*/
protected async resolveSandboxId(identifier: string): Promise<string> {
// If already a UUID, return directly
if (isUuid(identifier)) {
return identifier;
}

// Try to parse as friendly ID
const parsed = parseFriendlySandboxId(identifier);
if (!parsed) {
// Not a UUID and not a friendly ID - pass through as-is
// (let the API return an appropriate error)
return identifier;
}

// Log that we're looking up the sandbox
this.log(`Looking up sandbox ${identifier}...`);

// Query sandboxes filtered by realm
const {data, error} = await this.odsClient.GET('/sandboxes', {
params: {
query: {
filter_params: `realm=${parsed.realm}`,
},
},
});

if (error || !data?.data) {
this.error(new SandboxNotFoundError(identifier, parsed.realm, parsed.instance).message);
}

// Find sandbox with matching instance
const sandbox = data.data.find((s) => s.instance?.toLowerCase() === parsed.instance);

if (!sandbox?.id) {
this.error(new SandboxNotFoundError(identifier, parsed.realm, parsed.instance).message);
}

this.log(`Resolved ${identifier} to sandbox ${sandbox.id}`);
return sandbox.id;
}
}
9 changes: 9 additions & 0 deletions packages/b2c-tooling-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ export type {
DownloadDocsResult,
} from './operations/docs/index.js';

// Operations - ODS
export {
isUuid,
isFriendlySandboxId,
parseFriendlySandboxId,
resolveSandboxId,
SandboxNotFoundError,
} from './operations/ods/index.js';

// Defaults
export {DEFAULT_ACCOUNT_MANAGER_HOST, DEFAULT_ODS_HOST} from './defaults.js';

Expand Down
18 changes: 18 additions & 0 deletions packages/b2c-tooling-sdk/src/operations/ods/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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
*/
/**
* ODS (On-Demand Sandbox) operations.
*
* @module operations/ods
*/

export {
isUuid,
isFriendlySandboxId,
parseFriendlySandboxId,
resolveSandboxId,
SandboxNotFoundError,
} from './sandbox-lookup.js';
Loading
Loading