import { flags , SfdxCommand } from '@salesforce/command' ; import { JsonArray , JsonMap } from '@salesforce/ts-types' ; import { Messages , SfdxProjectJson , SfdxError } from '@salesforce/core' ; const spawn = require ( 'child-process-promise' ) . spawn ; const packageIdPrefix = '0Ho' ; const packageVersionIdPrefix = '04t' ; const packageAliasesMap = [ ] ; const defaultWait = 10 ; Messages . importMessagesDirectory ( __dirname ) ; const messages = Messages . loadMessages ( 'texei-sfdx-plugin' , 'package-dependencies-install' ) ; export default class Install extends SfdxCommand { public static description = messages . getMessage ( 'commandDescription' ) ; public static examples = [ '$ texei:package:dependencies:install -u MyScratchOrg -v MyDevHub -k "1:MyPackage1Key 2: 3:MyPackage3Key" -b "DEV"' ] ; protected static flagsConfig = { installationkeys : flags . string ( { char : 'k' , required : false , description : 'installation key for key-protected packages (format is 1:MyPackage1Key 2: 3:MyPackage3Key... to allow some packages without installation key)' } ) , branch : flags . string ( { char : 'b' , required : false , description : 'the package version’s branch' } ) , packages : flags . string ( { char : 'p' , required : false , description : "comma-separated list of the packages to install related dependencies" } ) , securitytype : flags . string ( { char : 's' , required : false , description : "security access type for the installed package (see force:package:install for default value)" } ) , namespaces : flags . string ( { char : 'n' , required : false , description : 'filter package installation by namespace' } ) , wait : flags . number ( { char : 'w' , required : false , description : 'number of minutes to wait for installation status (also used for publishwait). Default is 10' } ) , noprompt : flags . boolean ( { char : 'r' , required : false , description : 'allow Remote Site Settings and Content Security Policy websites to send or receive data without confirmation' } ) , apexcompile : flags . string ( { char : 'a' , required : false , description : "compile all Apex in the org and package, or only Apex in the package (see force:package:install for default value)" } ) } ; protected static requiresUsername = true ; protected static requiresDevhubUsername = true ; protected static requiresProject = true ; public async run ( ) { const result = { installedPackages : { } } ; const username = this . org . getUsername ( ) ; const options = SfdxProjectJson . getDefaultOptions ( ) ; const project = await SfdxProjectJson . create ( options ) ; if ( this . flags . packages != null ) { this . ux . log ( 'Filtering by packages: ' + this . flags . packages ) ; } if ( this . flags . namespaces != null ) { this . ux . log ( 'Filtering by namespaces: ' + this . flags . namespaces ) ; } const packageAliases = project . get ( 'packageAliases' ) || { } ; if ( typeof packageAliases !== undefined ) { Object . entries ( packageAliases ) . forEach ( ( [ key , value ] ) => { packageAliasesMap [ key ] = value ; } ) ; } const packagesToInstall = [ ] ; const packageDirectories = project . get ( 'packageDirectories' ) as JsonArray || [ ] ; const packages = new Set ( ) ; if ( this . flags . packages ) { for ( let pkg of this . flags . packages . split ( ',' ) ) { packages . add ( pkg . trim ( ) ) ; } } const packagesNoFilter = ( this . flags . packages == null ) ; ; this . ux . startSpinner ( 'Resolving dependencies' , null , { stdout : true } ) ; for ( let packageDirectory of packageDirectories ) { packageDirectory = packageDirectory as JsonMap ; const packageName = ( packageDirectory . package && packageDirectory . package . toString ( ) ) ? packageDirectory . package . toString ( ) : '' ; if ( packages . has ( packageName ) || packagesNoFilter ) { const dependencies = packageDirectory . dependencies || [ ] ; if ( dependencies && dependencies [ 0 ] !== undefined ) { this . ux . log ( `Package dependencies found for package directory ${ packageDirectory . path }` ) ; for ( const dependency of ( dependencies as JsonArray ) ) { const packageInfo = { } as JsonMap ; const dependencyInfo = dependency as JsonMap ; const dependentPackage = ( ( dependencyInfo . packageId != null ) ? dependencyInfo . packageId : dependencyInfo . package ) as string ; const versionNumber = ( dependencyInfo . versionNumber ) as string ; const namespaces = this . flags . namespaces !== undefined ? this . flags . namespaces . split ( ',' ) : null ; if ( dependentPackage == null ) { throw Error ( 'Dependent package version unknow error.' ) ; } packageInfo . dependentPackage = dependentPackage ; packageInfo . versionNumber = versionNumber ; const packageVersionId = await this . getPackageVersionId ( dependentPackage , versionNumber , namespaces ) ; if ( packageVersionId != null ) { packageInfo . packageVersionId = packageVersionId ; packagesToInstall . push ( packageInfo ) ; this . ux . log ( `    ${ packageInfo . packageVersionId } : ${ packageInfo . dependentPackage }${ packageInfo . versionNumber === undefined ? '' : ' ' + packageInfo . versionNumber }` ) ; } } } else { this . ux . log ( `No dependencies found for package directory ${ packageDirectory . path }` ) ; } if ( packages && packages . has ( packageName ) ) { packages . delete ( packageName ) ; } } } if ( packages && packages . size > 0 ) { this . ux . log ( `Following packages were used in the --packages flag but were not found in the packageDirectories:` ) ; for ( let packageName of packages ) { this . ux . log ( `    ${ packageName }` ) ; } } this . ux . stopSpinner ( 'Done.' ) ; if ( packagesToInstall . length > 0 ) { this . debug ( 'DEBUG looking for already installed packages' ) ; const conn = this . org . getConnection ( ) ; const installedPackagesQuery = 'Select SubscriberPackageVersionId from InstalledSubscriberPackage' ; const installedPackageIds = ( ( await conn . tooling . query ( installedPackagesQuery ) ) . records as any ) . map ( x => x . SubscriberPackageVersionId ) ; let installationKeys = this . flags . installationkeys ; if ( installationKeys ) { installationKeys = installationKeys . trim ( ) ; installationKeys = installationKeys . split ( ' ' ) ; for ( let keyIndex = 0 ; keyIndex < installationKeys . length ; keyIndex ++ ) { const key = installationKeys [ keyIndex ] . trim ( ) ; if ( key . startsWith ( `${ keyIndex + 1 }:` ) ) { installationKeys [ keyIndex ] = key . substring ( 2 ) ; } else { throw new SfdxError ( 'Installation Key should have this format: 1:MyPackage1Key 2: 3:MyPackage3Key' ) ; } } } this . ux . log ( '\n' ) ; let i = 0 ; for ( let packageInfo of packagesToInstall ) { packageInfo = packageInfo as JsonMap ; if ( result . installedPackages . hasOwnProperty ( packageInfo . packageVersionId ) || installedPackageIds . includes ( packageInfo . packageVersionId ) ) { this . ux . log ( `PackageVersionId ${ packageInfo . packageVersionId } already installed. Skipping...` ) ; continue ; } const args = [ ] ; args . push ( 'force:package:install' ) ; args . push ( '--targetusername' ) ; args . push ( `${ username }` ) ; args . push ( '--package' ) ; args . push ( `${ packageInfo . packageVersionId }` ) ; if ( installationKeys && installationKeys [ i ] ) { args . push ( '--installationkey' ) ; args . push ( `${ installationKeys [ i ] }` ) ; } if ( this . flags . securitytype ) { args . push ( '--securitytype' ) ; args . push ( `${ this . flags . securitytype }` ) ; } if ( this . flags . apexcompile ) { args . push ( '--apexcompile' ) ; args . push ( `${ this . flags . apexcompile }` ) ; } const wait = this . flags . wait != null ? this . flags . wait : defaultWait ; args . push ( '--wait' ) ; args . push ( `${ wait }` ) ; args . push ( '--publishwait' ) ; args . push ( `${ wait }` ) ; if ( this . flags . noprompt ) { args . push ( '--noprompt' ) ; } this . ux . log ( `Installing package ${ packageInfo . packageVersionId } : ${ packageInfo . dependentPackage }${ packageInfo . versionNumber === undefined ? '' : ' ' + packageInfo . versionNumber }` ) ; await spawn ( 'sfdx' , args , { stdio : 'inherit' } ) ; this . ux . log ( '\n' ) ; result . installedPackages [ packageInfo . packageVersionId ] = packageInfo ; i ++ ; } } return { message : result } ; } private async getPackageVersionId ( name , version , namespaces ) { let packageId = null ; let packageName = name ; if ( typeof packageAliasesMap [ packageName ] !== 'undefined' ) { packageName = packageAliasesMap [ packageName ] ; } if ( packageName . startsWith ( packageVersionIdPrefix ) ) { packageId = packageName ; } else if ( packageName . startsWith ( packageIdPrefix ) ) { const vers = version . split ( '.' ) ; let query = 'Select SubscriberPackageVersionId, IsPasswordProtected, IsReleased, Package2.NamespacePrefix ' ; query += 'from Package2Version ' ; query += `where Package2Id='${ packageName }' and MajorVersion=${ vers [ 0 ] } and MinorVersion=${ vers [ 1 ] } and PatchVersion=${ vers [ 2 ] } and IsDeprecated = false ` ; if ( namespaces != null ) { query += ` and Package2.NamespacePrefix IN ('${ namespaces . join ( '\',\'' ) }')` ; } if ( vers [ 3 ] !== 'LATEST' ) { query += `and BuildNumber=${ vers [ 3 ] } ` ; } if ( this . flags . branch ) { query += `and Branch='${ this . flags . branch . trim ( ) }' ` ; } query += ' ORDER BY BuildNumber DESC Limit 1' ; const connDevHub = this . hubOrg . getConnection ( ) ; const resultPackageId = await connDevHub . tooling . query ( query ) as any ; if ( resultPackageId . size > 0 ) { packageId = resultPackageId . records [ 0 ] . SubscriberPackageVersionId ; } } return packageId ; } }