@salesforce/plugin-org
Version:
Commands to interact with Salesforce orgs
269 lines • 11.8 kB
JavaScript
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { MultiStageOutput } from '@oclif/multi-stage-output';
import { envVars, Lifecycle, Messages, Org, scratchOrgCreate, scratchOrgLifecycleEventName, scratchOrgLifecycleStages, SfError, } from '@salesforce/core';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { Duration, omit } from '@salesforce/kit';
import terminalLink from 'terminal-link';
import { capitalCase } from 'change-case';
import { buildScratchOrgRequest } from '../../../shared/scratchOrgRequest.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'create_scratch');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted');
const definitionFileHelpGroupName = 'Definition File Override';
export default class OrgCreateScratch extends SfCommand {
static summary = messages.getMessage('summary');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static aliases = ['env:create:scratch'];
static deprecateAliases = true;
static flags = {
alias: Flags.string({
char: 'a',
summary: messages.getMessage('flags.alias.summary'),
description: messages.getMessage('flags.alias.description'),
}),
async: Flags.boolean({
summary: messages.getMessage('flags.async.summary'),
description: messages.getMessage('flags.async.description'),
}),
'set-default': Flags.boolean({
char: 'd',
summary: messages.getMessage('flags.set-default.summary'),
}),
'definition-file': Flags.file({
exists: true,
char: 'f',
summary: messages.getMessage('flags.definition-file.summary'),
description: messages.getMessage('flags.definition-file.description'),
}),
'target-dev-hub': Flags.requiredHub({
char: 'v',
summary: messages.getMessage('flags.target-dev-hub.summary'),
description: messages.getMessage('flags.target-dev-hub.description'),
required: true,
}),
'no-ancestors': Flags.boolean({
char: 'c',
summary: messages.getMessage('flags.no-ancestors.summary'),
helpGroup: 'Packaging',
}),
edition: Flags.string({
char: 'e',
summary: messages.getMessage('flags.edition.summary'),
description: messages.getMessage('flags.edition.description'),
options: [
'developer',
'enterprise',
'group',
'professional',
'partner-developer',
'partner-enterprise',
'partner-group',
'partner-professional',
],
exclusive: ['snapshot', 'source-org'],
// eslint-disable-next-line @typescript-eslint/require-await
parse: async (value) => {
// the API expects partner editions in `partner <EDITION>` format.
// so we replace the hyphen here with a space.
if (value.startsWith('partner-')) {
return value.replace('-', ' ');
}
return value;
},
helpGroup: definitionFileHelpGroupName,
}),
snapshot: Flags.string({
char: 's',
summary: messages.getMessage('flags.snapshot.summary'),
description: messages.getMessage('flags.snapshot.description'),
exclusive: ['edition', 'source-org'],
helpGroup: definitionFileHelpGroupName,
}),
'no-namespace': Flags.boolean({
char: 'm',
summary: messages.getMessage('flags.no-namespace.summary'),
helpGroup: 'Packaging',
}),
'duration-days': Flags.duration({
unit: 'days',
default: Duration.days(7),
min: 1,
max: 30,
char: 'y',
helpValue: '<days>',
summary: messages.getMessage('flags.duration-days.summary'),
}),
wait: Flags.duration({
unit: 'minutes',
default: Duration.minutes(5),
min: 2,
char: 'w',
helpValue: '<minutes>',
summary: messages.getMessage('flags.wait.summary'),
description: messages.getMessage('flags.wait.description'),
}),
'api-version': Flags.orgApiVersion(),
'client-id': Flags.string({
char: 'i',
summary: messages.getMessage('flags.client-id.summary'),
}),
'track-source': Flags.boolean({
default: true,
char: 't',
summary: messages.getMessage('flags.track-source.summary'),
description: messages.getMessage('flags.track-source.description'),
allowNo: true,
}),
username: Flags.string({
summary: messages.getMessage('flags.username.summary'),
description: messages.getMessage('flags.username.description'),
helpGroup: definitionFileHelpGroupName,
}),
description: Flags.string({
summary: messages.getMessage('flags.description.summary'),
helpGroup: definitionFileHelpGroupName,
}),
name: Flags.string({
summary: messages.getMessage('flags.name.summary'),
helpGroup: definitionFileHelpGroupName,
}),
release: Flags.string({
summary: messages.getMessage('flags.release.summary'),
description: messages.getMessage('flags.release.description'),
options: ['preview', 'previous'],
helpGroup: definitionFileHelpGroupName,
}),
'admin-email': Flags.string({
summary: messages.getMessage('flags.admin-email.summary'),
helpGroup: definitionFileHelpGroupName,
}),
'source-org': Flags.salesforceId({
summary: messages.getMessage('flags.source-org.summary'),
description: messages.getMessage('flags.source-org.description'),
exclusive: ['edition', 'snapshot'],
startsWith: '00D',
length: 15,
helpGroup: definitionFileHelpGroupName,
// salesforceId flag has `i` and that would be a conflict with client-id
char: undefined,
}),
};
async run() {
const lifecycle = Lifecycle.getInstance();
const { flags } = await this.parse(OrgCreateScratch);
const baseUrl = flags['target-dev-hub'].getField(Org.Fields.INSTANCE_URL)?.toString();
if (!baseUrl) {
throw new SfError('No instance URL found for the dev hub');
}
const createCommandOptions = await buildScratchOrgRequest(flags, flags['client-id'] ? await this.secretPrompt({ message: messages.getMessage('prompt.secret') }) : undefined);
const mso = new MultiStageOutput({
stages: (flags.async ? ['prepare request', 'send request', 'done'] : scratchOrgLifecycleStages).map((stage) => capitalCase(stage)),
title: flags.async ? 'Creating Scratch Org (async)' : 'Creating Scratch Org',
data: { alias: flags.alias },
jsonEnabled: this.jsonEnabled(),
postStagesBlock: [
{
label: 'Request Id',
type: 'dynamic-key-value',
get: (data) => data?.scratchOrgInfo?.Id && terminalLink(data.scratchOrgInfo.Id, `${baseUrl}/${data.scratchOrgInfo.Id}`),
bold: true,
},
{
label: 'OrgId',
type: 'dynamic-key-value',
get: (data) => data?.scratchOrgInfo?.ScratchOrg,
bold: true,
color: 'cyan',
},
{
label: 'Username',
type: 'dynamic-key-value',
get: (data) => data?.scratchOrgInfo?.SignupUsername,
bold: true,
color: 'cyan',
},
{
label: 'Alias',
type: 'static-key-value',
get: (data) => data?.alias,
},
],
});
lifecycle.on(scratchOrgLifecycleEventName, async (data) => {
mso.skipTo(capitalCase(data.stage), data);
if (data.stage === 'done') {
mso.stop();
}
return Promise.resolve();
});
try {
const { username, scratchOrgInfo, authFields, warnings } = await scratchOrgCreate(createCommandOptions);
if (!scratchOrgInfo) {
throw new SfError('The scratch org did not return with any information');
}
if (flags.async) {
mso.skipTo('Done', { scratchOrgInfo });
mso.stop();
this.info(messages.getMessage('action.resume', [this.config.bin, scratchOrgInfo.Id]));
}
else {
this.logSuccess(messages.getMessage('success'));
}
// TODO: Remove env var workaround
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken');
const redactedAuthFields = authFields
? {
...authFields,
accessToken: showSecretsEnvVarIsSet ? authFields.accessToken : accessTokenRedacted,
refreshToken: undefined,
clientSecret: undefined,
}
: undefined;
const redactedScratchOrgInfo = omit(scratchOrgInfo, ['AuthCode']);
if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org create scratch --json']));
}
else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org create scratch --json']));
}
}
return {
username,
scratchOrgInfo: redactedScratchOrgInfo,
authFields: redactedAuthFields,
warnings,
orgId: authFields?.orgId,
};
}
catch (error) {
mso.error();
if (error instanceof SfError && error.name === 'ScratchOrgInfoTimeoutError') {
const scratchOrgInfoId = error.data.scratchOrgInfoId;
const resumeMessage = messages.getMessage('action.resume', [this.config.bin, scratchOrgInfoId]);
const timeOutMessage = 'The scratch org did not complete within your wait time.';
this.error(`${timeOutMessage} ${resumeMessage}`, { code: '69', exit: 69 });
}
else {
throw error;
}
}
}
}
//# sourceMappingURL=scratch.js.map