88import { Flags , loglevel , SfCommand } from '@salesforce/sf-plugins-core' ;
99import { AuthInfo , ConfigAggregator , ConfigInfo , Connection , Org , SfError , Messages , Logger } from '@salesforce/core' ;
1010import { Interfaces } from '@oclif/core' ;
11+ import * as chalk from 'chalk' ;
1112import { OrgListUtil , identifyActiveOrgByStatus } from '../../shared/orgListUtil' ;
1213import { getStyledObject } from '../../shared/orgHighlighter' ;
1314import { ExtendedAuthFields , FullyPopulatedScratchOrgFields } from '../../shared/orgTypes' ;
1415
1516Messages . importMessagesDirectory ( __dirname ) ;
1617const messages = Messages . loadMessages ( '@salesforce/plugin-org' , 'list' ) ;
1718
19+ export const defaultOrgEmoji = '🍁' ;
20+ export const defaultHubEmoji = '🌳' ;
21+
1822export type OrgListResult = {
23+ /**
24+ * @deprecated
25+ * preserved for backward json compatibility. Duplicates devHubs, sandboxes, regularOrgs, which should be preferred*/
1926 nonScratchOrgs : ExtendedAuthFields [ ] ;
2027 scratchOrgs : FullyPopulatedScratchOrgFields [ ] ;
28+ sandboxes : ExtendedAuthFields [ ] ;
29+ other : ExtendedAuthFields [ ] ;
30+ devHubs : ExtendedAuthFields [ ] ;
2131} ;
32+
2233export class OrgListCommand extends SfCommand < OrgListResult > {
2334 public static readonly summary = messages . getMessage ( 'summary' ) ;
2435 public static readonly examples = messages . getMessages ( 'examples' ) ;
@@ -56,6 +67,9 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
5667 this . flags = flags ;
5768 const metaConfigs = await OrgListUtil . readLocallyValidatedMetaConfigsGroupedByOrgType ( fileNames , flags ) ;
5869 const groupedSortedOrgs = {
70+ devHubs : metaConfigs . devHubs . map ( decorateWithDefaultStatus ) . sort ( comparator ) ,
71+ other : metaConfigs . other . map ( decorateWithDefaultStatus ) . sort ( comparator ) ,
72+ sandboxes : metaConfigs . sandboxes . map ( decorateWithDefaultStatus ) . sort ( comparator ) ,
5973 nonScratchOrgs : metaConfigs . nonScratchOrgs . map ( decorateWithDefaultStatus ) . sort ( comparator ) ,
6074 scratchOrgs : metaConfigs . scratchOrgs . map ( decorateWithDefaultStatus ) . sort ( comparator ) ,
6175 expiredScratchOrgs : metaConfigs . scratchOrgs . filter ( ( org ) => ! identifyActiveOrgByStatus ( org ) ) ,
@@ -70,15 +84,29 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
7084 }
7185
7286 const result = {
87+ other : groupedSortedOrgs . other ,
88+ sandboxes : groupedSortedOrgs . sandboxes ,
7389 nonScratchOrgs : groupedSortedOrgs . nonScratchOrgs ,
90+ devHubs : groupedSortedOrgs . devHubs ,
7491 scratchOrgs : flags . all
7592 ? groupedSortedOrgs . scratchOrgs
7693 : groupedSortedOrgs . scratchOrgs . filter ( identifyActiveOrgByStatus ) ,
7794 } ;
7895
79- this . printOrgTable ( result . nonScratchOrgs , flags [ 'skip-connection-status' ] ) ;
96+ this . printOrgTable ( {
97+ devHubs : result . devHubs ,
98+ other : result . other ,
99+ sandboxes : result . sandboxes ,
100+ scratchOrgs : result . scratchOrgs ,
101+ skipconnectionstatus : flags [ 'skip-connection-status' ] ,
102+ } ) ;
80103
81- this . printScratchOrgTable ( result . scratchOrgs ) ;
104+ this . info (
105+ `
106+ Legend: ${ defaultHubEmoji } =Default DevHub, ${ defaultOrgEmoji } =Default Org ${
107+ flags . all ? '' : ' Use --all to see expired and deleted scratch orgs'
108+ } `
109+ ) ;
82110
83111 return result ;
84112 }
@@ -112,108 +140,97 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
112140 ) ;
113141 }
114142
115- protected printOrgTable ( nonScratchOrgs : ExtendedAuthFields [ ] , skipconnectionstatus : boolean ) : void {
116- if ( ! nonScratchOrgs . length ) {
117- this . log ( messages . getMessage ( 'noResultsFound' ) ) ;
118- } else {
119- const rows = nonScratchOrgs
120- . map ( ( row ) => getStyledObject ( row ) )
121- . map ( ( org ) =>
122- Object . fromEntries (
123- Object . entries ( org ) . filter ( ( [ key ] ) =>
124- [ 'defaultMarker' , 'alias' , 'username' , 'orgId' , 'connectedStatus' ] . includes ( key )
125- )
126- )
127- ) ;
128-
129- this . table (
130- rows ,
131- {
132- defaultMarker : {
133- header : '' ,
134- get : ( data ) : string => data . defaultMarker ?? '' ,
135- } ,
136- alias : {
137- header : 'ALIAS' ,
138- get : ( data ) : string => data . alias ?? '' ,
139- } ,
140- username : { header : 'USERNAME' } ,
141- orgId : { header : 'ORG ID' } ,
142- ...( ! skipconnectionstatus ? { connectedStatus : { header : 'CONNECTED STATUS' } } : { } ) ,
143- } ,
144- {
145- title : 'Non-scratch orgs' ,
146- }
147- ) ;
143+ protected printOrgTable ( {
144+ devHubs,
145+ scratchOrgs,
146+ other,
147+ sandboxes,
148+ skipconnectionstatus,
149+ } : {
150+ devHubs : ExtendedAuthFields [ ] ;
151+ other : ExtendedAuthFields [ ] ;
152+ sandboxes : ExtendedAuthFields [ ] ;
153+ scratchOrgs : FullyPopulatedScratchOrgFields [ ] ;
154+ skipconnectionstatus : boolean ;
155+ } ) : void {
156+ if ( ! devHubs . length && ! other . length && ! sandboxes . length ) {
157+ this . info ( messages . getMessage ( 'noResultsFound' ) ) ;
158+ return ;
148159 }
149- }
160+ const allOrgs : Array < FullyPopulatedScratchOrgFields | ExtendedAuthFieldsWithType > = [
161+ ...devHubs
162+ . map ( addType ( 'DevHub' ) )
163+ . map ( colorEveryFieldButConnectedStatus ( chalk . cyanBright ) )
164+ . map ( ( row ) => getStyledObject ( row ) )
165+ . map ( statusToEmoji ) ,
150166
151- private printScratchOrgTable ( scratchOrgs : FullyPopulatedScratchOrgFields [ ] ) : void {
152- if ( scratchOrgs . length === 0 ) {
153- this . log ( messages . getMessage ( 'noActiveScratchOrgs' ) ) ;
154- } else {
155- // One or more rows are available.
156- // we only need a few of the props for our table. Oclif table doesn't like extra props non-string props.
157- const rows = scratchOrgs
158- . map ( getStyledObject )
159- . map ( ( org ) =>
160- Object . fromEntries (
161- Object . entries ( org ) . filter ( ( [ key ] ) =>
162- [
163- 'defaultMarker' ,
164- 'alias' ,
165- 'username' ,
166- 'orgId' ,
167- 'status' ,
168- 'expirationDate' ,
169- 'devHubOrgId' ,
170- 'createdDate' ,
171- 'instanceUrl' ,
172- ] . includes ( key )
173- )
174- )
175- ) ;
176- this . table (
177- rows ,
178- {
179- defaultMarker : {
180- header : '' ,
181- get : ( data ) : string => data . defaultMarker ?? '' ,
182- } ,
183- alias : {
184- header : 'ALIAS' ,
185- get : ( data ) : string => data . alias ?? '' ,
186- } ,
187- username : { header : 'USERNAME' } ,
188- orgId : { header : 'ORG ID' } ,
189- ...( this . flags . all || this . flags . verbose ? { status : { header : 'STATUS' } } : { } ) ,
190- ...( this . flags . verbose
191- ? {
192- devHubOrgId : { header : 'DEV HUB' } ,
193- createdDate : { header : 'CREATED DATE' } ,
194- instanceUrl : { header : 'INSTANCE URL' } ,
195- }
196- : { } ) ,
197- expirationDate : { header : 'EXPIRATION DATE' } ,
167+ ...other
168+ . map ( colorEveryFieldButConnectedStatus ( chalk . magentaBright ) )
169+ . map ( ( row ) => getStyledObject ( row ) )
170+ . map ( statusToEmoji ) ,
171+
172+ ...sandboxes
173+ . map ( addType ( 'Sandbox' ) )
174+ . map ( colorEveryFieldButConnectedStatus ( chalk . yellowBright ) )
175+ . map ( ( row ) => getStyledObject ( row ) )
176+ . map ( statusToEmoji ) ,
177+
178+ ...scratchOrgs
179+ . map ( ( row ) => ( { ...row , type : 'Scratch' } ) )
180+ . map ( convertScratchOrgStatus )
181+ . map ( ( row ) => getStyledObject ( row ) )
182+ . map ( statusToEmoji ) ,
183+ ] ;
184+
185+ this . table (
186+ allOrgs . map ( ( org ) => Object . fromEntries ( Object . entries ( org ) . filter ( fieldFilter ) ) ) ,
187+ {
188+ defaultMarker : {
189+ header : '' ,
198190 } ,
199- {
200- title : 'Scratch orgs' ,
201- }
202- ) ;
203- }
191+ type : {
192+ header : 'Type' ,
193+ } ,
194+ alias : {
195+ header : 'Alias' ,
196+ } ,
197+ username : { header : 'Username' } ,
198+ orgId : { header : 'Org ID' } ,
199+ ...( ! skipconnectionstatus ? { connectedStatus : { header : 'Status' } } : { } ) ,
200+ ...( this . flags . verbose
201+ ? {
202+ instanceUrl : { header : 'Instance URL' } ,
203+ devHubOrgId : { header : 'Dev Hub ID' } ,
204+ createdDate : {
205+ header : 'Created' ,
206+ get : ( data ) : string => ( data . createdDate as string ) ?. split ( 'T' ) ?. [ 0 ] ?? '' ,
207+ } ,
208+ }
209+ : { } ) ,
210+ expirationDate : { header : 'Expires' } ,
211+ }
212+ ) ;
204213 }
205214}
206215
207216const decorateWithDefaultStatus = < T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields > ( val : T ) : T => ( {
208217 ...val ,
209218 ...( val . isDefaultDevHubUsername ? { defaultMarker : '(D)' } : { } ) ,
210219 ...( val . isDefaultUsername ? { defaultMarker : '(U)' } : { } ) ,
220+ ...( val . isDefaultDevHubUsername && val . isDefaultUsername ? { defaultMarker : '(D),(U)' } : { } ) ,
211221} ) ;
212222
223+ const statusToEmoji = < T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields > ( val : T ) : T => ( {
224+ ...val ,
225+ defaultMarker : val . defaultMarker ?. replace ( '(D)' , defaultHubEmoji ) ?. replace ( '(U)' , defaultOrgEmoji ) ,
226+ } ) ;
227+
228+ const EMPTIES_LAST = 'zzzzzzzzzz' ;
229+
213230// sort by alias then username
214231const comparator = < T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields > ( a : T , b : T ) : number => {
215- const aliasCompareResult = ( a . alias ?? '' ) . localeCompare ( b . alias ?? '' ) ;
216- return aliasCompareResult !== 0 ? aliasCompareResult : ( a . username ?? '' ) . localeCompare ( b . username ) ;
232+ const aliasCompareResult = ( a . alias ?? EMPTIES_LAST ) . localeCompare ( b . alias ?? EMPTIES_LAST ) ;
233+ return aliasCompareResult !== 0 ? aliasCompareResult : ( a . username ?? EMPTIES_LAST ) . localeCompare ( b . username ) ;
217234} ;
218235
219236const getAuthFileNames = async ( ) : Promise < string [ ] > => {
@@ -228,3 +245,41 @@ const getAuthFileNames = async (): Promise<string[]> => {
228245 }
229246 }
230247} ;
248+
249+ type ExtendedAuthFieldsWithType = ExtendedAuthFields & { type ?: string } ;
250+
251+ const addType =
252+ ( type : string ) =>
253+ ( val : ExtendedAuthFields ) : ExtendedAuthFieldsWithType => ( { ...val , type } ) ;
254+
255+ const colorEveryFieldButConnectedStatus =
256+ ( colorFn : chalk . Chalk ) =>
257+ ( row : ExtendedAuthFieldsWithType ) : ExtendedAuthFieldsWithType =>
258+ Object . fromEntries (
259+ Object . entries ( row ) . map ( ( [ key , val ] ) => [
260+ key ,
261+ typeof val === 'string' && key !== 'connectedStatus' ? colorFn ( val ) : val ,
262+ ] )
263+ // TS is not smart enough to know this didn't change any types
264+ ) as ExtendedAuthFieldsWithType ;
265+
266+ const fieldFilter = ( [ key ] : [ string , string ] ) : boolean =>
267+ [
268+ 'defaultMarker' ,
269+ 'alias' ,
270+ 'username' ,
271+ 'orgId' ,
272+ 'status' ,
273+ 'connectedStatus' ,
274+ 'expirationDate' ,
275+ 'devHubOrgId' ,
276+ 'createdDate' ,
277+ 'instanceUrl' ,
278+ 'type' ,
279+ 'createdDate' ,
280+ ] . includes ( key ) ;
281+
282+ const convertScratchOrgStatus = (
283+ row : FullyPopulatedScratchOrgFields
284+ ) : FullyPopulatedScratchOrgFields & { connectedStatus : string } =>
285+ ( { ...row , connectedStatus : row . status } as FullyPopulatedScratchOrgFields & { connectedStatus : string } ) ;
0 commit comments