From b81a7c8872308965d5e5b2dd6f4c5750246efbdc Mon Sep 17 00:00:00 2001 From: soridalac Date: Fri, 13 Dec 2024 09:27:10 -0800 Subject: [PATCH 01/13] feat: update mso --- src/commands/org/create/sandbox.ts | 49 ++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index daa6dd29..15647473 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -6,12 +6,14 @@ */ import { Duration } from '@salesforce/kit'; +import { MultiStageOutput } from '@oclif/multi-stage-output'; import { Flags } from '@salesforce/sf-plugins-core'; -import { Lifecycle, Messages, SandboxEvents, SandboxRequest, SfError } from '@salesforce/core'; +import { Lifecycle, Messages, SandboxEvents, SandboxProcessObject, SandboxRequest, SfError } from '@salesforce/core'; import { Interfaces } from '@oclif/core'; import requestFunctions, { readSandboxDefFile } from '../../../shared/sandboxRequest.js'; import { SandboxCommandBase, SandboxCommandResponse } from '../../../shared/sandboxCommandBase.js'; import { SandboxLicenseType } from '../../../shared/orgTypes.js'; +import sandboxRequest from '../../../shared/sandboxRequest.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'create.sandbox'); @@ -124,6 +126,7 @@ export default class CreateSandbox extends SandboxCommandBase({ + stages: this.flags.async + ? ['Prepare request', 'Send request', 'Done'] + : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], + title: this.flags.async ? 'Create Sandbox (async)': 'Create Sandbox', + jsonEnabled: false, + postStagesBlock: [ + { + label: 'SandboxId', + get: (data) => data?.id, + type: 'dynamic-key-value', + bold: true + }, + { + label: 'Status', + get: (data) => data?.status, + type: 'dynamic-key-value', + bold: true + }, + ] + }); + + mso.goto('Prepare request', {status: 'Pending'}); + if (!this.flags.async) { - this.spinner.start('Sandbox Create'); + mso.skipTo('Send request'); } this.debug('Calling create with SandboxRequest: %s ', sandboxReq); try { + mso.goto('Send request', {status: 'In Progress'}); const sandboxProcessObject = await this.prodOrg.createSandbox(sandboxReq, { wait: this.flags.wait, interval: this.flags['poll-interval'], async: this.flags.async, }); + console.log('Sandbox Process Object', sandboxProcessObject); + + mso.updateData({status: sandboxProcessObject.Status, id: sandboxProcessObject.Id}); + + if (sandboxProcessObject.Status === 'In Progress') { + mso.goto('Waiting for org to respond', { status: 'In Progress', id: sandboxProcessObject.Id}); + } + this.latestSandboxProgressObj = sandboxProcessObject; this.saveSandboxProgressConfig(); + + mso.goto('Done', {status: sandboxProcessObject.Status, id: sandboxProcessObject.Id}); if (this.flags.async) { + mso.goto('Waiting for org to respond', { status: 'Pending', id: sandboxProcessObject.Id}); + + mso.skipTo('Done', {status: sandboxProcessObject.Status, id: sandboxProcessObject.Id}); + mso.stop(); process.exitCode = 68; } return this.getSandboxCommandResponse(); } catch (err) { - this.spinner.stop(); + mso.error() + mso.stop(); if (this.pollingTimeOut && this.latestSandboxProgressObj) { void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined); process.exitCode = 68; From 4ce625468e378e633cea481b5c960052aa843839 Mon Sep 17 00:00:00 2001 From: soridalac Date: Fri, 13 Dec 2024 09:29:20 -0800 Subject: [PATCH 02/13] feat: update mso --- src/commands/org/create/sandbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index 15647473..ec35c3a0 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -243,7 +243,7 @@ export default class CreateSandbox extends SandboxCommandBase Date: Mon, 16 Dec 2024 09:19:53 -0800 Subject: [PATCH 03/13] fix: mso for sandbox --- src/commands/org/create/sandbox.ts | 44 ++++++++++++++---------------- src/shared/sandboxCommandBase.ts | 21 +++++++++++++- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index ec35c3a0..2a98adac 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -8,12 +8,11 @@ import { Duration } from '@salesforce/kit'; import { MultiStageOutput } from '@oclif/multi-stage-output'; import { Flags } from '@salesforce/sf-plugins-core'; -import { Lifecycle, Messages, SandboxEvents, SandboxProcessObject, SandboxRequest, SfError } from '@salesforce/core'; +import { Lifecycle, Messages, SandboxEvents, SandboxRequest, SfError } from '@salesforce/core'; import { Interfaces } from '@oclif/core'; import requestFunctions, { readSandboxDefFile } from '../../../shared/sandboxRequest.js'; import { SandboxCommandBase, SandboxCommandResponse } from '../../../shared/sandboxCommandBase.js'; import { SandboxLicenseType } from '../../../shared/orgTypes.js'; -import sandboxRequest from '../../../shared/sandboxRequest.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'create.sandbox'); @@ -126,7 +125,7 @@ export default class CreateSandbox extends SandboxCommandBase({ - stages: this.flags.async - ? ['Prepare request', 'Send request', 'Done'] - : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], - title: this.flags.async ? 'Create Sandbox (async)': 'Create Sandbox', + const mso = new MultiStageOutput<{ status: string; id: string }>({ + stages: this.flags.async + ? ['Prepare request', 'Send request', 'Done'] + : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], + title: this.flags.async ? 'Create Sandbox (async)' : 'Create Sandbox', jsonEnabled: false, postStagesBlock: [ { label: 'SandboxId', get: (data) => data?.id, type: 'dynamic-key-value', - bold: true + bold: true, }, { label: 'Status', get: (data) => data?.status, type: 'dynamic-key-value', - bold: true + bold: true, }, - ] + ], }); - mso.goto('Prepare request', {status: 'Pending'}); + mso.goto('Prepare request', { status: 'Pending' }); if (!this.flags.async) { - mso.skipTo('Send request'); + mso.skipTo('Sandbox Create', { status: 'Pending' }); } this.debug('Calling create with SandboxRequest: %s ', sandboxReq); try { - mso.goto('Send request', {status: 'In Progress'}); + mso.goto('Send request', { status: 'In Progress' }); const sandboxProcessObject = await this.prodOrg.createSandbox(sandboxReq, { wait: this.flags.wait, interval: this.flags['poll-interval'], @@ -245,27 +244,26 @@ export default class CreateSandbox extends SandboxCommandBase extends SfCommand { ): void { lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; + this.warn('Sandbox creation process time out.'); + + if (this.latestSandboxProgressObj) { + const sandboxId = this.latestSandboxProgressObj.Id; + if (sandboxId) { + this.info(`Sandbox ID: ${sandboxId}`); + } + } return Promise.resolve(this.updateSandboxRequestData()); }); @@ -131,6 +139,12 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; + const sandboxId = this.latestSandboxProgressObj?.Id; + + if (sandboxId) { + this.info(`Sandbox ID: ${sandboxId}`); + } + this.updateSandboxRequestData(); const progress = this.sandboxProgress.getSandboxProgress(results); const currentStage = progress.status; @@ -144,7 +158,12 @@ export abstract class SandboxCommandBase extends SfCommand { }); lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { - this.latestSandboxProgressObj = results.sandboxProcessObj; + // this.latestSandboxProgressObj = results.sandboxProcessObj; + const sandboxId = results.sandboxProcessObj?.Id; + + if (sandboxId) { + this.info(`Sandbox created with ID: ${sandboxId}`); + } this.updateSandboxRequestData(); this.sandboxProgress.markPreviousStagesAsCompleted(); this.updateProgress(results, options.isAsync); From 1732d412a47b9bf49f9dc4c1190493503eb713af Mon Sep 17 00:00:00 2001 From: soridalac Date: Mon, 16 Dec 2024 12:00:58 -0800 Subject: [PATCH 04/13] fix: mso --- src/commands/org/create/sandbox.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index 2a98adac..217a7e25 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -213,7 +213,7 @@ export default class CreateSandbox extends SandboxCommandBase data?.id, type: 'dynamic-key-value', bold: true, @@ -246,18 +246,13 @@ export default class CreateSandbox extends SandboxCommandBase Date: Tue, 17 Dec 2024 10:21:21 -0800 Subject: [PATCH 05/13] fix: mso --- src/commands/org/create/sandbox.ts | 76 +++++++++++++++--------------- src/shared/sandboxCommandBase.ts | 73 ++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 52 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index 217a7e25..dad5d0de 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -6,7 +6,6 @@ */ import { Duration } from '@salesforce/kit'; -import { MultiStageOutput } from '@oclif/multi-stage-output'; import { Flags } from '@salesforce/sf-plugins-core'; import { Lifecycle, Messages, SandboxEvents, SandboxRequest, SfError } from '@salesforce/core'; import { Interfaces } from '@oclif/core'; @@ -192,51 +191,52 @@ export default class CreateSandbox extends SandboxCommandBase({ - stages: this.flags.async - ? ['Prepare request', 'Send request', 'Done'] - : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], - title: this.flags.async ? 'Create Sandbox (async)' : 'Create Sandbox', - jsonEnabled: false, - postStagesBlock: [ - { - label: 'Sandbox ID', - get: (data) => data?.id, - type: 'dynamic-key-value', - bold: true, - }, - { - label: 'Status', - get: (data) => data?.status, - type: 'dynamic-key-value', - bold: true, - }, - ], + this.registerLifecycleListeners(lifecycle, { + isAsync: this.flags.async, + setDefault: this.flags['set-default'], + alias: this.flags.alias, + prodOrg: this.prodOrg, + tracksSource: this.flags['no-track-source'] === true ? false : undefined, }); - - mso.goto('Prepare request', { status: 'Pending' }); + // const mso = new MultiStageOutput<{ status: string; id: string }>({ + // stages: this.flags.async + // ? ['Prepare request', 'Send request', 'Done'] + // : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], + // title: this.flags.async ? 'Create Sandbox (async)' : 'Create Sandbox', + // jsonEnabled: false, + // postStagesBlock: [ + // { + // label: 'Sandbox ID', + // get: (data) => data?.id, + // type: 'dynamic-key-value', + // bold: true, + // }, + // { + // label: 'Status', + // get: (data) => data?.status, + // type: 'dynamic-key-value', + // bold: true, + // }, + // ], + // }); + + // this.mso.goto('Prepare request', { status: 'Pending' }); if (!this.flags.async) { - mso.skipTo('Sandbox Create', { status: 'Pending' }); + this.spinner.start('Sandbox Create'); + // this.mso.skipTo('Sandbox Create', { status: 'Pending' }); } this.debug('Calling create with SandboxRequest: %s ', sandboxReq); try { - mso.goto('Send request', { status: 'In Progress' }); + // this.mso.goto('Send request', { status: 'In Progress' }); const sandboxProcessObject = await this.prodOrg.createSandbox(sandboxReq, { wait: this.flags.wait, interval: this.flags['poll-interval'], @@ -244,21 +244,21 @@ export default class CreateSandbox extends SandboxCommandBase extends SfCommand { : this.constructor.name === 'CreateSandbox' ? 'Create' : 'Create/Refresh'; + this.sandboxProgress = new SandboxProgress({ action: this.action }); } protected async getSandboxRequestConfig(): Promise { @@ -89,6 +96,34 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle: Lifecycle, options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean } ): void { + const mso = new MultiStageOutput({ + stages: ['Creating new sandbox', 'Authenticating'], + title: 'Create Sandbox', + jsonEnabled: false, + postStagesBlock: [ + { + label: 'Sandbox ID', + get: (data): string | undefined => data?.id, + type: 'dynamic-key-value', + bold: true, + }, + { + label: 'Status', + get: (data): string | undefined => data?.status, + type: 'dynamic-key-value', + bold: true, + }, + { + label: 'Copy Progress', + get: (data): string | undefined => { + if (data?.sandboxStatus.sandboxProcessObj.CopyProgress) + return `${data?.sandboxStatus.sandboxProcessObj.CopyProgress}`; + }, + type: 'dynamic-key-value', + bold: true, + }, + ], + }); lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; this.warn('Sandbox creation process time out.'); @@ -97,6 +132,7 @@ export abstract class SandboxCommandBase extends SfCommand { const sandboxId = this.latestSandboxProgressObj.Id; if (sandboxId) { this.info(`Sandbox ID: ${sandboxId}`); + mso.updateData({ status: this.latestSandboxProgressObj.Status }); } } return Promise.resolve(this.updateSandboxRequestData()); @@ -114,7 +150,7 @@ export abstract class SandboxCommandBase extends SfCommand { this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; this.updateSandboxRequestData(); if (!options.isAsync) { - this.spinner.stop(); + // this.spinner.stop(); } // things that require data on latestSandboxProgressObj if (this.latestSandboxProgressObj) { @@ -124,7 +160,7 @@ export abstract class SandboxCommandBase extends SfCommand { }); const currentStage = progress.status; this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); - this.updateStage(currentStage, 'inProgress'); + // this.updateStage(currentStage, 'inProgress'); this.updateProgress( { sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined }, options.isAsync @@ -140,15 +176,15 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; const sandboxId = this.latestSandboxProgressObj?.Id; - + // results.sandboxProcessObj.Status; if (sandboxId) { this.info(`Sandbox ID: ${sandboxId}`); } this.updateSandboxRequestData(); - const progress = this.sandboxProgress.getSandboxProgress(results); - const currentStage = progress.status; - this.updateStage(currentStage, 'inProgress'); + // const progress = this.sandboxProgress.getSandboxProgress(results); + // const currentStage = progress.status; + // this.updateStage(currentStage, 'inProgress'); return Promise.resolve(this.updateProgress(results, options.isAsync)); }); @@ -231,16 +267,25 @@ export abstract class SandboxCommandBase extends SfCommand { sandboxProcessObj: event.sandboxProcessObj, }; if (!isAsync) { - this.spinner.status = this.sandboxProgress.formatProgressStatus(); + // this.spinner.status = this.sandboxProgress.formatProgressStatus(); } - } - protected updateStage(stage: string | undefined, state: State): void { - if (stage) { - this.sandboxProgress.transitionStages(stage, state); - } + // Dynamically update MultiStageOutput status + // const currentStage = this.sandboxProgress.getSandboxProgress(event)?.status; + // this.mso.updateStage(currentStage, 'inProgress'); + + // if (event.sandboxProcessObj) { + // this.mso.updateProgress(event.sandboxProcessObj, true); + // } } + // protected updateStage(stage: string | undefined, state: State): void { + // if (stage) { + // this.sandboxProgress.transitionStages(stage, state); + // this.mso.updateData(stage, state); + // } + // } + protected updateSandboxRequestData(): void { if (this.sandboxRequestData && this.latestSandboxProgressObj) { this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj; From c46d2bc2be68b30c68eed56c01b884fb97447e85 Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 17 Dec 2024 13:55:17 -0800 Subject: [PATCH 06/13] feat: update mso --- src/shared/sandboxCommandBase.ts | 88 +++++++++++--------------------- src/shared/sandboxProgress.ts | 10 ++-- 2 files changed, 36 insertions(+), 62 deletions(-) diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index bcd3b023..7abac720 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -22,7 +22,7 @@ import { StatusEvent, } from '@salesforce/core'; import { SandboxProgress } from './sandboxProgress.js'; -// import { State } from './stagedProgress.js'; +import { State } from './stagedProgress.js'; type SandboxData = { sandboxStatus: StatusEvent; @@ -97,7 +97,7 @@ export abstract class SandboxCommandBase extends SfCommand { options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean } ): void { const mso = new MultiStageOutput({ - stages: ['Creating new sandbox', 'Authenticating'], + stages: ['Creating new sandbox', 'Authenticating', 'Done'], title: 'Create Sandbox', jsonEnabled: false, postStagesBlock: [ @@ -113,28 +113,12 @@ export abstract class SandboxCommandBase extends SfCommand { type: 'dynamic-key-value', bold: true, }, - { - label: 'Copy Progress', - get: (data): string | undefined => { - if (data?.sandboxStatus.sandboxProcessObj.CopyProgress) - return `${data?.sandboxStatus.sandboxProcessObj.CopyProgress}`; - }, - type: 'dynamic-key-value', - bold: true, - }, ], }); lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; - this.warn('Sandbox creation process time out.'); - - if (this.latestSandboxProgressObj) { - const sandboxId = this.latestSandboxProgressObj.Id; - if (sandboxId) { - this.info(`Sandbox ID: ${sandboxId}`); - mso.updateData({ status: this.latestSandboxProgressObj.Status }); - } - } + mso.goto('Done'); + mso.updateData({ status: 'Polling timeout', id: 'N/A' }); return Promise.resolve(this.updateSandboxRequestData()); }); @@ -143,6 +127,9 @@ export abstract class SandboxCommandBase extends SfCommand { this.sandboxProgress.markPreviousStagesAsCompleted( results.Status !== 'Completed' ? results.Status : 'Authenticating' ); + mso.goto('Authenticating'); + mso.updateData({ status: 'Resuming process', id: results.Id }); + return Promise.resolve(this.updateSandboxRequestData()); }); @@ -150,7 +137,7 @@ export abstract class SandboxCommandBase extends SfCommand { this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; this.updateSandboxRequestData(); if (!options.isAsync) { - // this.spinner.stop(); + mso.stop(); } // things that require data on latestSandboxProgressObj if (this.latestSandboxProgressObj) { @@ -160,11 +147,10 @@ export abstract class SandboxCommandBase extends SfCommand { }); const currentStage = progress.status; this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); - // this.updateStage(currentStage, 'inProgress'); - this.updateProgress( - { sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined }, - options.isAsync - ); + this.updateStage(currentStage, 'inProgress'); + mso.goto('Creating new sandbox'); + mso.updateData({ status: currentStage, id: this.latestSandboxProgressObj?.Id }); + mso.goto(progress.status); } if (this.pollingTimeOut) { this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()])); @@ -175,16 +161,13 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; - const sandboxId = this.latestSandboxProgressObj?.Id; - // results.sandboxProcessObj.Status; - if (sandboxId) { - this.info(`Sandbox ID: ${sandboxId}`); - } - this.updateSandboxRequestData(); - // const progress = this.sandboxProgress.getSandboxProgress(results); - // const currentStage = progress.status; - // this.updateStage(currentStage, 'inProgress'); + const progress = this.sandboxProgress.getSandboxProgress(results); + const currentStage = progress.status; + this.updateStage(currentStage, 'inProgress'); + mso.goto('Authenticating'); + mso.updateData({ status: currentStage, id: results.sandboxProcessObj.Id }); + return Promise.resolve(this.updateProgress(results, options.isAsync)); }); @@ -194,18 +177,15 @@ export abstract class SandboxCommandBase extends SfCommand { }); lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { - // this.latestSandboxProgressObj = results.sandboxProcessObj; - const sandboxId = results.sandboxProcessObj?.Id; - - if (sandboxId) { - this.info(`Sandbox created with ID: ${sandboxId}`); - } + this.latestSandboxProgressObj = results.sandboxProcessObj; this.updateSandboxRequestData(); this.sandboxProgress.markPreviousStagesAsCompleted(); this.updateProgress(results, options.isAsync); + if (!options.isAsync) { - this.progress.stop(); + mso.stop(); } + if (results.sandboxRes?.authUserName) { const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName }); await authInfo.handleAliasAndDefaultSettings({ @@ -218,6 +198,9 @@ export abstract class SandboxCommandBase extends SfCommand { this.removeSandboxProgressConfig(); this.updateProgress(results, options.isAsync); this.reportResults(results); + + mso.goto('Done'); + mso.updateData({ status: 'Completed', id: results.sandboxProcessObj.Id }); }); lifecycle.on(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, async (results: SandboxProcessObject[]) => { @@ -267,24 +250,15 @@ export abstract class SandboxCommandBase extends SfCommand { sandboxProcessObj: event.sandboxProcessObj, }; if (!isAsync) { - // this.spinner.status = this.sandboxProgress.formatProgressStatus(); + this.spinner.status = this.sandboxProgress.formatProgressStatus(); } - - // Dynamically update MultiStageOutput status - // const currentStage = this.sandboxProgress.getSandboxProgress(event)?.status; - // this.mso.updateStage(currentStage, 'inProgress'); - - // if (event.sandboxProcessObj) { - // this.mso.updateProgress(event.sandboxProcessObj, true); - // } } - // protected updateStage(stage: string | undefined, state: State): void { - // if (stage) { - // this.sandboxProgress.transitionStages(stage, state); - // this.mso.updateData(stage, state); - // } - // } + protected updateStage(stage: string | undefined, state: State): void { + if (stage) { + this.sandboxProgress.transitionStages(stage, state); + } + } protected updateSandboxRequestData(): void { if (this.sandboxRequestData && this.latestSandboxProgressObj) { diff --git a/src/shared/sandboxProgress.ts b/src/shared/sandboxProgress.ts index 6b6a97c2..69d4b762 100644 --- a/src/shared/sandboxProgress.ts +++ b/src/shared/sandboxProgress.ts @@ -66,17 +66,17 @@ export class SandboxProgress extends StagedProgress { } public formatProgressStatus(withClock = true): string { - const table = getSandboxTableAsText(undefined, this.statusData?.sandboxProcessObj); + // const table = getSandboxTableAsText(undefined, this.statusData?.sandboxProcessObj); return [ withClock && this.statusData ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout. ${ this.statusData.sandboxProgress.percentComplete ?? 0 }%` : undefined, - table, - '---------------------', - `Sandbox ${this.action ?? ''} Stages`, - this.formatStages(), + // table, + // '---------------------', + // `Sandbox ${this.action ?? ''} Stages`, + // this.formatStages(), ] .filter(isDefined) .join(os.EOL); From 8c394b36cc4f21989bceb62aaaecdaec38c7561b Mon Sep 17 00:00:00 2001 From: soridalac Date: Tue, 17 Dec 2024 13:57:52 -0800 Subject: [PATCH 07/13] fix: delete comment --- src/commands/org/create/sandbox.ts | 36 ------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index dad5d0de..7d9ada0e 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -188,7 +188,6 @@ export default class CreateSandbox extends SandboxCommandBase { const lifecycle = Lifecycle.getInstance(); - this.prodOrg = this.flags['target-org']; const sandboxReq = await this.createSandboxRequest(); @@ -204,61 +203,26 @@ export default class CreateSandbox extends SandboxCommandBase({ - // stages: this.flags.async - // ? ['Prepare request', 'Send request', 'Done'] - // : ['Prepare request', 'Send request', 'Waiting for org to respond', 'Done'], - // title: this.flags.async ? 'Create Sandbox (async)' : 'Create Sandbox', - // jsonEnabled: false, - // postStagesBlock: [ - // { - // label: 'Sandbox ID', - // get: (data) => data?.id, - // type: 'dynamic-key-value', - // bold: true, - // }, - // { - // label: 'Status', - // get: (data) => data?.status, - // type: 'dynamic-key-value', - // bold: true, - // }, - // ], - // }); - - // this.mso.goto('Prepare request', { status: 'Pending' }); if (!this.flags.async) { this.spinner.start('Sandbox Create'); - // this.mso.skipTo('Sandbox Create', { status: 'Pending' }); } - this.debug('Calling create with SandboxRequest: %s ', sandboxReq); try { - // this.mso.goto('Send request', { status: 'In Progress' }); const sandboxProcessObject = await this.prodOrg.createSandbox(sandboxReq, { wait: this.flags.wait, interval: this.flags['poll-interval'], async: this.flags.async, }); - // console.log('Sandbox Process Object', sandboxProcessObject); - - // this.mso.updateData({ status: sandboxProcessObject.Status, id: sandboxProcessObject.Id }); - this.latestSandboxProgressObj = sandboxProcessObject; this.saveSandboxProgressConfig(); - // this.mso.goto('Done', { status: sandboxProcessObject.Status, id: sandboxProcessObject.Id }); if (this.flags.async) { - // this.mso.goto('Waiting for org to respond', { status: 'Pending', id: sandboxProcessObject.Id }); - // this.mso.skipTo('Done'); - // this.mso.stop(); process.exitCode = 68; } return this.getSandboxCommandResponse(); } catch (err) { - // this.mso.error(); if (this.pollingTimeOut && this.latestSandboxProgressObj) { void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined); process.exitCode = 68; From a69235ca380a260826f6ce7879b331b2899ef318 Mon Sep 17 00:00:00 2001 From: soridalac Date: Wed, 18 Dec 2024 21:21:53 -0800 Subject: [PATCH 08/13] fix: update mso resume --- src/shared/sandboxCommandBase.ts | 136 ++++++++++++++++++++++++++----- src/shared/sandboxProgress.ts | 9 +- 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index 7abac720..4da431d2 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -28,6 +28,7 @@ type SandboxData = { sandboxStatus: StatusEvent; status: string; id: string; + copyProgress: number; }; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); @@ -98,27 +99,30 @@ export abstract class SandboxCommandBase extends SfCommand { ): void { const mso = new MultiStageOutput({ stages: ['Creating new sandbox', 'Authenticating', 'Done'], - title: 'Create Sandbox', + title: 'Sandbox Process', jsonEnabled: false, postStagesBlock: [ { - label: 'Sandbox ID', - get: (data): string | undefined => data?.id, + label: 'Status', + get: (data) => data?.status, type: 'dynamic-key-value', bold: true, }, { - label: 'Status', - get: (data): string | undefined => data?.status, + label: 'Sandbox ID', + get: (data) => data?.id, + type: 'static-key-value', + }, + { + label: 'Copy Progress', + get: (data) => `${data?.copyProgress ?? 0}%`, type: 'dynamic-key-value', - bold: true, }, ], }); lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; - mso.goto('Done'); - mso.updateData({ status: 'Polling timeout', id: 'N/A' }); + mso.updateData({ status: 'Polling Timeout' }); return Promise.resolve(this.updateSandboxRequestData()); }); @@ -127,8 +131,25 @@ export abstract class SandboxCommandBase extends SfCommand { this.sandboxProgress.markPreviousStagesAsCompleted( results.Status !== 'Completed' ? results.Status : 'Authenticating' ); - mso.goto('Authenticating'); - mso.updateData({ status: 'Resuming process', id: results.Id }); + mso.goto('Creating new sandbox'); + mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); + if (results.Status === 'Activating') { + mso.goto('Authenticating'); + mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); + } else if (results.Status === 'Completed') { + mso.goto('Done'); + mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); + mso.stop(); + } + + if (results.Status !== 'Completed' && !this.pollingTimeOut) { + setTimeout(() => { + this.pollingTimeOut = true; + mso.updateData({ status: 'Polling Timeout' }); + mso.stop(); + }, 15 * 60 * 1000); + return Promise.resolve(this.updateSandboxRequestData()); + } return Promise.resolve(this.updateSandboxRequestData()); }); @@ -137,8 +158,40 @@ export abstract class SandboxCommandBase extends SfCommand { this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; this.updateSandboxRequestData(); if (!options.isAsync) { - mso.stop(); + mso.goto('Authenticating'); + mso.updateData({ + status: results?.Status, + id: results?.Id, + copyProgress: results?.CopyProgress, + }); + if (results?.Status !== 'Completed' && !this.pollingTimeOut) { + setTimeout(() => { + this.pollingTimeOut = true; + mso.updateData({ status: 'Polling Timeout' }); + mso.stop(); + }, 15 * 60 * 1000); + } + // mso.stop(); } + // if (!options.isAsync) { + // if (results?.Status === 'Activating') { + // mso.goto('Authenticating'); + // mso.updateData({ + // status: results.Status, + // id: results.Id, + // copyProgress: results.CopyProgress + // }); + // } else if (results?.Status === 'Completed') { + // mso.goto('Done'); + // mso.updateData({ + // status: 'Completed', + // id: results.Id, + // copyProgress: results.CopyProgress + // }); + // mso.stop(); + // } + // mso.stop(); + // } // things that require data on latestSandboxProgressObj if (this.latestSandboxProgressObj) { const progress = this.sandboxProgress.getSandboxProgress({ @@ -149,9 +202,14 @@ export abstract class SandboxCommandBase extends SfCommand { this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); this.updateStage(currentStage, 'inProgress'); mso.goto('Creating new sandbox'); - mso.updateData({ status: currentStage, id: this.latestSandboxProgressObj?.Id }); + mso.updateData({ + status: currentStage, + id: this.latestSandboxProgressObj?.Id, + copyProgress: this.latestSandboxProgressObj?.CopyProgress, + }); mso.goto(progress.status); } + if (this.pollingTimeOut) { this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()])); } @@ -165,9 +223,13 @@ export abstract class SandboxCommandBase extends SfCommand { const progress = this.sandboxProgress.getSandboxProgress(results); const currentStage = progress.status; this.updateStage(currentStage, 'inProgress'); - mso.goto('Authenticating'); - mso.updateData({ status: currentStage, id: results.sandboxProcessObj.Id }); + mso.goto('Authenticating'); + mso.updateData({ + status: currentStage, + id: results.sandboxProcessObj.Id, + copyProgress: results.sandboxProcessObj.CopyProgress, + }); return Promise.resolve(this.updateProgress(results, options.isAsync)); }); @@ -183,9 +245,48 @@ export abstract class SandboxCommandBase extends SfCommand { this.updateProgress(results, options.isAsync); if (!options.isAsync) { - mso.stop(); - } + mso.goto('Authenticating'); + mso.updateData({ + status: results.sandboxProcessObj.Status, + id: results.sandboxProcessObj.Id, + copyProgress: results.sandboxProcessObj.CopyProgress, + }); + if (results.sandboxProcessObj.Status !== 'Completed' && !this.pollingTimeOut) { + setTimeout(() => { + this.pollingTimeOut = true; + mso.updateData({ status: 'Polling Timeout' }); + mso.stop(); + }, 15 * 60 * 1000); + } + // mso.stop(); + } + // if (!options.isAsync) { + // if (results.sandboxProcessObj.Status === 'Activating') { + // mso.goto('Authenticating'); + // mso.updateData({ + // status: results.sandboxProcessObj.Status, + // id: results.sandboxProcessObj.Id, + // copyProgress: results.sandboxProcessObj.CopyProgress + // }); + // } else if (results.sandboxProcessObj.Status === 'Completed') { + // mso.goto('Done'); + // mso.updateData({ + // status: 'Completed', + // id: results.sandboxProcessObj.Id, + // copyProgress: results.sandboxProcessObj.CopyProgress + // }); + // mso.stop(); + // } + // mso.stop(); + // } + mso.goto('Done'); + mso.updateData({ + status: 'Completed', + id: results.sandboxProcessObj.Id, + copyProgress: results.sandboxProcessObj.CopyProgress, + }); + mso.stop(); if (results.sandboxRes?.authUserName) { const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName }); await authInfo.handleAliasAndDefaultSettings({ @@ -198,9 +299,6 @@ export abstract class SandboxCommandBase extends SfCommand { this.removeSandboxProgressConfig(); this.updateProgress(results, options.isAsync); this.reportResults(results); - - mso.goto('Done'); - mso.updateData({ status: 'Completed', id: results.sandboxProcessObj.Id }); }); lifecycle.on(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, async (results: SandboxProcessObject[]) => { diff --git a/src/shared/sandboxProgress.ts b/src/shared/sandboxProgress.ts index 69d4b762..59bb9905 100644 --- a/src/shared/sandboxProgress.ts +++ b/src/shared/sandboxProgress.ts @@ -66,17 +66,10 @@ export class SandboxProgress extends StagedProgress { } public formatProgressStatus(withClock = true): string { - // const table = getSandboxTableAsText(undefined, this.statusData?.sandboxProcessObj); return [ withClock && this.statusData - ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout. ${ - this.statusData.sandboxProgress.percentComplete ?? 0 - }%` + ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout.` : undefined, - // table, - // '---------------------', - // `Sandbox ${this.action ?? ''} Stages`, - // this.formatStages(), ] .filter(isDefined) .join(os.EOL); From 9e0a008713b1ce2a14cecd1c06c7e99cf0badf3f Mon Sep 17 00:00:00 2001 From: soridalac Date: Fri, 20 Dec 2024 08:29:32 -0800 Subject: [PATCH 09/13] feat: update mso with create/refresh/resume sandbox --- src/commands/org/create/sandbox.ts | 3 - src/commands/org/refresh/sandbox.ts | 4 -- src/commands/org/resume/sandbox.ts | 4 -- src/shared/sandboxCommandBase.ts | 98 ++++------------------------- 4 files changed, 13 insertions(+), 96 deletions(-) diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index 7d9ada0e..a49b2040 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -204,9 +204,6 @@ export default class CreateSandbox extends SandboxCommandBase 0) { - this.spinner.start(`Resume ${this.sandboxRequestData.action ?? 'Create/Refresh'}`); - } - this.debug('Calling resume with ResumeSandboxRequest: %s ', sandboxReq); try { diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index 4da431d2..a58f5bf3 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -128,11 +128,7 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => { this.latestSandboxProgressObj = results; - this.sandboxProgress.markPreviousStagesAsCompleted( - results.Status !== 'Completed' ? results.Status : 'Authenticating' - ); - mso.goto('Creating new sandbox'); - mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); + if (results.Status === 'Activating') { mso.goto('Authenticating'); mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); @@ -141,57 +137,17 @@ export abstract class SandboxCommandBase extends SfCommand { mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); mso.stop(); } - - if (results.Status !== 'Completed' && !this.pollingTimeOut) { - setTimeout(() => { - this.pollingTimeOut = true; - mso.updateData({ status: 'Polling Timeout' }); - mso.stop(); - }, 15 * 60 * 1000); - return Promise.resolve(this.updateSandboxRequestData()); - } - return Promise.resolve(this.updateSandboxRequestData()); }); lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => { this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; this.updateSandboxRequestData(); + if (!options.isAsync) { - mso.goto('Authenticating'); - mso.updateData({ - status: results?.Status, - id: results?.Id, - copyProgress: results?.CopyProgress, - }); - if (results?.Status !== 'Completed' && !this.pollingTimeOut) { - setTimeout(() => { - this.pollingTimeOut = true; - mso.updateData({ status: 'Polling Timeout' }); - mso.stop(); - }, 15 * 60 * 1000); - } - // mso.stop(); + mso.stop(); } - // if (!options.isAsync) { - // if (results?.Status === 'Activating') { - // mso.goto('Authenticating'); - // mso.updateData({ - // status: results.Status, - // id: results.Id, - // copyProgress: results.CopyProgress - // }); - // } else if (results?.Status === 'Completed') { - // mso.goto('Done'); - // mso.updateData({ - // status: 'Completed', - // id: results.Id, - // copyProgress: results.CopyProgress - // }); - // mso.stop(); - // } - // mso.stop(); - // } + // things that require data on latestSandboxProgressObj if (this.latestSandboxProgressObj) { const progress = this.sandboxProgress.getSandboxProgress({ @@ -224,13 +180,13 @@ export abstract class SandboxCommandBase extends SfCommand { const currentStage = progress.status; this.updateStage(currentStage, 'inProgress'); - mso.goto('Authenticating'); + mso.goto('Creating new sandbox'); mso.updateData({ status: currentStage, id: results.sandboxProcessObj.Id, copyProgress: results.sandboxProcessObj.CopyProgress, }); - return Promise.resolve(this.updateProgress(results, options.isAsync)); + return Promise.resolve(this.updateProgress(results)); }); lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => { @@ -242,44 +198,20 @@ export abstract class SandboxCommandBase extends SfCommand { this.latestSandboxProgressObj = results.sandboxProcessObj; this.updateSandboxRequestData(); this.sandboxProgress.markPreviousStagesAsCompleted(); - this.updateProgress(results, options.isAsync); + this.updateProgress(results); if (!options.isAsync) { + mso.stop(); + } + + if (results.sandboxProcessObj.Status === 'Authenticating') { mso.goto('Authenticating'); mso.updateData({ status: results.sandboxProcessObj.Status, id: results.sandboxProcessObj.Id, copyProgress: results.sandboxProcessObj.CopyProgress, }); - - if (results.sandboxProcessObj.Status !== 'Completed' && !this.pollingTimeOut) { - setTimeout(() => { - this.pollingTimeOut = true; - mso.updateData({ status: 'Polling Timeout' }); - mso.stop(); - }, 15 * 60 * 1000); - } - // mso.stop(); } - // if (!options.isAsync) { - // if (results.sandboxProcessObj.Status === 'Activating') { - // mso.goto('Authenticating'); - // mso.updateData({ - // status: results.sandboxProcessObj.Status, - // id: results.sandboxProcessObj.Id, - // copyProgress: results.sandboxProcessObj.CopyProgress - // }); - // } else if (results.sandboxProcessObj.Status === 'Completed') { - // mso.goto('Done'); - // mso.updateData({ - // status: 'Completed', - // id: results.sandboxProcessObj.Id, - // copyProgress: results.sandboxProcessObj.CopyProgress - // }); - // mso.stop(); - // } - // mso.stop(); - // } mso.goto('Done'); mso.updateData({ status: 'Completed', @@ -297,7 +229,7 @@ export abstract class SandboxCommandBase extends SfCommand { }); } this.removeSandboxProgressConfig(); - this.updateProgress(results, options.isAsync); + this.updateProgress(results); this.reportResults(results); }); @@ -337,8 +269,7 @@ export abstract class SandboxCommandBase extends SfCommand { } protected updateProgress( - event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }), - isAsync: boolean + event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }) ): void { const sandboxProgress = this.sandboxProgress.getSandboxProgress(event); this.sandboxUsername = (event as ResultEvent).sandboxRes?.authUserName; @@ -347,9 +278,6 @@ export abstract class SandboxCommandBase extends SfCommand { sandboxProgress, sandboxProcessObj: event.sandboxProcessObj, }; - if (!isAsync) { - this.spinner.status = this.sandboxProgress.formatProgressStatus(); - } } protected updateStage(stage: string | undefined, state: State): void { From 458b27152523d90c7b2639d41c35e50b73f4ebe6 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Wed, 19 Feb 2025 18:49:38 -0300 Subject: [PATCH 10/13] chore: refactor --- messages/sandboxbase.md | 5 +- src/commands/org/create/sandbox.ts | 5 +- src/commands/org/refresh/sandbox.ts | 10 +- src/commands/org/resume/sandbox.ts | 14 ++- src/shared/sandboxCommandBase.ts | 181 ++++++++-------------------- src/shared/sandboxProgress.ts | 128 -------------------- src/shared/sandboxReporter.ts | 49 -------- src/shared/sandboxStages.ts | 77 ++++++++++++ src/shared/stagedProgress.ts | 128 -------------------- src/shared/timeUtils.ts | 43 ------- test/shared/sandboxProgress.test.ts | 84 ------------- test/shared/sandboxReporter.test.ts | 123 ------------------- test/shared/stagedProgress.test.ts | 104 ---------------- test/shared/timeUtils.test.ts | 46 ------- 14 files changed, 152 insertions(+), 845 deletions(-) delete mode 100644 src/shared/sandboxProgress.ts delete mode 100644 src/shared/sandboxReporter.ts create mode 100644 src/shared/sandboxStages.ts delete mode 100644 src/shared/stagedProgress.ts delete mode 100644 src/shared/timeUtils.ts delete mode 100644 test/shared/sandboxProgress.test.ts delete mode 100644 test/shared/sandboxReporter.test.ts delete mode 100644 test/shared/stagedProgress.test.ts delete mode 100644 test/shared/timeUtils.test.ts diff --git a/messages/sandboxbase.md b/messages/sandboxbase.md index c18581da..4e39a40b 100644 --- a/messages/sandboxbase.md +++ b/messages/sandboxbase.md @@ -1,11 +1,10 @@ # sandboxSuccess -The sandbox org %s was successful. +Your sandbox is ready. # sandboxSuccess.actions -The username for the sandbox is %s. -You can open the org by running "%s org open -o %s" +You can open it by running "%s org open -o %s" # checkSandboxStatus diff --git a/src/commands/org/create/sandbox.ts b/src/commands/org/create/sandbox.ts index a49b2040..b00fd885 100644 --- a/src/commands/org/create/sandbox.ts +++ b/src/commands/org/create/sandbox.ts @@ -196,7 +196,10 @@ export default class CreateSandbox extends SandboxCommandBase; const sce = entries.find(([, e]) => e?.sandboxProcessObject?.Id === this.flags['job-id'])?.[1]; sandboxRequestCacheEntry = sce; + if (sandboxRequestCacheEntry === undefined) { + this.warn( + `Could not find a cache entry for ${this.flags['job-id']}.${EOL}If you are resuming a sandbox operation from a different machine note that we cannot set the alias/set-default flag values as those are saved locally.` + ); + } } // If the action is in the cache entry, use it. if (sandboxRequestCacheEntry?.action) { this.action = sandboxRequestCacheEntry?.action; - this.sandboxProgress.action = sandboxRequestCacheEntry?.action; } return { diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index 1c187e86..5fffc897 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -5,7 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import os from 'node:os'; -import { MultiStageOutput } from '@oclif/multi-stage-output'; import { SfCommand } from '@salesforce/sf-plugins-core'; import { Config } from '@oclif/core'; import { @@ -21,15 +20,7 @@ import { SandboxUserAuthResponse, StatusEvent, } from '@salesforce/core'; -import { SandboxProgress } from './sandboxProgress.js'; -import { State } from './stagedProgress.js'; - -type SandboxData = { - sandboxStatus: StatusEvent; - status: string; - id: string; - copyProgress: number; -}; +import { SandboxStages } from './sandboxStages.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'sandboxbase'); @@ -39,7 +30,7 @@ export type SandboxCommandResponse = SandboxProcessObject & { }; export abstract class SandboxCommandBase extends SfCommand { - protected sandboxProgress: SandboxProgress; + protected stages!: SandboxStages; protected latestSandboxProgressObj?: SandboxProcessObject; protected sandboxAuth?: SandboxUserAuthResponse; protected prodOrg?: Org; @@ -54,11 +45,9 @@ export abstract class SandboxCommandBase extends SfCommand { this.action = this.constructor.name === 'RefreshSandbox' ? 'Refresh' - : this.constructor.name === 'CreateSandbox' + : ['CreateSandbox', 'ResumeSandbox'].includes(this.constructor.name) ? 'Create' : 'Create/Refresh'; - - this.sandboxProgress = new SandboxProgress({ action: this.action }); } protected async getSandboxRequestConfig(): Promise { if (!this.sandboxRequestConfig) { @@ -93,103 +82,69 @@ export abstract class SandboxCommandBase extends SfCommand { return true; } - protected registerLifecycleListeners( + protected registerLifecycleListenersAndMSO( lifecycle: Lifecycle, - options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean } + options: { + mso: { title: string; refresh?: boolean }; + isAsync: boolean; + alias?: string; + setDefault?: boolean; + prodOrg?: Org; + tracksSource?: boolean; + } ): void { - const mso = new MultiStageOutput({ - stages: ['Creating new sandbox', 'Authenticating', 'Done'], - title: 'Sandbox Process', - jsonEnabled: false, - postStagesBlock: [ - { - label: 'Status', - get: (data) => data?.status, - type: 'dynamic-key-value', - bold: true, - }, - { - label: 'Sandbox ID', - get: (data) => data?.id, - type: 'static-key-value', - }, - { - label: 'Copy Progress', - get: (data) => `${data?.copyProgress ?? 0}%`, - type: 'dynamic-key-value', - }, - ], + this.stages = new SandboxStages({ + refresh: options.mso.refresh ?? false, + jsonEnabled: this.jsonEnabled(), + title: options.isAsync ? `${options.mso.title} (async)` : options.mso.title, }); + + this.stages.start(); + lifecycle.on('POLLING_TIME_OUT', async () => { this.pollingTimeOut = true; - mso.updateData({ status: 'Polling Timeout' }); + this.stages.stop(); return Promise.resolve(this.updateSandboxRequestData()); }); lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => { + this.stages.start(); this.latestSandboxProgressObj = results; + this.stages.update(this.latestSandboxProgressObj); - if (results.Status === 'Activating') { - mso.goto('Authenticating'); - mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); - } else if (results.Status === 'Completed') { - mso.goto('Done'); - mso.updateData({ status: results.Status, id: results.Id, copyProgress: results.CopyProgress }); - mso.stop(); - } return Promise.resolve(this.updateSandboxRequestData()); }); - lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => { - this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj; - this.updateSandboxRequestData(); - - if (!options.isAsync) { - mso.stop(); - } - - // things that require data on latestSandboxProgressObj - if (this.latestSandboxProgressObj) { - const progress = this.sandboxProgress.getSandboxProgress({ - sandboxProcessObj: this.latestSandboxProgressObj, - sandboxRes: undefined, - }); - const currentStage = progress.status; - this.sandboxProgress.markPreviousStagesAsCompleted(currentStage); - this.updateStage(currentStage, 'inProgress'); - mso.goto('Creating new sandbox'); - mso.updateData({ - status: currentStage, - id: this.latestSandboxProgressObj?.Id, - copyProgress: this.latestSandboxProgressObj?.CopyProgress, - }); - mso.goto(progress.status); + lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject | undefined) => { + // this event is fired by commands on poll timeout without any payload, + // we want to make sure to only update state if there's payload (event from sfdx-core). + if (results) { + this.latestSandboxProgressObj = results; + this.stages.update(this.latestSandboxProgressObj); + this.updateSandboxRequestData(); } + this.stages.stop('async'); if (this.pollingTimeOut) { this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()])); } - this.log(this.sandboxProgress.formatProgressStatus(false)); return Promise.resolve(this.info(messages.getMessage('checkSandboxStatus', this.getCheckSandboxStatusParams()))); }); lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => { + // this starts MSO for: + // * org create/create sandbox + this.stages.start(); this.latestSandboxProgressObj = results.sandboxProcessObj; this.updateSandboxRequestData(); - const progress = this.sandboxProgress.getSandboxProgress(results); - const currentStage = progress.status; - this.updateStage(currentStage, 'inProgress'); - - mso.goto('Creating new sandbox'); - mso.updateData({ - status: currentStage, - id: results.sandboxProcessObj.Id, - copyProgress: results.sandboxProcessObj.CopyProgress, - }); - return Promise.resolve(this.updateProgress(results)); + + this.stages.update(this.latestSandboxProgressObj); + + return Promise.resolve(); }); lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => { + this.stages.auth(); this.sandboxAuth = results; return Promise.resolve(); }); @@ -197,28 +152,9 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; this.updateSandboxRequestData(); - this.sandboxProgress.markPreviousStagesAsCompleted(); - this.updateProgress(results); - if (!options.isAsync) { - mso.stop(); - } + this.stages.update(results.sandboxProcessObj); - if (results.sandboxProcessObj.Status === 'Authenticating') { - mso.goto('Authenticating'); - mso.updateData({ - status: results.sandboxProcessObj.Status, - id: results.sandboxProcessObj.Id, - copyProgress: results.sandboxProcessObj.CopyProgress, - }); - } - mso.goto('Done'); - mso.updateData({ - status: 'Completed', - id: results.sandboxProcessObj.Id, - copyProgress: results.sandboxProcessObj.CopyProgress, - }); - mso.stop(); if (results.sandboxRes?.authUserName) { const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName }); await authInfo.handleAliasAndDefaultSettings({ @@ -228,8 +164,9 @@ export abstract class SandboxCommandBase extends SfCommand { setTracksSource: await this.calculateTrackingSetting(options.tracksSource), }); } + this.stages.stop(); + this.removeSandboxProgressConfig(); - this.updateProgress(results); this.reportResults(results); }); @@ -253,39 +190,14 @@ export abstract class SandboxCommandBase extends SfCommand { } protected reportResults(results: ResultEvent): void { - this.log(); - this.styledHeader(`Sandbox Org ${this.action} Status`); - this.log(this.sandboxProgress.formatProgressStatus(false)); this.logSuccess( [ - messages.getMessage('sandboxSuccess', [this.action.toLowerCase()]), - messages.getMessages('sandboxSuccess.actions', [ - results.sandboxRes?.authUserName, - this.config.bin, - results.sandboxRes?.authUserName, - ]), + messages.getMessage('sandboxSuccess'), + messages.getMessages('sandboxSuccess.actions', [this.config.bin, results.sandboxRes?.authUserName]), ].join(os.EOL) ); } - protected updateProgress( - event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }) - ): void { - const sandboxProgress = this.sandboxProgress.getSandboxProgress(event); - this.sandboxUsername = (event as ResultEvent).sandboxRes?.authUserName; - this.sandboxProgress.statusData = { - sandboxUsername: this.sandboxUsername, - sandboxProgress, - sandboxProcessObj: event.sandboxProcessObj, - }; - } - - protected updateStage(stage: string | undefined, state: State): void { - if (stage) { - this.sandboxProgress.transitionStages(stage, state); - } - } - protected updateSandboxRequestData(): void { if (this.sandboxRequestData && this.latestSandboxProgressObj) { this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj; @@ -326,6 +238,13 @@ export abstract class SandboxCommandBase extends SfCommand { return { ...(this.latestSandboxProgressObj as SandboxProcessObject), SandboxUsername: sbxUsername }; } + protected catch(error: Error): Promise { + if (this.stages) { + this.stages.stop('failed'); + } + + return super.catch(error); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async finally(_: Error | undefined): Promise { const lifecycle = Lifecycle.getInstance(); diff --git a/src/shared/sandboxProgress.ts b/src/shared/sandboxProgress.ts deleted file mode 100644 index 59bb9905..00000000 --- a/src/shared/sandboxProgress.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import os from 'node:os'; -import { colorize } from '@oclif/core/ux'; -import { StatusEvent, ResultEvent, SandboxProcessObject } from '@salesforce/core'; -import { getClockForSeconds } from '../shared/timeUtils.js'; -import { StagedProgress } from './stagedProgress.js'; -import { isDefined } from './utils.js'; - -export type SandboxProgressData = { - id: string; - status: string; - percentComplete?: number; - remainingWaitTime: number; - remainingWaitTimeHuman: string; -}; - -export type SandboxStatusData = { - sandboxUsername: string; - sandboxProgress: SandboxProgressData; - sandboxProcessObj?: SandboxProcessObject | undefined; -}; - -export type SandboxProgressConfig = { - stageNames?: string[]; - action?: 'Create' | 'Refresh' | 'Create/Refresh'; -}; - -export class SandboxProgress extends StagedProgress { - public action: SandboxProgressConfig['action']; - - public constructor(config?: SandboxProgressConfig) { - const stageNames = config?.stageNames ?? ['Pending', 'Processing', 'Activating', 'Authenticating']; - super(stageNames); - this.action = config?.action ?? 'Create/Refresh'; - } - // eslint-disable-next-line class-methods-use-this - public getLogSandboxProcessResult(result: ResultEvent): string { - const { sandboxProcessObj } = result; - const sandboxReadyForUse = `Sandbox ${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id}) is ready for use.`; - return sandboxReadyForUse; - } - - // eslint-disable-next-line class-methods-use-this - public getSandboxProgress( - // sometimes an undefined sandboxRes is passed in - event: StatusEvent | (Omit & { sandboxRes?: ResultEvent['sandboxRes'] }) - ): SandboxProgressData { - const waitingOnAuth = 'waitingOnAuth' in event ? event.waitingOnAuth : false; - const { sandboxProcessObj } = event; - const waitTimeInSec = 'remainingWait' in event ? event.remainingWait ?? 0 : 0; - - const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`; - - return { - id: sandboxIdentifierMsg, - status: waitingOnAuth || sandboxProcessObj.Status === 'Completed' ? 'Authenticating' : sandboxProcessObj.Status, - percentComplete: sandboxProcessObj.CopyProgress, - remainingWaitTime: waitTimeInSec, - remainingWaitTimeHuman: waitTimeInSec === 0 ? '' : `${getClockForSeconds(waitTimeInSec)} until timeout.`, - }; - } - - public formatProgressStatus(withClock = true): string { - return [ - withClock && this.statusData - ? `${getClockForSeconds(this.statusData.sandboxProgress.remainingWaitTime)} until timeout.` - : undefined, - ] - .filter(isDefined) - .join(os.EOL); - } - // eslint-disable-next-line class-methods-use-this - protected mapCurrentStage(currentStage: string): string { - switch (currentStage) { - case 'Pending Remote Creation': - return 'Pending'; - case 'Remote Sandbox Created': - return 'Pending'; - case 'Completed': - return 'Authenticating'; - default: - return currentStage; - } - } -} - -export const getTableDataFromProcessObj = ( - sandboxProcessObj: SandboxProcessObject, - authUserName?: string | undefined -): Array<{ key: string; value: string | number }> => [ - { key: 'Id', value: sandboxProcessObj.Id }, - { key: 'SandboxName', value: sandboxProcessObj.SandboxName }, - { key: 'Status', value: sandboxProcessObj.Status }, - { key: 'LicenseType', value: sandboxProcessObj.LicenseType }, - { key: 'SandboxInfoId', value: sandboxProcessObj.SandboxInfoId }, - { key: 'Created Date', value: sandboxProcessObj.CreatedDate }, - { key: 'CopyProgress', value: `${sandboxProcessObj.CopyProgress ?? 0}%` }, - ...(sandboxProcessObj.SourceId ? [{ key: 'SourceId', value: sandboxProcessObj.SourceId }] : []), - ...(sandboxProcessObj.SandboxOrganization - ? [{ key: 'SandboxOrg', value: sandboxProcessObj.SandboxOrganization }] - : []), - ...(sandboxProcessObj.ApexClassId ? [{ key: 'ApexClassId', value: sandboxProcessObj.ApexClassId }] : []), - ...(sandboxProcessObj.Description ? [{ key: 'Description', value: sandboxProcessObj.Description }] : []), - ...(authUserName ? [{ key: 'Authorized Sandbox Username', value: authUserName }] : []), -]; - -export const getSandboxTableAsText = (sandboxUsername?: string, sandboxProgress?: SandboxProcessObject): string => { - if (!sandboxProgress) { - return ''; - } - - const data = getTableDataFromProcessObj(sandboxProgress, sandboxUsername); - const longestKey = data.reduce((acc, row) => (row.key.length > acc ? row.key.length : acc), 0); - const longestValue = data.reduce( - (acc, row) => (row.value.toString().length > acc ? row.value.toString().length : acc), - 0 - ); - return [ - colorize('bold', `${'Field'.padEnd(longestKey)} Value`), - `${'-'.repeat(longestKey)} ${'-'.repeat(longestValue)}`, - ...data.map((row) => `${row.key.padEnd(longestKey)} ${row.value}`), - ].join(os.EOL); -}; diff --git a/src/shared/sandboxReporter.ts b/src/shared/sandboxReporter.ts deleted file mode 100644 index 6ca2f606..00000000 --- a/src/shared/sandboxReporter.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { StatusEvent, ResultEvent } from '@salesforce/core'; -import { getSecondsToHuman } from './timeUtils.js'; - -export class SandboxReporter { - public static sandboxProgress(update: StatusEvent): string { - const { remainingWait, interval, sandboxProcessObj, waitingOnAuth } = update; - - const waitTime: string = getSecondsToHuman(remainingWait); - const waitTimeMsg = `Sleeping ${interval} seconds. Will wait ${waitTime} more before timing out.`; - const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`; - const waitingOnAuthMessage: string = waitingOnAuth ? ', waiting on JWT auth' : ''; - const completionMessage = sandboxProcessObj.CopyProgress - ? `(${sandboxProcessObj.CopyProgress}% completed${waitingOnAuthMessage})` - : ''; - - return `Sandbox request ${sandboxIdentifierMsg} is ${sandboxProcessObj.Status} ${completionMessage}. ${waitTimeMsg}`; - } - - public static logSandboxProcessResult( - result: ResultEvent - // sandboxProcessObj.CopyProgress is a number - ): { sandboxReadyForUse: string; data: Array<{ key: string; value: string | number | undefined }> } { - const { sandboxProcessObj, sandboxRes } = result; - const sandboxReadyForUse = `Sandbox ${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id}) is ready for use.`; - - const data = [ - { key: 'Id', value: sandboxProcessObj.Id }, - { key: 'SandboxName', value: sandboxProcessObj.SandboxName }, - { key: 'Status', value: sandboxProcessObj.Status }, - { key: 'CopyProgress', value: sandboxProcessObj.CopyProgress }, - { key: 'Description', value: sandboxProcessObj.Description }, - { key: 'LicenseType', value: sandboxProcessObj.LicenseType }, - { key: 'SandboxInfoId', value: sandboxProcessObj.SandboxInfoId }, - { key: 'SourceId', value: sandboxProcessObj.SourceId }, - { key: 'SandboxOrg', value: sandboxProcessObj.SandboxOrganization }, - { key: 'Created Date', value: sandboxProcessObj.CreatedDate }, - { key: 'ApexClassId', value: sandboxProcessObj.ApexClassId }, - { key: 'Authorized Sandbox Username', value: sandboxRes.authUserName }, - ]; - - return { sandboxReadyForUse, data }; - } -} diff --git a/src/shared/sandboxStages.ts b/src/shared/sandboxStages.ts new file mode 100644 index 00000000..9b4ec1bf --- /dev/null +++ b/src/shared/sandboxStages.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { MultiStageOutput } from '@oclif/multi-stage-output'; +import { SandboxProcessObject } from '@salesforce/core'; +import { StageStatus } from 'node_modules/@oclif/multi-stage-output/lib/stage-tracker.js'; + +type Options = { + title: string; + jsonEnabled: boolean; + refresh: boolean; +}; + +export class SandboxStages { + private mso: MultiStageOutput; + private refresh: boolean; + + public constructor({ title, jsonEnabled, refresh = false }: Options) { + this.refresh = refresh; + this.mso = new MultiStageOutput({ + stages: ['Creating new sandbox', 'Refreshing org', 'Authenticating'], + title, + jsonEnabled, + stageSpecificBlock: [ + { + get: (data): string | undefined => data?.SandboxName, + stage: 'Creating new sandbox', + label: 'Name', + type: 'dynamic-key-value', + }, + { + get: (data): string | undefined => data?.Id, + stage: 'Creating new sandbox', + label: 'ID', + type: 'dynamic-key-value', + }, + ], + postStagesBlock: [ + { + label: 'Status', + get: (data): string | undefined => data?.Status, + type: 'dynamic-key-value', + bold: true, + }, + { + label: 'Copy progress', + get: (data): string | undefined => `${data?.CopyProgress ?? 0}%`, + type: 'dynamic-key-value', + }, + ], + }); + } + + public start(): void { + if (this.refresh) { + this.mso.skipTo('Refreshing org'); + } else { + this.mso.goto('Creating new sandbox'); + } + } + + public auth(): void { + this.mso.goto('Authenticating'); + } + + public update(data: SandboxProcessObject): void { + this.mso.updateData(data); + } + + public stop(finalStatus?: StageStatus): void { + this.mso.stop(finalStatus); + } +} diff --git a/src/shared/stagedProgress.ts b/src/shared/stagedProgress.ts deleted file mode 100644 index 02be37fe..00000000 --- a/src/shared/stagedProgress.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import os from 'node:os'; -import ansis, { type Ansis } from 'ansis'; -import { StandardColors } from '@salesforce/sf-plugins-core'; -import { SfError } from '@salesforce/core'; - -const compareStages = ([, aValue]: [string, StageAttributes], [, bValue]: [string, StageAttributes]): number => - aValue.index - bValue.index; - -export const boldPurple = ansis.rgb(157, 129, 221).bold; - -export type State = 'inProgress' | 'completed' | 'failed' | 'unknown'; - -export type StageAttributes = { - state: State; - char: string; - color: Ansis; - index: number; - visited: boolean; -}; - -export const StateConstants: { [stage: string]: Omit } = { - inProgress: { color: boldPurple, char: '…', visited: false, state: 'inProgress' }, - completed: { color: StandardColors.success, char: '✓', visited: false, state: 'completed' }, - failed: { color: ansis.bold.red, char: '✖', visited: false, state: 'failed' }, - unknown: { color: ansis.dim, char: '…', visited: false, state: 'unknown' }, -}; - -export type Stage = { - [stage: string]: StageAttributes; -}; - -export abstract class StagedProgress { - private dataForTheStatus: T | undefined; - private theStages: Stage; - private currentStage?: string; - private previousStage?: string; - public constructor(stages: string[]) { - this.theStages = stages - .map((stage, index) => ({ - [stage]: { ...StateConstants['unknown'], index: (index + 1) * 10 }, - })) - .reduce((m, b) => Object.assign(m, b), {}); - } - - public get statusData(): T | undefined { - return this.dataForTheStatus; - } - public set statusData(statusData: T | undefined) { - this.dataForTheStatus = statusData; - } - - public formatStages(): string { - return Object.entries(this.theStages) - .sort(compareStages) - .map(([stage, stageState]) => stageState.color(`${stageState.char} - ${stage}`)) - .join(os.EOL); - } - - public transitionStages(currentStage: string, newState: State): void { - currentStage = this.mapCurrentStage(currentStage); - if (this.previousStage && this.previousStage !== currentStage) { - this.updateStages(this.previousStage, 'completed'); - } - - // mark all previous stages as visited and completed - this.markPreviousStagesAsCompleted(currentStage); - - this.previousStage = currentStage; - this.currentStage = currentStage; - this.updateStages(currentStage, newState); - } - - public markPreviousStagesAsCompleted(currentStage?: string): void { - if (currentStage) { - currentStage = this.mapCurrentStage(currentStage); - } - Object.entries(this.theStages).forEach(([stage, stageState]) => { - if (!currentStage || stageState.index < (this.theStages[currentStage]?.index ?? 0)) { - this.updateStages(stage, 'completed'); - } - }); - } - - public updateCurrentStage(newState: State): void { - if (!this.currentStage) { - throw new SfError('transitionStages must be called before updateCurrentStage'); - } - this.updateStages(this.currentStage, newState); - } - - public updateStages(currentStage: string, newState?: State): void { - currentStage = this.mapCurrentStage(currentStage); - if (!this.theStages[currentStage]) { - const sortedEntries = Object.entries(this.theStages).sort(compareStages); - const visitedEntries = sortedEntries.filter(([, stageState]) => stageState.visited); - const [, lastState] = visitedEntries.length - ? visitedEntries[visitedEntries.length - 1] - : ['', { state: StateConstants.unknown.state, index: 0, visited: true }]; - const newEntry = { - [currentStage]: { state: StateConstants.unknown.state, visited: true, index: lastState.index + 1 }, - }; - this.theStages = Object.assign(this.theStages, newEntry); - } - this.theStages[currentStage].visited = true; - this.theStages[currentStage].state = newState ?? 'inProgress'; - this.theStages[currentStage].char = StateConstants[this.theStages[currentStage].state].char; - if (newState) { - this.theStages[currentStage].color = StateConstants[newState.toString()].color; - } - } - - public getStages(): Stage { - return this.theStages; - } - - // eslint-disable-next-line class-methods-use-this - protected mapCurrentStage(currentStage: string): string { - return currentStage; - } - - public abstract formatProgressStatus(withClock: boolean): string; -} diff --git a/src/shared/timeUtils.ts b/src/shared/timeUtils.ts deleted file mode 100644 index 6e6d309c..00000000 --- a/src/shared/timeUtils.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2022, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { Duration } from '@salesforce/kit'; - -export type TimeComponents = { - days: Duration; - hours: Duration; - minutes: Duration; - seconds: Duration; -}; - -export const getClockForSeconds = (timeInSec: number): string => { - const tc = getTimeComponentsFromSeconds(timeInSec); - - const dDisplay: string = tc.days.days > 0 ? `${tc.days.days.toString()}:` : ''; - const hDisplay: string = tc.hours.hours.toString().padStart(2, '0'); - const mDisplay: string = tc.minutes.minutes.toString().padStart(2, '0'); - const sDisplay: string = tc.seconds.seconds.toString().padStart(2, '0'); - - return `${dDisplay}${hDisplay}:${mDisplay}:${sDisplay}`; -}; -export const getTimeComponentsFromSeconds = (timeInSec: number): TimeComponents => { - const days = Duration.days(Math.floor(timeInSec / 86_400)); - const hours = Duration.hours(Math.floor((timeInSec % 86_400) / 3600)); - const minutes = Duration.minutes(Math.floor((timeInSec % 3600) / 60)); - const seconds = Duration.seconds(Math.floor(timeInSec % 60)); - - return { days, hours, minutes, seconds }; -}; -export const getSecondsToHuman = (timeInSec: number): string => { - const tc = getTimeComponentsFromSeconds(timeInSec); - - const dDisplay: string = tc.days.days > 0 ? tc.days.toString() + ' ' : ''; - const hDisplay: string = tc.hours.hours > 0 ? tc.hours.toString() + ' ' : ''; - const mDisplay: string = tc.minutes.minutes > 0 ? tc.minutes.toString() + ' ' : ''; - const sDisplay: string = tc.seconds.seconds > 0 ? tc.seconds.toString() : ''; - - return (dDisplay + hDisplay + mDisplay + sDisplay).trim(); -}; diff --git a/test/shared/sandboxProgress.test.ts b/test/shared/sandboxProgress.test.ts deleted file mode 100644 index cdb59700..00000000 --- a/test/shared/sandboxProgress.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import { Duration } from '@salesforce/cli-plugins-testkit'; -import { SandboxProcessObject, StatusEvent } from '@salesforce/core'; -import { - SandboxProgress, - getTableDataFromProcessObj, - getSandboxTableAsText, -} from '../../src/shared/sandboxProgress.js'; - -const sandboxProcessObj: SandboxProcessObject = { - Id: '0GR4p000000U8EMXXX', - Status: 'Completed', - SandboxName: 'TestSandbox', - SandboxInfoId: '0GQ4p000000U6sKXXX', - LicenseType: 'DEVELOPER', - CreatedDate: '2021-12-07T16:20:21.000+0000', - CopyProgress: 100, - SandboxOrganization: '00D2f0000008XXX', - SourceId: '123', - Description: 'sandbox description', - ApexClassId: '123', - EndDate: '2021-12-07T16:38:47.000+0000', -}; - -describe('sandbox progress', () => { - let sandboxProgress: SandboxProgress; - beforeEach(() => { - sandboxProgress = new SandboxProgress(); - }); - describe('getSandboxProgress', () => { - it('will calculate the correct human readable message (1h 33min 00seconds seconds left)', () => { - const data: StatusEvent = { - // 186*30 = 5580 = 1 hour, 33 min, 0 seconds. so 186 attempts left, at a 30 second polling interval - sandboxProcessObj, - interval: 30, - remainingWait: Duration.minutes(93).seconds, - waitingOnAuth: false, - }; - const res = sandboxProgress.getSandboxProgress(data); - expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); - expect(res).to.have.property('status', 'Authenticating'); - expect(res).to.have.property('percentComplete', 100); - expect(res).to.have.property('remainingWaitTimeHuman', '01:33:00 until timeout.'); - }); - - it('will calculate the correct human readable message (5 min 30seconds seconds left)', () => { - const data: StatusEvent = { - sandboxProcessObj, - interval: 30, - remainingWait: Duration.minutes(5).seconds + Duration.seconds(30).seconds, - waitingOnAuth: false, - }; - const res = sandboxProgress.getSandboxProgress(data); - expect(res).to.have.property('id', 'TestSandbox(0GR4p000000U8EMXXX)'); - expect(res).to.have.property('status', 'Authenticating'); - expect(res).to.have.property('percentComplete', 100); - expect(res).to.have.property('remainingWaitTimeHuman', '00:05:30 until timeout.'); - }); - }); - - describe('getTableDataFromProcessObj', () => { - it('getTableDataFromProcessObj should work', () => { - const tableData = getTableDataFromProcessObj(sandboxProcessObj, 'admin@prod.org.sandbox'); - expect(tableData.find((r) => r.key === 'Authorized Sandbox Username')).to.have.property( - 'value', - 'admin@prod.org.sandbox' - ); - expect(tableData.find((r) => r.key === 'SandboxInfoId')).to.have.property('value', '0GQ4p000000U6sKXXX'); - }); - }); - describe('getSandboxTableAsText', () => { - it('getSandboxTableAsText should work', () => { - const tableData = getSandboxTableAsText('admin@prod.org.sandbox', sandboxProcessObj); - expect(tableData).to.include('Authorized Sandbox Username'); - expect(tableData).to.include('admin@prod.org.sandbox'); - }); - }); -}); diff --git a/test/shared/sandboxReporter.test.ts b/test/shared/sandboxReporter.test.ts deleted file mode 100644 index d5014819..00000000 --- a/test/shared/sandboxReporter.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ - -import { expect, config } from 'chai'; -import { StatusEvent, SandboxProcessObject, SandboxUserAuthResponse } from '@salesforce/core'; -import { SandboxReporter } from '../../src/shared/sandboxReporter.js'; - -config.truncateThreshold = 0; - -const sandboxProcessObj: SandboxProcessObject = { - Id: '0GR4p000000U8EMXXX', - Status: 'Completed', - SandboxName: 'TestSandbox', - SandboxInfoId: '0GQ4p000000U6sKXXX', - LicenseType: 'DEVELOPER', - CreatedDate: '2021-12-07T16:20:21.000+0000', - CopyProgress: 100, - SandboxOrganization: '00D2f0000008XXX', - SourceId: '123', - Description: 'sandbox description', - ApexClassId: '123', - EndDate: '2021-12-07T16:38:47.000+0000', -}; - -describe('sandboxReporter', () => { - describe('sandboxProgress', () => { - it('will calculate the correct human readable message (1h 33min 00seconds seconds left)', () => { - const data = { - sandboxProcessObj, - interval: 30, - remainingWait: 5580, - waitingOnAuth: false, - }; - const res = SandboxReporter.sandboxProgress(data); - expect(res).to.equal( - 'Sandbox request TestSandbox(0GR4p000000U8EMXXX) is Completed (100% completed). Sleeping 30 seconds. Will wait 1 hour 33 minutes more before timing out.' - ); - }); - - it('will calculate the correct human readable message (5 min 30seconds seconds left)', () => { - const data: StatusEvent = { - sandboxProcessObj, - interval: 30, - remainingWait: 330, - waitingOnAuth: false, - }; - const res = SandboxReporter.sandboxProgress(data); - expect(res).to.equal( - 'Sandbox request TestSandbox(0GR4p000000U8EMXXX) is Completed (100% completed). Sleeping 30 seconds. Will wait 5 minutes 30 seconds more before timing out.' - ); - }); - }); - - describe('logSandboxProcessResult', () => { - it('sandboxCreate EVENT_RESULT', () => { - const sandboxRes: SandboxUserAuthResponse = { - authCode: 'sandboxTestAuthCode', - authUserName: 'newSandboxUsername', - instanceUrl: 'https://login.salesforce.com', - loginUrl: 'https://productionOrg--createdSandbox.salesforce.com/', - }; - - const data = { sandboxProcessObj, sandboxRes }; - expect(SandboxReporter.logSandboxProcessResult(data)).to.deep.equal({ - sandboxReadyForUse: 'Sandbox TestSandbox(0GR4p000000U8EMXXX) is ready for use.', - data: [ - { - key: 'Id', - value: '0GR4p000000U8EMXXX', - }, - { - key: 'SandboxName', - value: 'TestSandbox', - }, - { - key: 'Status', - value: 'Completed', - }, - { - key: 'CopyProgress', - value: 100, - }, - { - key: 'Description', - value: 'sandbox description', - }, - { - key: 'LicenseType', - value: 'DEVELOPER', - }, - { - key: 'SandboxInfoId', - value: '0GQ4p000000U6sKXXX', - }, - { - key: 'SourceId', - value: '123', - }, - { - key: 'SandboxOrg', - value: '00D2f0000008XXX', - }, - { - key: 'Created Date', - value: '2021-12-07T16:20:21.000+0000', - }, - { - key: 'ApexClassId', - value: '123', - }, - { - key: 'Authorized Sandbox Username', - value: 'newSandboxUsername', - }, - ], - }); - }); - }); -}); diff --git a/test/shared/stagedProgress.test.ts b/test/shared/stagedProgress.test.ts deleted file mode 100644 index 4c69f96e..00000000 --- a/test/shared/stagedProgress.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import ansis from 'ansis'; -import { StagedProgress, StateConstants } from '../../src/shared/stagedProgress.js'; -import { SandboxStatusData } from '../../src/shared/sandboxProgress.js'; - -class TestStagedProgress extends StagedProgress { - // eslint-disable-next-line class-methods-use-this - public formatProgressStatus(): string { - return ''; - } -} - -describe('stagedProgress', () => { - let stagedProgress: TestStagedProgress; - describe('updateStages', () => { - beforeEach(() => { - stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); - }); - it('should update existing stage', () => { - stagedProgress.updateStages('Pending', 'failed'); - const pendingStage = stagedProgress.getStages()['Pending']; - expect(pendingStage).to.be.ok; - expect(pendingStage.state).to.equal('failed'); - }); - it('should insert new stage at beginning of stages', () => { - stagedProgress.updateStages('Creating', 'inProgress'); - const creatingStage = stagedProgress.getStages()['Creating']; - const pendingStage = stagedProgress.getStages()['Pending']; - expect(creatingStage).to.be.ok; - expect(creatingStage.state).to.equal('inProgress'); - expect(creatingStage.index).to.be.lessThan(pendingStage.index); - }); - it('should insert new stage at end of stages', () => { - const stages = stagedProgress.getStages(); - Object.keys(stages).forEach((stage) => { - stages[stage].visited = true; - stages[stage].state = 'completed'; - }); - stagedProgress.updateStages('Past the End', 'inProgress'); - const pastTheEnd = stagedProgress.getStages()['Past the End']; - const authenticatingStage = stagedProgress.getStages()['Authenticating']; - expect(pastTheEnd).to.be.ok; - expect(pastTheEnd.state).to.equal('inProgress'); - expect(pastTheEnd.index).to.be.greaterThan(authenticatingStage.index); - }); - it('should insert new stage after Processing', () => { - const stages = stagedProgress.getStages(); - Object.keys(stages).forEach((stage) => { - if (['Pending', 'Processing'].includes(stage)) { - stages[stage].visited = true; - stages[stage].state = 'completed'; - } - }); - stagedProgress.updateStages('After Processing', 'inProgress'); - const afterProcessStage = stagedProgress.getStages()['After Processing']; - const processingStage = stagedProgress.getStages()['Processing']; - expect(afterProcessStage).to.be.ok; - expect(afterProcessStage.state).to.equal('inProgress'); - expect(afterProcessStage.index).to.be.equal(processingStage.index + 1); - }); - }); - describe('getFormattedStages', () => { - beforeEach(() => { - stagedProgress = new TestStagedProgress(['Pending', 'Processing', 'Activating', 'Completed', 'Authenticating']); - }); - it('should get formatted stages - all unknown', () => { - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(ansis.dim('')); - }); - it('should get formatted stages - pending in progress', () => { - stagedProgress.updateStages('Pending', 'inProgress'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include( - StateConstants.inProgress.color(`${StateConstants.inProgress.char} - Pending`) - ); - }); - it('should get formatted stages - pending successful', () => { - stagedProgress.updateStages('Pending', 'completed'); - stagedProgress.updateStages('Processing', 'inProgress'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(StateConstants.completed.color(`${StateConstants.completed.char} - Pending`)); - }); - it('should get formatted stages - processing failed', () => { - stagedProgress.updateStages('Pending', 'completed'); - stagedProgress.updateStages('Processing', 'failed'); - const formattedStages = stagedProgress.formatStages(); - expect(formattedStages).to.be.ok; - expect(formattedStages).to.include(StateConstants.unknown.char); - expect(formattedStages).to.include(StateConstants.failed.color(`${StateConstants.failed.char} - Processing`)); - }); - }); -}); diff --git a/test/shared/timeUtils.test.ts b/test/shared/timeUtils.test.ts deleted file mode 100644 index a007a87d..00000000 --- a/test/shared/timeUtils.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2020, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { expect } from 'chai'; -import { Duration } from '@salesforce/cli-plugins-testkit'; -import { getClockForSeconds, getSecondsToHuman } from '../../src/shared/timeUtils.js'; - -describe('timeUtils', () => { - describe('getSecondsToHuman', () => { - it('should build time string with 10 seconds', () => { - expect(getSecondsToHuman(10)).to.includes('10 seconds'); - }); - it('should build time string 1 minute', () => { - expect(getSecondsToHuman(60)).to.includes('1 minute'); - }); - it('should build time string 1 hour', () => { - expect(getSecondsToHuman(Duration.hours(1).seconds)).to.includes('1 hour'); - }); - it('should build time string 1 day', () => { - expect(getSecondsToHuman(Duration.days(1).seconds)).to.includes('1 day'); - }); - it('should build time string 1 day 12 hours', () => { - expect(getSecondsToHuman(Duration.days(1).seconds + Duration.hours(12).seconds)).to.includes('1 day 12 hour'); - }); - }); - describe('getClockForSeconds', () => { - it('should build time string with 10 seconds', () => { - expect(getClockForSeconds(10)).to.be.equal('00:00:10'); - }); - it('should build time string 1 minute', () => { - expect(getClockForSeconds(60)).to.be.equal('00:01:00'); - }); - it('should build time string 1 hour', () => { - expect(getClockForSeconds(Duration.hours(1).seconds)).to.be.equal('01:00:00'); - }); - it('should build time string 1 day', () => { - expect(getClockForSeconds(Duration.days(1).seconds)).to.be.equal('1:00:00:00'); - }); - it('should build time string 1 day 12 hours', () => { - expect(getClockForSeconds(Duration.days(1).seconds + Duration.hours(12).seconds)).to.be.equal('1:12:00:00'); - }); - }); -}); From 3742134a55ec411291332b03662d1ea3c3084836 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 21 Feb 2025 14:39:38 -0300 Subject: [PATCH 11/13] fix: set sandbox username --- src/shared/sandboxCommandBase.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/sandboxCommandBase.ts b/src/shared/sandboxCommandBase.ts index 5fffc897..b68adc50 100644 --- a/src/shared/sandboxCommandBase.ts +++ b/src/shared/sandboxCommandBase.ts @@ -144,6 +144,7 @@ export abstract class SandboxCommandBase extends SfCommand { }); lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => { + this.sandboxUsername = results.authUserName; this.stages.auth(); this.sandboxAuth = results; return Promise.resolve(); @@ -151,6 +152,7 @@ export abstract class SandboxCommandBase extends SfCommand { lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => { this.latestSandboxProgressObj = results.sandboxProcessObj; + this.sandboxUsername = results.sandboxRes.authUserName; this.updateSandboxRequestData(); this.stages.update(results.sandboxProcessObj); From c2923b0131e03c3dd46e72170c7d1657708ac9e5 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Thu, 27 Feb 2025 12:06:17 -0300 Subject: [PATCH 12/13] test: update sandbox NUT --- test/nut/sandbox.sandboxNut.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/nut/sandbox.sandboxNut.ts b/test/nut/sandbox.sandboxNut.ts index 54db8f85..45eebbe1 100644 --- a/test/nut/sandbox.sandboxNut.ts +++ b/test/nut/sandbox.sandboxNut.ts @@ -29,13 +29,13 @@ describe('Sandbox Orgs', () => { it('will create a sandbox, verify it can be opened, and then attempt to delete it', () => { let result: SandboxProcessObject | undefined; try { - Lifecycle.getInstance().on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => + Lifecycle.getInstance().on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: StatusEvent) => // eslint-disable-next-line no-console Promise.resolve(console.log('sandbox copy progress', results.sandboxProcessObj.CopyProgress)) ); let rawResult = execCmd( `env:create:sandbox -a mySandbox -s -l Developer -o ${hubOrgUsername} --no-prompt --json --async`, - { timeout: 3_600_000 } + { timeout: 3_600_000, ensureExitCode: 68 } ); result = rawResult.jsonOutput?.result as SandboxProcessObject; // autogenerated sandbox names start with 'sbx' From 0cce46bf170ae32275ca5930132a4cff653fab36 Mon Sep 17 00:00:00 2001 From: Cristian Dominguez Date: Fri, 28 Feb 2025 13:20:12 -0300 Subject: [PATCH 13/13] test: log failure --- test/nut/sandbox.sandboxNut.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/nut/sandbox.sandboxNut.ts b/test/nut/sandbox.sandboxNut.ts index 45eebbe1..a11af0f6 100644 --- a/test/nut/sandbox.sandboxNut.ts +++ b/test/nut/sandbox.sandboxNut.ts @@ -48,7 +48,9 @@ describe('Sandbox Orgs', () => { result = rawResult.jsonOutput?.result as SandboxProcessObject; expect(result).to.be.ok; } catch (e) { - expect(false).to.be.true(JSON.stringify(e)); + // eslint-disable-next-line no-console + console.error(e); + expect(false, 'catch verification').to.be.true(JSON.stringify(e)); } assert(result);