@salesforce/plugin-org
Version:
Commands to interact with Salesforce orgs
222 lines • 10.2 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 { EOL } from 'node:os';
import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core';
import { AuthInfo, Connection, envVars, Org, SfError, Messages, Logger, } from '@salesforce/core';
import ansis from 'ansis';
import { OrgListUtil, identifyActiveOrgByStatus } from '../../shared/orgListUtil.js';
import { getStyledObject } from '../../shared/orgHighlighter.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'list');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted');
export const defaultOrgEmoji = '🍁';
export const defaultHubEmoji = '🌳';
export class OrgListCommand extends SfCommand {
static summary = messages.getMessage('summary');
static examples = messages.getMessages('examples');
static aliases = ['force:org:list'];
static deprecateAliases = true;
static flags = {
verbose: Flags.boolean({
summary: messages.getMessage('flags.verbose.summary'),
}),
all: Flags.boolean({
summary: messages.getMessage('flags.all.summary'),
}),
clean: Flags.boolean({
summary: messages.getMessage('flags.clean.summary'),
}),
'no-prompt': Flags.boolean({
char: 'p',
summary: messages.getMessage('flags.no-prompt.summary'),
relationships: [
{
type: 'some',
flags: [{ name: 'clean', when: async (flags) => Promise.resolve(flags['clean'] === true) }],
},
],
aliases: ['noprompt'],
deprecateAliases: true,
}),
'skip-connection-status': Flags.boolean({
summary: messages.getMessage('flags.skip-connection-status.summary'),
aliases: ['skipconnectionstatus'],
deprecateAliases: true,
}),
loglevel,
};
flags;
async run() {
const [{ flags }, fileNames] = await Promise.all([this.parse(OrgListCommand), getAuthFileNames()]);
this.flags = flags;
const metaConfigs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, flags['skip-connection-status']);
const groupedSortedOrgs = {
devHubs: metaConfigs.devHubs.map(decorateWithDefaultStatus).sort(comparator),
other: metaConfigs.other.map(decorateWithDefaultStatus).sort(comparator),
sandboxes: metaConfigs.sandboxes.map(decorateWithDefaultStatus).sort(comparator),
nonScratchOrgs: metaConfigs.nonScratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
scratchOrgs: metaConfigs.scratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
expiredScratchOrgs: metaConfigs.scratchOrgs.filter((org) => !identifyActiveOrgByStatus(org)),
};
if (flags.clean && groupedSortedOrgs.expiredScratchOrgs.length > 0) {
await this.cleanScratchOrgs(groupedSortedOrgs.expiredScratchOrgs, !flags['no-prompt']);
}
if (groupedSortedOrgs.expiredScratchOrgs.length >= 1 && !flags.clean) {
this.warn(messages.getMessage('deleteOrgs', [groupedSortedOrgs.expiredScratchOrgs.length, EOL]));
}
const result = {
other: groupedSortedOrgs.other,
sandboxes: groupedSortedOrgs.sandboxes,
nonScratchOrgs: groupedSortedOrgs.nonScratchOrgs,
devHubs: groupedSortedOrgs.devHubs,
scratchOrgs: flags.all
? groupedSortedOrgs.scratchOrgs
: groupedSortedOrgs.scratchOrgs.filter(identifyActiveOrgByStatus),
};
this.printOrgTable({
devHubs: result.devHubs,
other: result.other,
sandboxes: result.sandboxes,
scratchOrgs: result.scratchOrgs,
skipconnectionstatus: flags['skip-connection-status'],
});
this.info(`
Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${flags.all ? '' : ' Use --all to see expired and deleted scratch orgs'}`);
// TODO: Remove after env var workaround is removed
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org list']));
}
else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org list']));
}
}
return result;
}
async cleanScratchOrgs(scratchOrgs, prompt) {
if (prompt && (await this.confirm({ message: messages.getMessage('prompt', [scratchOrgs.length]) })) === false) {
return;
}
await Promise.all(scratchOrgs.map(async (fields) => {
try {
const authInfo = await AuthInfo.create({ username: fields.username });
const connection = await Connection.create({
authInfo,
configAggregator: {
// Force an api version to prevent connection check with the server for expired orgs.
getInfo: () => ({ value: '47.0' }),
},
});
const org = await Org.create({ aliasOrUsername: fields.username, connection });
await org.remove();
}
catch (e) {
this.warn(messages.getMessage('cleanWarning', [fields.username, this.config.bin, fields.username]));
if (e instanceof Error) {
const logger = await Logger.child('org:list');
logger.debug(`Error cleaning org ${fields.username}: ${e.message}`);
}
}
}));
}
printOrgTable({ devHubs, scratchOrgs, other, sandboxes, skipconnectionstatus, }) {
if (!devHubs.length && !other.length && !sandboxes.length) {
this.info(messages.getMessage('noResultsFound'));
return;
}
const allOrgs = [
...devHubs
.map(addType('DevHub'))
.map(colorEveryFieldButConnectedStatus(ansis.cyanBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),
...other
.map(colorEveryFieldButConnectedStatus(ansis.magentaBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),
...sandboxes
.map(addType('Sandbox'))
.map(colorEveryFieldButConnectedStatus(ansis.yellowBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),
...scratchOrgs
.map((row) => ({ ...row, type: 'Scratch' }))
.map(convertScratchOrgStatus)
.map((row) => getStyledObject(row))
.map(statusToEmoji),
];
this.table({
data: allOrgs.map((row) => ({
' ': row.defaultMarker,
...('type' in row ? { Type: row.type } : {}),
Alias: row.alias,
Username: row.username,
'Org ID': row.orgId,
...(skipconnectionstatus ? {} : { Status: row.connectedStatus }),
...(this.flags.verbose
? {
'Instance URL': row.instanceUrl,
Namespace: row.namespacePrefix,
...('devHubOrgId' in row ? { 'Dev Hub ID': row.devHubOrgId } : {}),
...('createdDate' in row ? { Created: row.createdDate } : {}),
}
: {}),
...('expirationDate' in row ? { Expires: row.expirationDate } : {}),
})),
...(this.flags.verbose ? { overflow: 'wrap' } : {}),
});
}
}
const decorateWithDefaultStatus = (val) => ({
...val,
...(val.isDefaultDevHubUsername ? { defaultMarker: '(D)' } : {}),
...(val.isDefaultUsername ? { defaultMarker: '(U)' } : {}),
...(val.isDefaultDevHubUsername && val.isDefaultUsername ? { defaultMarker: '(D),(U)' } : {}),
});
const statusToEmoji = (val) => ({
...val,
defaultMarker: val.defaultMarker?.replace('(D)', defaultHubEmoji)?.replace('(U)', defaultOrgEmoji),
});
const EMPTIES_LAST = 'zzzzzzzzzz';
// sort by alias then username
const comparator = (a, b) => {
const aliasCompareResult = (a.alias ?? EMPTIES_LAST).localeCompare(b.alias ?? EMPTIES_LAST);
return aliasCompareResult !== 0 ? aliasCompareResult : (a.username ?? EMPTIES_LAST).localeCompare(b.username);
};
const getAuthFileNames = async () => {
try {
return ((await AuthInfo.listAllAuthorizations()) ?? []).map((auth) => auth.username);
}
catch (err) {
const error = err;
if (error.name === 'NoAuthInfoFound') {
throw new SfError(messages.getMessage('noOrgsFound'), 'noOrgsFound', [messages.getMessage('noOrgsFoundAction')]);
}
else {
throw error;
}
}
};
const addType = (type) => (val) => ({ ...val, type });
const colorEveryFieldButConnectedStatus = (colorFn) => (row) => Object.fromEntries(Object.entries(row).map(([key, val]) => [
key,
typeof val === 'string' && key !== 'connectedStatus' ? colorFn(val) : val,
])
// TS is not smart enough to know this didn't change any types
);
const convertScratchOrgStatus = (row) => ({ ...row, connectedStatus: row.status });
//# sourceMappingURL=list.js.map