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
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 realm-instance format 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 |
| Realm-instance | `zzzv-123` or `zzzv_123` | Realm-instance format |

The realm-instance format uses the 4-character realm code followed by a dash (`-`) or underscore (`_`) and the instance identifier. When using the realm-instance format, 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 realm-instance format
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 realm-instance format
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 realm-instance format
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 realm-instance format
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 realm-instance format
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