Skip to content

Commit ddee52e

Browse files
authored
feat: support realm-instance sandbox ID format (#83)
* ods command identifier lookup * docs: update ODS documentation for friendly sandbox ID format Add documentation for the realm-instance friendly ID format (e.g., zzzv-123) to the ODS skill and CLI reference. Also refactor OdsCommand to inline the lookup logic with user-facing log messages. * add changeset for friendly sandbox ID feature * support f_ecom_ prefix (organization ID format) in sandbox lookup * use consistent 'realm-instance format' terminology in docs
1 parent 1afc734 commit ddee52e

File tree

13 files changed

+627
-30
lines changed

13 files changed

+627
-30
lines changed

.changeset/friendly-sandbox-id.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@salesforce/b2c-cli': minor
3+
'@salesforce/b2c-tooling-sdk': minor
4+
---
5+
6+
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.

docs/cli/ods.md

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ description: Commands for creating, managing, starting, stopping, and deleting O
66

77
Commands for managing On-Demand Sandboxes (ODS).
88

9+
## Sandbox ID Formats
10+
11+
Commands that operate on a specific sandbox (`get`, `start`, `stop`, `restart`, `delete`) accept two ID formats:
12+
13+
| Format | Example | Description |
14+
|--------|---------|-------------|
15+
| UUID | `abc12345-1234-1234-1234-abc123456789` | Full sandbox UUID |
16+
| Realm-instance | `zzzv-123` or `zzzv_123` | Realm-instance format |
17+
18+
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.
19+
20+
```bash
21+
# These are equivalent (assuming zzzv-123 resolves to the UUID)
22+
b2c ods get abc12345-1234-1234-1234-abc123456789
23+
b2c ods get zzzv-123
24+
```
25+
926
## Global ODS Flags
1027

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

177194
| Argument | Description | Required |
178195
|----------|-------------|----------|
179-
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
196+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
180197

181198
### Examples
182199

183200
```bash
184-
# Get sandbox details
201+
# Get sandbox details using UUID
185202
b2c ods get abc12345-1234-1234-1234-abc123456789
186203

204+
# Get sandbox details using realm-instance format
205+
b2c ods get zzzv-123
206+
187207
# Output as JSON
188-
b2c ods get abc12345-1234-1234-1234-abc123456789 --json
208+
b2c ods get zzzv_123 --json
189209
```
190210

191211
### Output
@@ -244,16 +264,19 @@ b2c ods start <SANDBOXID>
244264

245265
| Argument | Description | Required |
246266
|----------|-------------|----------|
247-
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
267+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
248268

249269
### Examples
250270

251271
```bash
252-
# Start a sandbox
272+
# Start a sandbox using UUID
253273
b2c ods start abc12345-1234-1234-1234-abc123456789
254274

275+
# Start a sandbox using realm-instance format
276+
b2c ods start zzzv-123
277+
255278
# Output as JSON
256-
b2c ods start abc12345-1234-1234-1234-abc123456789 --json
279+
b2c ods start zzzv_123 --json
257280
```
258281

259282
---
@@ -272,16 +295,19 @@ b2c ods stop <SANDBOXID>
272295

273296
| Argument | Description | Required |
274297
|----------|-------------|----------|
275-
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
298+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
276299

277300
### Examples
278301

279302
```bash
280-
# Stop a sandbox
303+
# Stop a sandbox using UUID
281304
b2c ods stop abc12345-1234-1234-1234-abc123456789
282305

306+
# Stop a sandbox using realm-instance format
307+
b2c ods stop zzzv-123
308+
283309
# Output as JSON
284-
b2c ods stop abc12345-1234-1234-1234-abc123456789 --json
310+
b2c ods stop zzzv_123 --json
285311
```
286312

287313
---
@@ -300,16 +326,19 @@ b2c ods restart <SANDBOXID>
300326

301327
| Argument | Description | Required |
302328
|----------|-------------|----------|
303-
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
329+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
304330

305331
### Examples
306332

307333
```bash
308-
# Restart a sandbox
334+
# Restart a sandbox using UUID
309335
b2c ods restart abc12345-1234-1234-1234-abc123456789
310336

337+
# Restart a sandbox using realm-instance format
338+
b2c ods restart zzzv-123
339+
311340
# Output as JSON
312-
b2c ods restart abc12345-1234-1234-1234-abc123456789 --json
341+
b2c ods restart zzzv_123 --json
313342
```
314343

315344
---
@@ -328,7 +357,7 @@ b2c ods delete <SANDBOXID>
328357

329358
| Argument | Description | Required |
330359
|----------|-------------|----------|
331-
| `SANDBOXID` | Sandbox ID (UUID) | Yes |
360+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
332361

333362
### Flags
334363

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

341370
```bash
342-
# Delete a sandbox (with confirmation prompt)
371+
# Delete a sandbox using UUID (with confirmation prompt)
343372
b2c ods delete abc12345-1234-1234-1234-abc123456789
344373

374+
# Delete a sandbox using realm-instance format
375+
b2c ods delete zzzv-123
376+
345377
# Delete without confirmation
346-
b2c ods delete abc12345-1234-1234-1234-abc123456789 --force
378+
b2c ods delete zzzv_123 --force
347379
```
348380

349381
### Notes

packages/b2c-cli/src/commands/ods/delete.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function confirm(message: string): Promise<boolean> {
3232
export default class OdsDelete extends OdsCommand<typeof OdsDelete> {
3333
static args = {
3434
sandboxId: Args.string({
35-
description: 'Sandbox ID (UUID)',
35+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
3636
required: true,
3737
}),
3838
};
@@ -44,7 +44,8 @@ export default class OdsDelete extends OdsCommand<typeof OdsDelete> {
4444

4545
static examples = [
4646
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
47-
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --force',
47+
'<%= config.bin %> <%= command.id %> zzzv-123',
48+
'<%= config.bin %> <%= command.id %> zzzv_123 --force',
4849
];
4950

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

5859
async run(): Promise<void> {
59-
const sandboxId = this.args.sandboxId;
60+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
6061

6162
// Get sandbox details first to show in confirmation
6263
const getResult = await this.odsClient.GET('/sandboxes/{sandboxId}', {

packages/b2c-cli/src/commands/ods/get.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type SandboxModel = OdsComponents['schemas']['SandboxModel'];
1717
export default class OdsGet extends OdsCommand<typeof OdsGet> {
1818
static args = {
1919
sandboxId: Args.string({
20-
description: 'Sandbox ID (UUID)',
20+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
2121
required: true,
2222
}),
2323
};
@@ -31,11 +31,12 @@ export default class OdsGet extends OdsCommand<typeof OdsGet> {
3131

3232
static examples = [
3333
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
34-
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json',
34+
'<%= config.bin %> <%= command.id %> zzzv-123',
35+
'<%= config.bin %> <%= command.id %> zzzv_123 --json',
3536
];
3637

3738
async run(): Promise<SandboxModel> {
38-
const sandboxId = this.args.sandboxId;
39+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
3940

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

packages/b2c-cli/src/commands/ods/restart.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
1616
export default class OdsRestart extends OdsCommand<typeof OdsRestart> {
1717
static args = {
1818
sandboxId: Args.string({
19-
description: 'Sandbox ID (UUID)',
19+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
2020
required: true,
2121
}),
2222
};
@@ -30,11 +30,12 @@ export default class OdsRestart extends OdsCommand<typeof OdsRestart> {
3030

3131
static examples = [
3232
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
33-
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json',
33+
'<%= config.bin %> <%= command.id %> zzzv-123',
34+
'<%= config.bin %> <%= command.id %> zzzv_123 --json',
3435
];
3536

3637
async run(): Promise<SandboxOperationModel> {
37-
const sandboxId = this.args.sandboxId;
38+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
3839

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

packages/b2c-cli/src/commands/ods/start.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
1616
export default class OdsStart extends OdsCommand<typeof OdsStart> {
1717
static args = {
1818
sandboxId: Args.string({
19-
description: 'Sandbox ID (UUID)',
19+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
2020
required: true,
2121
}),
2222
};
@@ -30,11 +30,12 @@ export default class OdsStart extends OdsCommand<typeof OdsStart> {
3030

3131
static examples = [
3232
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
33-
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json',
33+
'<%= config.bin %> <%= command.id %> zzzv-123',
34+
'<%= config.bin %> <%= command.id %> zzzv_123 --json',
3435
];
3536

3637
async run(): Promise<SandboxOperationModel> {
37-
const sandboxId = this.args.sandboxId;
38+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
3839

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

packages/b2c-cli/src/commands/ods/stop.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type SandboxOperationModel = OdsComponents['schemas']['SandboxOperationModel'];
1616
export default class OdsStop extends OdsCommand<typeof OdsStop> {
1717
static args = {
1818
sandboxId: Args.string({
19-
description: 'Sandbox ID (UUID)',
19+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
2020
required: true,
2121
}),
2222
};
@@ -30,11 +30,12 @@ export default class OdsStop extends OdsCommand<typeof OdsStop> {
3030

3131
static examples = [
3232
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789',
33-
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 --json',
33+
'<%= config.bin %> <%= command.id %> zzzv-123',
34+
'<%= config.bin %> <%= command.id %> zzzv_123 --json',
3435
];
3536

3637
async run(): Promise<SandboxOperationModel> {
37-
const sandboxId = this.args.sandboxId;
38+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
3839

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

packages/b2c-tooling-sdk/src/cli/ods-command.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {Command, Flags} from '@oclif/core';
77
import {OAuthCommand} from './oauth-command.js';
88
import {createOdsClient, type OdsClient} from '../clients/ods.js';
99
import {DEFAULT_ODS_HOST} from '../defaults.js';
10+
import {isUuid, parseFriendlySandboxId, SandboxNotFoundError} from '../operations/ods/sandbox-lookup.js';
1011

1112
/**
1213
* Base command for ODS (On-Demand Sandbox) operations.
@@ -82,4 +83,62 @@ export abstract class OdsCommand<T extends typeof Command> extends OAuthCommand<
8283
protected get odsHost(): string {
8384
return this.flags['sandbox-api-host'] ?? DEFAULT_ODS_HOST;
8485
}
86+
87+
/**
88+
* Resolves a sandbox identifier to a UUID.
89+
*
90+
* Supports both UUID format and friendly format (realm-instance, e.g., "abcd-123" or "abcd_123").
91+
* If given a UUID, returns it directly. If given a friendly format, queries the API to find
92+
* the matching sandbox and logs the resolution.
93+
*
94+
* @param identifier - Sandbox identifier (UUID or friendly format)
95+
* @returns The sandbox UUID
96+
* @throws Error if the sandbox cannot be found (friendly ID not resolved)
97+
*
98+
* @example
99+
* ```typescript
100+
* // In a command's run() method:
101+
* const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
102+
* ```
103+
*/
104+
protected async resolveSandboxId(identifier: string): Promise<string> {
105+
// If already a UUID, return directly
106+
if (isUuid(identifier)) {
107+
return identifier;
108+
}
109+
110+
// Try to parse as friendly ID
111+
const parsed = parseFriendlySandboxId(identifier);
112+
if (!parsed) {
113+
// Not a UUID and not a friendly ID - pass through as-is
114+
// (let the API return an appropriate error)
115+
return identifier;
116+
}
117+
118+
// Log that we're looking up the sandbox
119+
this.log(`Looking up sandbox ${identifier}...`);
120+
121+
// Query sandboxes filtered by realm
122+
const {data, error} = await this.odsClient.GET('/sandboxes', {
123+
params: {
124+
query: {
125+
filter_params: `realm=${parsed.realm}`,
126+
},
127+
},
128+
});
129+
130+
if (error || !data?.data) {
131+
this.error(new SandboxNotFoundError(identifier, parsed.realm, parsed.instance).message);
132+
}
133+
134+
// Find sandbox with matching instance
135+
const sandbox = data.data.find((s) => s.instance?.toLowerCase() === parsed.instance);
136+
137+
if (!sandbox?.id) {
138+
this.error(new SandboxNotFoundError(identifier, parsed.realm, parsed.instance).message);
139+
}
140+
141+
this.log(`Resolved ${identifier} to sandbox ${sandbox.id}`);
142+
return sandbox.id;
143+
}
85144
}

packages/b2c-tooling-sdk/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,15 @@ export type {
204204
DownloadDocsResult,
205205
} from './operations/docs/index.js';
206206

207+
// Operations - ODS
208+
export {
209+
isUuid,
210+
isFriendlySandboxId,
211+
parseFriendlySandboxId,
212+
resolveSandboxId,
213+
SandboxNotFoundError,
214+
} from './operations/ods/index.js';
215+
207216
// Defaults
208217
export {DEFAULT_ACCOUNT_MANAGER_HOST, DEFAULT_ODS_HOST} from './defaults.js';
209218

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* SPDX-License-Identifier: Apache-2
4+
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
/**
7+
* ODS (On-Demand Sandbox) operations.
8+
*
9+
* @module operations/ods
10+
*/
11+
12+
export {
13+
isUuid,
14+
isFriendlySandboxId,
15+
parseFriendlySandboxId,
16+
resolveSandboxId,
17+
SandboxNotFoundError,
18+
} from './sandbox-lookup.js';

0 commit comments

Comments
 (0)