Skip to content

Commit 31e136b

Browse files
authored
@W-22135727 adding operations, scheduling, user and email management in ODS (#328)
* @W-21766978 adding scheduling, user and email management in ODS * adding operations command support for ODS - claude * addressing review comments
1 parent c573171 commit 31e136b

16 files changed

Lines changed: 1153 additions & 27 deletions

File tree

.changeset/ods-enhancements.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@salesforce/b2c-cli': minor
3+
'@salesforce/b2c-dx-docs': patch
4+
---
5+
6+
ODS CLI: **`b2c sandbox create`** adds **`--emails`** for notification addresses; **`b2c sandbox update`** adds **`--start-scheduler`**, **`--stop-scheduler`**, **`--clear-start-scheduler`**, and **`--clear-stop-scheduler`**; **`b2c realm update`** adds **`--emails`**, **`--start-scheduler`**, **`--stop-scheduler`**, **`--clear-start-scheduler`**, and **`--clear-stop-scheduler`**.
7+
8+
Sandbox API: **`b2c sandbox operations list`** and **`b2c sandbox operations get`** (inspect lifecycle operations); **`b2c sandbox alias get`** (get one alias by ID, same endpoint as **`alias list --alias-id`**).
9+
10+
User guide updated for scheduling flags, sandbox operations, and **`b2c sandbox alias get`**.

docs/cli/sandbox.md

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ These commands were previously available as `b2c ods <command>`. The `ods` prefi
1212

1313
## Sandbox ID Formats
1414

15-
Commands that operate on a specific sandbox (`get`, `update`, `start`, `stop`, `restart`, `delete`) accept two ID formats:
15+
Commands that operate on a specific sandbox (`get`, `update`, `start`, `stop`, `restart`, `delete`, `operations list`, `operations get`) accept two ID formats:
1616

1717
| Format | Example | Description |
1818
|--------|---------|-------------|
@@ -149,6 +149,7 @@ b2c sandbox create --realm <REALM>
149149
| `--ttl` | Time to live in hours (0 for infinite) | `24` |
150150
| `--profile` | Resource profile (medium, large, xlarge, xxlarge) | `medium` |
151151
| `--auto-scheduled` | Enable automatic start/stop scheduling | `false` |
152+
| `--emails` | Comma-separated list of notification email addresses | |
152153
| `--wait`, `-w` | Wait for sandbox to reach started or failed state | `false` |
153154
| `--poll-interval` | Polling interval in seconds when using --wait | `10` |
154155
| `--timeout` | Maximum wait time in seconds (0 for no timeout) | `600` |
@@ -177,6 +178,9 @@ b2c sandbox create --realm abcd --wait
177178
# Create with auto-scheduling enabled
178179
b2c sandbox create --realm abcd --auto-scheduled
179180

181+
# Create with notification emails
182+
b2c sandbox create --realm abcd --emails dev@example.com,ops@example.com
183+
180184
# Create without automatic permissions
181185
b2c sandbox create --realm abcd --no-set-permissions
182186

@@ -388,6 +392,106 @@ b2c sandbox restart zzzv_123 --json
388392

389393
---
390394

395+
## b2c sandbox operations list {#b2c-sandbox-operations-list}
396+
397+
List past and current **operations** on a sandbox (for example start, stop, restart, reset, create, delete, upgrade). This maps to the ODS API `GET /sandboxes/{sandboxId}/operations` endpoint.
398+
399+
To **request** a lifecycle operation (`start`, `stop`, `restart`, `reset`), use `b2c sandbox start|stop|restart|reset` instead; those commands call `POST /sandboxes/{sandboxId}/operations`.
400+
401+
### Usage
402+
403+
```bash
404+
b2c sandbox operations list <SANDBOXID>
405+
```
406+
407+
### Arguments
408+
409+
| Argument | Description | Required |
410+
|----------|-------------|----------|
411+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
412+
413+
### Flags
414+
415+
| Flag | Short | Description | Default |
416+
|------|-------|-------------|---------|
417+
| `--from` | | Earliest operation time (ISO 8601). If omitted, the API defaults to roughly the last 30 days. | |
418+
| `--to` | | Latest operation time (ISO 8601). If omitted, the API defaults to now. | |
419+
| `--operation-state` | | Filter by lifecycle state: `pending`, `running`, or `finished` | |
420+
| `--status` | | Filter finished operations by outcome: `success` or `failure` | |
421+
| `--operation` | | Filter by operation type: `start`, `stop`, `restart`, `reset`, `create`, `delete`, `upgrade` | |
422+
| `--sort-order` | | Sort order: `asc` or `desc` | |
423+
| `--sort-by` | | Sort field: `created`, `operation_state`, `status`, or `operation` | |
424+
| `--page` | | Page index (0-based) | |
425+
| `--per-page` | | Page size (API default is typically 20) | |
426+
| `--columns`, `-c` | | Columns to display (comma-separated); see **Available columns** below | |
427+
| `--extended`, `-x` | | Include extended columns (for example `operationBy`) | `false` |
428+
429+
### Available columns
430+
431+
`id`, `operation`, `operationState`, `status`, `sandboxState`, `createdAt`, `operationBy` (extended)
432+
433+
**Default columns:** `operation`, `operationState`, `status`, `sandboxState`, `createdAt`, `id`
434+
435+
### Examples
436+
437+
```bash
438+
# List recent operations for a sandbox
439+
b2c sandbox operations list zzzv-123
440+
441+
# Only finished operations
442+
b2c sandbox operations list zzzv-123 --operation-state finished
443+
444+
# Date range and paging
445+
b2c sandbox operations list zzzv-123 --from 2025-01-01 --to 2025-12-31 --page 0 --per-page 50
446+
447+
# Custom columns
448+
b2c sandbox operations list zzzv-123 --columns operation,operationState,status,createdAt
449+
450+
# Full API response (includes paging metadata when present)
451+
b2c sandbox operations list zzzv-123 --json
452+
```
453+
454+
### Output
455+
456+
When not using `--json`, the command prints a one-line paging summary when metadata is present, then a table of operations. Use `--json` to inspect `metadata` (page, totals) and the raw `data` array.
457+
458+
---
459+
460+
## b2c sandbox operations get {#b2c-sandbox-operations-get}
461+
462+
Return details for a **single** sandbox operation by its operation UUID (maps to `GET /sandboxes/{sandboxId}/operations/{operationId}`). Use the operation `id` from `b2c sandbox operations list` or from the JSON output of `b2c sandbox start|stop|restart|reset` when using `--json`.
463+
464+
### Usage
465+
466+
```bash
467+
b2c sandbox operations get <SANDBOXID> <OPERATIONID>
468+
```
469+
470+
### Arguments
471+
472+
| Argument | Description | Required |
473+
|----------|-------------|----------|
474+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
475+
| `OPERATIONID` | Operation UUID | Yes |
476+
477+
### Examples
478+
479+
```bash
480+
# Show operation details (human-readable)
481+
b2c sandbox operations get zzzv-123 550e8400-e29b-41d4-a716-446655440000
482+
483+
# Operation as JSON (sandbox operation model only)
484+
b2c sandbox operations get zzzv-123 550e8400-e29b-41d4-a716-446655440000 --json
485+
```
486+
487+
### Output
488+
489+
When not using `--json`, the command prints a short **Operation Details** block including operation type, state, outcome, sandbox state, created time, and who ran the operation when available.
490+
491+
With `--json`, the command returns the **operation object** (not the full API envelope), consistent with `b2c sandbox get`.
492+
493+
---
494+
391495
## b2c sandbox delete
392496

393497
Delete an on-demand sandbox.
@@ -513,6 +617,8 @@ b2c sandbox update <SANDBOXID> [FLAGS]
513617
| `--resource-profile` | Resource profile (`medium`, `large`, `xlarge`, `xxlarge`) |
514618
| `--tags` | Comma-separated list of tags |
515619
| `--emails` | Comma-separated list of notification email addresses |
620+
| `--start-scheduler` | Start schedule JSON (or `"null"` to remove existing scheduler) |
621+
| `--stop-scheduler` | Stop schedule JSON (or `"null"` to remove existing scheduler) |
516622

517623
At least one flag is required.
518624

@@ -540,6 +646,9 @@ b2c sandbox update zzzv-123 --resource-profile large
540646
# Set notification emails
541647
b2c sandbox update zzzv-123 --emails dev@example.com,qa@example.com
542648

649+
# Enable automatic scheduling and set scheduler values
650+
b2c sandbox update zzzv-123 --auto-scheduled --start-scheduler '{"weekdays":["MONDAY"],"time":"08:00:00Z"}' --stop-scheduler "null"
651+
543652
# Combine multiple updates
544653
b2c sandbox update zzzv-123 --ttl 48 --resource-profile xlarge --tags ci,nightly
545654

@@ -551,6 +660,8 @@ b2c sandbox update zzzv-123 --ttl 48 --json
551660

552661
- The `--ttl` value is added to the existing sandbox lifetime, not an absolute value. Together with previous extensions, it must adhere to the realm's maximum TTL configuration.
553662
- Setting `--ttl` to 0 or less gives the sandbox an infinite lifetime (subject to realm configuration).
663+
- `--auto-scheduled` controls whether automatic start/stop behavior is enabled for the sandbox.
664+
- `--start-scheduler` and `--stop-scheduler` define scheduler values, but scheduler automation is effective only when `--auto-scheduled` is enabled.
554665

555666
---
556667

@@ -685,6 +796,7 @@ Alias commands are available both under the `sandbox` topic and the legacy `ods`
685796

686797
- `b2c sandbox alias create`
687798
- `b2c sandbox alias list`
799+
- `b2c sandbox alias get`
688800
- `b2c sandbox alias delete`
689801

690802
### b2c sandbox alias create
@@ -779,6 +891,39 @@ When listing multiple aliases without `--json`, the command prints a table with:
779891
- Whether the alias is unique
780892
- DNS verification record (if any)
781893

894+
For **one alias** by ID, you can also use **`b2c sandbox alias get`** (same API as `list --alias-id`).
895+
896+
### b2c sandbox alias get {#b2c-sandbox-alias-get}
897+
898+
Get details for a **single** hostname alias (ODS API `GET /sandboxes/{sandboxId}/aliases/{sandboxAliasId}`). This is equivalent to `b2c sandbox alias list <SANDBOXID> --alias-id <ALIASID>` but uses positional arguments only.
899+
900+
#### Usage
901+
902+
```bash
903+
b2c sandbox alias get <SANDBOXID> <ALIASID>
904+
```
905+
906+
#### Arguments
907+
908+
| Argument | Description | Required |
909+
|----------|-------------|----------|
910+
| `SANDBOXID` | Sandbox ID (UUID or realm-instance, e.g., `zzzv-123`) | Yes |
911+
| `ALIASID` | Alias UUID | Yes |
912+
913+
#### Examples
914+
915+
```bash
916+
# Human-readable details
917+
b2c sandbox alias get zzzv-123 some-alias-uuid
918+
919+
# Alias object as JSON (same shape as list/get for a single alias)
920+
b2c sandbox alias get zzzv-123 some-alias-uuid --json
921+
```
922+
923+
#### Output
924+
925+
When not using `--json`, the command prints an **Alias Details** section (hostname, status, uniqueness, DNS TXT verification, registration URL when present, optional cookie hint). With `--json`, it returns the **alias object** only.
926+
782927
### b2c sandbox alias delete
783928

784929
Delete a sandbox alias.
@@ -1174,6 +1319,8 @@ b2c sandbox realm update <REALM> [FLAGS]
11741319
| `--default-sandbox-ttl` | Default sandbox TTL in hours when no TTL is specified at creation |
11751320
| `--start-scheduler` | Start schedule JSON for sandboxes in this realm (use `"null"` to remove) |
11761321
| `--stop-scheduler` | Stop schedule JSON for sandboxes in this realm (use `"null"` to remove) |
1322+
| `--emails` | Comma-separated list of realm notification email addresses |
1323+
| `--local-users-allowed` / `--no-local-users-allowed` | Enable or disable local users in realm sandbox configuration |
11771324

11781325
The scheduler flags expect a JSON value or the literal string `"null"`:
11791326

@@ -1195,6 +1342,9 @@ b2c sandbox realm update zzzz \
11951342

11961343
# Remove an existing stop scheduler
11971344
b2c sandbox realm update zzzz --stop-scheduler "null"
1345+
1346+
# Update realm emails and local user setting
1347+
b2c sandbox realm update zzzz --emails dev@example.com,ops@example.com --local-users-allowed
11981348
```
11991349

12001350
If no update flags are provided, the command fails with a helpful error explaining which flags can be used.

packages/b2c-cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@
179179
},
180180
"realm": {
181181
"description": "Manage sandbox realms (alias for 'sandbox realm')"
182+
},
183+
"operations": {
184+
"description": "List and inspect sandbox lifecycle operations (alias for 'sandbox operations')"
182185
}
183186
}
184187
},
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
import {Args, ux} from '@oclif/core';
7+
import cliui from 'cliui';
8+
import {OdsCommand} from '@salesforce/b2c-tooling-sdk/cli';
9+
import {getApiErrorMessage, type OdsComponents} from '@salesforce/b2c-tooling-sdk';
10+
import {t, withDocs} from '../../../i18n/index.js';
11+
12+
type SandboxAliasModel = OdsComponents['schemas']['SandboxAliasModel'];
13+
14+
/**
15+
* Get details for a single sandbox hostname alias (ODS API GET /sandboxes/{sandboxId}/aliases/{sandboxAliasId}).
16+
*/
17+
export default class SandboxAliasGet extends OdsCommand<typeof SandboxAliasGet> {
18+
static aliases = ['ods:alias:get'];
19+
20+
static args = {
21+
sandboxId: Args.string({
22+
description: 'Sandbox ID (UUID or realm-instance, e.g., abcd-123)',
23+
required: true,
24+
}),
25+
aliasId: Args.string({
26+
description: 'Alias UUID',
27+
required: true,
28+
}),
29+
};
30+
31+
static description = withDocs(
32+
t('commands.sandbox.alias.get.description', 'Show details for a specific sandbox hostname alias'),
33+
'/cli/sandbox.html#b2c-sandbox-alias-get',
34+
);
35+
36+
static enableJsonFlag = true;
37+
38+
static examples = [
39+
'<%= config.bin %> <%= command.id %> zzzv-123 alias-uuid-here',
40+
'<%= config.bin %> <%= command.id %> abc12345-1234-1234-1234-abc123456789 alias-uuid-here --json',
41+
];
42+
43+
async run(): Promise<SandboxAliasModel | undefined> {
44+
const sandboxId = await this.resolveSandboxId(this.args.sandboxId);
45+
const {aliasId} = this.args;
46+
47+
this.log(
48+
t('commands.sandbox.alias.get.fetching', 'Fetching alias {{aliasId}} for sandbox {{sandboxId}}...', {
49+
aliasId,
50+
sandboxId: this.args.sandboxId,
51+
}),
52+
);
53+
54+
const result = await this.odsClient.GET('/sandboxes/{sandboxId}/aliases/{sandboxAliasId}', {
55+
params: {
56+
path: {sandboxId, sandboxAliasId: aliasId},
57+
},
58+
});
59+
60+
if (result.error) {
61+
this.error(
62+
t('commands.sandbox.alias.get.error', 'Failed to fetch alias: {{message}}', {
63+
message: getApiErrorMessage(result.error, result.response),
64+
}),
65+
);
66+
}
67+
68+
const alias = result.data?.data;
69+
if (!alias) {
70+
this.log(t('commands.sandbox.alias.get.noData', 'No alias details were returned.'));
71+
return undefined;
72+
}
73+
74+
if (this.jsonEnabled()) {
75+
return alias;
76+
}
77+
78+
this.printAlias(alias);
79+
return alias;
80+
}
81+
82+
private printAlias(alias: SandboxAliasModel): void {
83+
const ui = cliui({width: process.stdout.columns || 80});
84+
ui.div({text: 'Alias Details', padding: [1, 0, 0, 0]});
85+
ui.div({text: '─'.repeat(50), padding: [0, 0, 0, 0]});
86+
87+
const labelWidth = 22;
88+
const rows: [string, string | undefined][] = [
89+
['ID', alias.id],
90+
['Hostname', alias.name],
91+
['Status', alias.status],
92+
['Unique', alias.unique === undefined ? undefined : String(alias.unique)],
93+
['Sandbox ID', alias.sandboxId],
94+
[
95+
'Let’s Encrypt',
96+
alias.requestLetsEncryptCertificate === undefined ? undefined : String(alias.requestLetsEncryptCertificate),
97+
],
98+
['DNS verification (TXT)', alias.domainVerificationRecord],
99+
['Registration URL', alias.registration],
100+
];
101+
102+
for (const [label, value] of rows) {
103+
if (value !== undefined) {
104+
ui.div({text: `${label}:`, width: labelWidth, padding: [0, 2, 0, 0]}, {text: value, padding: [0, 0, 0, 0]});
105+
}
106+
}
107+
108+
if (alias.cookie) {
109+
const c = alias.cookie;
110+
ui.div(
111+
{text: 'Cookie:', width: labelWidth, padding: [0, 2, 0, 0]},
112+
{text: `${c.name}=${c.value}${c.path ? ` (path: ${c.path})` : ''}`, padding: [0, 0, 0, 0]},
113+
);
114+
}
115+
116+
ux.stdout(ui.toString());
117+
}
118+
}

0 commit comments

Comments
 (0)