Skip to content

Commit 147a585

Browse files
committed
update env create semantics
1 parent f29479a commit 147a585

File tree

5 files changed

+270
-25
lines changed

5 files changed

+270
-25
lines changed

AGENTS.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
1+
# B2C CLI
12

2-
- this is a monorepo project; packages:
3-
- ./packages/b2c-cli - the command line interface built with oclif
4-
- ./packages/b2c-tooling-sdk - the SDK/library for B2C Commerce operations; support the CLI and can be used standalone
3+
This is a monorepo project with the following packages:
4+
- `./packages/b2c-cli` - the command line interface built with oclif
5+
- `./packages/b2c-tooling-sdk` - the SDK/library for B2C Commerce operations; supports the CLI and can be used standalone
6+
7+
## Common Commands
8+
9+
```bash
10+
# Install dependencies
11+
pnpm install
12+
13+
# Build all packages
14+
pnpm run build
15+
16+
# Build specific package
17+
pnpm --filter @salesforce/b2c-cli run build
18+
pnpm --filter @salesforce/b2c-tooling-sdk run build
19+
20+
# Run tests (includes linting)
21+
pnpm run test
22+
23+
# Run tests for specific package
24+
pnpm --filter @salesforce/b2c-cli run test
25+
pnpm --filter @salesforce/b2c-tooling-sdk run test
26+
27+
# Format code with prettier
28+
pnpm run -r format
29+
30+
# Lint only (without tests)
31+
pnpm run -r lint
32+
33+
# Run CLI in development mode
34+
./packages/b2c-cli/bin/dev.js <command>
35+
```
536

637
## Setup/Packaging
738

packages/b2c-cli/src/commands/mrt/env/create.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import {Args, Flags, ux} from '@oclif/core';
77
import cliui from 'cliui';
88
import {MrtCommand} from '@salesforce/b2c-tooling-sdk/cli';
9-
import {createEnv, type MrtEnvironment} from '@salesforce/b2c-tooling-sdk/operations/mrt';
9+
import {createEnv, waitForEnv, type MrtEnvironment} from '@salesforce/b2c-tooling-sdk/operations/mrt';
1010
import {t} from '../../../i18n/index.js';
1111

1212
/**
@@ -140,18 +140,19 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
140140
static enableJsonFlag = true;
141141

142142
static examples = [
143+
'<%= config.bin %> <%= command.id %> staging --project my-storefront',
143144
'<%= config.bin %> <%= command.id %> staging --project my-storefront --name "Staging Environment"',
144-
'<%= config.bin %> <%= command.id %> production --project my-storefront --name "Production" --production',
145-
'<%= config.bin %> <%= command.id %> feature-test -p my-storefront -n "Feature Test" --region eu-west-1',
146-
'<%= config.bin %> <%= command.id %> staging -p my-storefront -n "Staging" --proxy api=api.example.com --proxy ocapi=ocapi.example.com',
145+
'<%= config.bin %> <%= command.id %> production --project my-storefront --production',
146+
'<%= config.bin %> <%= command.id %> feature-test -p my-storefront --region eu-west-1',
147+
'<%= config.bin %> <%= command.id %> staging -p my-storefront --proxy api=api.example.com --proxy ocapi=ocapi.example.com',
148+
'<%= config.bin %> <%= command.id %> staging -p my-storefront --wait',
147149
];
148150

149151
static flags = {
150152
...MrtCommand.baseFlags,
151153
name: Flags.string({
152154
char: 'n',
153-
description: 'Display name for the environment',
154-
required: true,
155+
description: 'Display name for the environment (defaults to slug)',
155156
}),
156157
region: Flags.string({
157158
char: 'r',
@@ -185,6 +186,11 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
185186
description: 'Proxy configuration in format path=host (can be specified multiple times)',
186187
multiple: true,
187188
}),
189+
wait: Flags.boolean({
190+
char: 'w',
191+
description: 'Wait for the environment to be ready before returning',
192+
default: false,
193+
}),
188194
};
189195

190196
async run(): Promise<MrtEnvironment> {
@@ -200,7 +206,7 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
200206
}
201207

202208
const {
203-
name,
209+
name: nameFlag,
204210
region,
205211
production: isProduction,
206212
hostname,
@@ -209,8 +215,12 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
209215
'allow-cookies': allowCookies,
210216
'enable-source-maps': enableSourceMaps,
211217
proxy: proxyStrings,
218+
wait,
212219
} = this.flags;
213220

221+
// Default name to slug if not provided
222+
const name = nameFlag ?? slug;
223+
214224
// Parse proxy configurations
215225
const proxyConfigs = proxyStrings?.map((p) => parseProxyString(p));
216226

@@ -219,7 +229,7 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
219229
);
220230

221231
try {
222-
const result = await createEnv(
232+
let result = await createEnv(
223233
{
224234
projectSlug: project,
225235
slug,
@@ -237,6 +247,32 @@ export default class MrtEnvCreate extends MrtCommand<typeof MrtEnvCreate> {
237247
this.getMrtAuth(),
238248
);
239249

250+
// Wait for environment to be ready if requested
251+
if (wait) {
252+
this.log(t('commands.mrt.env.create.waiting', 'Waiting for environment "{{slug}}" to be ready...', {slug}));
253+
254+
const waitStartTime = Date.now();
255+
result = await waitForEnv(
256+
{
257+
projectSlug: project,
258+
slug,
259+
origin: this.resolvedConfig.mrtOrigin,
260+
onPoll: (env) => {
261+
if (!this.jsonEnabled()) {
262+
const elapsed = Math.round((Date.now() - waitStartTime) / 1000);
263+
this.log(
264+
t('commands.mrt.env.create.state', '[{{elapsed}}s] State: {{state}}', {
265+
elapsed: String(elapsed),
266+
state: env.state ?? 'unknown',
267+
}),
268+
);
269+
}
270+
},
271+
},
272+
this.getMrtAuth(),
273+
);
274+
}
275+
240276
if (this.jsonEnabled()) {
241277
return result;
242278
}

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

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
253253
const startTime = Date.now();
254254
const pollIntervalMs = pollIntervalSeconds * 1000;
255255
const timeoutMs = timeoutSeconds * 1000;
256-
let lastState: SandboxState | undefined;
257256

258257
this.log(t('commands.ods.create.waiting', 'Waiting for sandbox to be ready...'));
259258

@@ -285,17 +284,14 @@ export default class OdsCreate extends OdsCommand<typeof OdsCreate> {
285284
const sandbox = result.data.data;
286285
const currentState = sandbox.state as SandboxState;
287286

288-
// Log state changes
289-
if (currentState !== lastState) {
290-
const elapsed = Math.round((Date.now() - startTime) / 1000);
291-
this.log(
292-
t('commands.ods.create.stateChange', '[{{elapsed}}s] State: {{state}}', {
293-
elapsed: String(elapsed),
294-
state: currentState || 'unknown',
295-
}),
296-
);
297-
lastState = currentState;
298-
}
287+
// Log current state on each poll
288+
const elapsed = Math.round((Date.now() - startTime) / 1000);
289+
this.log(
290+
t('commands.ods.create.stateChange', '[{{elapsed}}s] State: {{state}}', {
291+
elapsed: String(elapsed),
292+
state: currentState || 'unknown',
293+
}),
294+
);
299295

300296
// Check for terminal states
301297
if (currentState && TERMINAL_STATES.has(currentState)) {

packages/b2c-tooling-sdk/src/operations/mrt/env.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import {getLogger} from '../../logging/logger.js';
2020
*/
2121
export type MrtEnvironment = components['schemas']['APITargetV2Create'];
2222

23+
/**
24+
* Environment state from the MRT API.
25+
*/
26+
export type MrtEnvironmentState = components['schemas']['StateEnum'];
27+
2328
type SsrRegion = components['schemas']['SsrRegionEnum'];
2429
type LogLevel = components['schemas']['LogLevelEnum'];
2530

@@ -271,3 +276,173 @@ export async function deleteEnv(options: DeleteEnvOptions, auth: AuthStrategy):
271276

272277
logger.debug({slug}, '[MRT] Environment deleted successfully');
273278
}
279+
280+
/**
281+
* Options for getting an MRT environment.
282+
*/
283+
export interface GetEnvOptions {
284+
/**
285+
* The project slug containing the environment.
286+
*/
287+
projectSlug: string;
288+
289+
/**
290+
* Environment slug/identifier to retrieve.
291+
*/
292+
slug: string;
293+
294+
/**
295+
* MRT API origin URL.
296+
* @default "https://cloud.mobify.com"
297+
*/
298+
origin?: string;
299+
}
300+
301+
/**
302+
* Gets an environment (target) from an MRT project.
303+
*
304+
* @param options - Environment retrieval options
305+
* @param auth - Authentication strategy (ApiKeyStrategy)
306+
* @returns The environment object from the API
307+
* @throws Error if retrieval fails
308+
*
309+
* @example
310+
* ```typescript
311+
* import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth';
312+
* import { getEnv } from '@salesforce/b2c-tooling-sdk/operations/mrt';
313+
*
314+
* const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization');
315+
*
316+
* const env = await getEnv({
317+
* projectSlug: 'my-storefront',
318+
* slug: 'staging'
319+
* }, auth);
320+
*
321+
* console.log(`Environment state: ${env.state}`);
322+
* ```
323+
*/
324+
export async function getEnv(options: GetEnvOptions, auth: AuthStrategy): Promise<MrtEnvironment> {
325+
const logger = getLogger();
326+
const {projectSlug, slug, origin} = options;
327+
328+
logger.debug({projectSlug, slug}, '[MRT] Getting environment');
329+
330+
const client = createMrtClient({origin: origin || DEFAULT_MRT_ORIGIN}, auth);
331+
332+
const {data, error} = await client.GET('/api/projects/{project_slug}/target/{target_slug}/', {
333+
params: {
334+
path: {project_slug: projectSlug, target_slug: slug},
335+
},
336+
});
337+
338+
if (error) {
339+
const errorMessage =
340+
typeof error === 'object' && error !== null && 'message' in error
341+
? String((error as {message: unknown}).message)
342+
: JSON.stringify(error);
343+
throw new Error(`Failed to get environment: ${errorMessage}`);
344+
}
345+
346+
logger.debug({slug: data.slug, state: data.state}, '[MRT] Environment retrieved');
347+
348+
return data;
349+
}
350+
351+
/**
352+
* Terminal states for MRT environments (no longer changing).
353+
*/
354+
const TERMINAL_STATES: MrtEnvironmentState[] = ['ACTIVE', 'CREATE_FAILED', 'PUBLISH_FAILED'];
355+
356+
/**
357+
* Options for waiting for an MRT environment to be ready.
358+
*/
359+
export interface WaitForEnvOptions extends GetEnvOptions {
360+
/**
361+
* Polling interval in milliseconds.
362+
* @default 10000
363+
*/
364+
pollInterval?: number;
365+
366+
/**
367+
* Maximum time to wait in milliseconds.
368+
* @default 2700000 (45 minutes)
369+
*/
370+
timeout?: number;
371+
372+
/**
373+
* Optional callback called on each poll with the current environment state.
374+
*/
375+
onPoll?: (env: MrtEnvironment) => void;
376+
}
377+
378+
/**
379+
* Waits for an environment to reach a terminal state (ACTIVE or failed).
380+
*
381+
* Polls the environment status until it reaches ACTIVE, CREATE_FAILED,
382+
* or PUBLISH_FAILED state, or until the timeout is reached.
383+
*
384+
* @param options - Wait options including polling interval and timeout
385+
* @param auth - Authentication strategy (ApiKeyStrategy)
386+
* @returns The environment in its terminal state
387+
* @throws Error if timeout is reached or environment fails
388+
*
389+
* @example
390+
* ```typescript
391+
* import { ApiKeyStrategy } from '@salesforce/b2c-tooling-sdk/auth';
392+
* import { createEnv, waitForEnv } from '@salesforce/b2c-tooling-sdk/operations/mrt';
393+
*
394+
* const auth = new ApiKeyStrategy(process.env.MRT_API_KEY!, 'Authorization');
395+
*
396+
* // Create environment
397+
* const env = await createEnv({
398+
* projectSlug: 'my-storefront',
399+
* slug: 'staging',
400+
* name: 'Staging'
401+
* }, auth);
402+
*
403+
* // Wait for it to be ready
404+
* const readyEnv = await waitForEnv({
405+
* projectSlug: 'my-storefront',
406+
* slug: 'staging',
407+
* timeout: 60000, // 1 minute
408+
* onPoll: (e) => console.log(`State: ${e.state}`)
409+
* }, auth);
410+
*
411+
* if (readyEnv.state === 'ACTIVE') {
412+
* console.log('Environment is ready!');
413+
* }
414+
* ```
415+
*/
416+
export async function waitForEnv(options: WaitForEnvOptions, auth: AuthStrategy): Promise<MrtEnvironment> {
417+
const logger = getLogger();
418+
const {projectSlug, slug, pollInterval = 10000, timeout = 2700000, onPoll, origin} = options;
419+
420+
logger.debug({projectSlug, slug, pollInterval, timeout}, '[MRT] Waiting for environment');
421+
422+
const startTime = Date.now();
423+
424+
while (Date.now() - startTime < timeout) {
425+
const env = await getEnv({projectSlug, slug, origin}, auth);
426+
427+
if (onPoll) {
428+
onPoll(env);
429+
}
430+
431+
if (env.state && TERMINAL_STATES.includes(env.state as MrtEnvironmentState)) {
432+
if (env.state === 'CREATE_FAILED') {
433+
throw new Error(`Environment creation failed`);
434+
}
435+
if (env.state === 'PUBLISH_FAILED') {
436+
throw new Error(`Environment publish failed`);
437+
}
438+
logger.debug({slug, state: env.state}, '[MRT] Environment reached terminal state');
439+
return env;
440+
}
441+
442+
logger.debug({slug, state: env.state, elapsed: Date.now() - startTime}, '[MRT] Environment still in progress');
443+
444+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
445+
}
446+
447+
throw new Error(`Timeout waiting for environment "${slug}" to be ready after ${timeout}ms`);
448+
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,12 @@ export type {
6565
} from './env-var.js';
6666

6767
// Environment (target) operations
68-
export {createEnv, deleteEnv} from './env.js';
69-
export type {CreateEnvOptions, DeleteEnvOptions, MrtEnvironment} from './env.js';
68+
export {createEnv, deleteEnv, getEnv, waitForEnv} from './env.js';
69+
export type {
70+
CreateEnvOptions,
71+
DeleteEnvOptions,
72+
GetEnvOptions,
73+
WaitForEnvOptions,
74+
MrtEnvironment,
75+
MrtEnvironmentState,
76+
} from './env.js';

0 commit comments

Comments
 (0)