diff --git a/command-snapshot.json b/command-snapshot.json index 12fd33f3..9e6885c8 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -105,6 +105,14 @@ "flags": ["api-version", "flags-dir", "json", "loglevel", "package-install-request-id", "target-org", "verbose"], "plugin": "@salesforce/plugin-packaging" }, + { + "alias": [], + "command": "package:bundle:installed:list", + "flagAliases": ["apiversion", "target-hub-org", "targetdevhubusername", "targetusername", "u"], + "flagChars": ["b", "o", "v"], + "flags": ["api-version", "bundles", "flags-dir", "json", "loglevel", "target-dev-hub", "target-org", "verbose"], + "plugin": "@salesforce/plugin-packaging" + }, { "alias": [], "command": "package:bundle:list", diff --git a/messages/bundle_installed_list.md b/messages/bundle_installed_list.md new file mode 100644 index 00000000..5a9facc1 --- /dev/null +++ b/messages/bundle_installed_list.md @@ -0,0 +1,31 @@ +# summary + +List installed package bundles with expected vs actual component versions. + +# description + +Shows each installed bundle (by provided bundle version IDs) with its components, expected version from the Dev Hub bundle definition, and the actual installed version in the subscriber org. Highlights mismatches and missing packages. + +# examples + +- List specific installed bundles in your default org and dev hub: + + <%= config.bin %> <%= command.id %> --target-org me@example.com --target-dev-hub devhub@example.com -b 1Q8xxxxxxxAAAAAA -b 1Q8yyyyyyyBBBBBB + +# flags.bundles.summary + +One or more bundle version IDs to inspect. + +# flags.bundles.description + +Provide bundle version IDs (e.g., 1Q8...) to evaluate expected vs actual component versions. + +# flags.verbose.summary + +Show additional details. + +# noBundles + +No package bundles found in this org. + + diff --git a/src/commands/package/bundle/installed/list.ts b/src/commands/package/bundle/installed/list.ts new file mode 100644 index 00000000..9a989f17 --- /dev/null +++ b/src/commands/package/bundle/installed/list.ts @@ -0,0 +1,103 @@ +/* + * 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 { Flags, loglevel, orgApiVersionFlagWithDeprecations, requiredOrgFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core'; +import { Connection, Messages } from '@salesforce/core'; +import { BundleSObjects, PackageBundleInstall, getInstalledBundleStatuses, InstalledBundleStatus } from '@salesforce/packaging'; +import chalk from 'chalk'; +import { requiredHubFlag } from '../../../../utils/hubFlag.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_installed_list'); + +export type BundleInstalledListResults = InstalledBundleStatus[]; + +export class PackageBundleInstalledListCommand extends SfCommand { + public static readonly hidden = true; + public static state = 'beta'; + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + loglevel, + 'target-org': requiredOrgFlagWithDeprecations, + 'target-dev-hub': requiredHubFlag, + 'api-version': orgApiVersionFlagWithDeprecations, + bundles: Flags.string({ + char: 'b', + multiple: true, + summary: messages.getMessage('flags.bundles.summary'), + description: messages.getMessage('flags.bundles.description'), + }), + verbose: Flags.boolean({ summary: messages.getMessage('flags.verbose.summary') }), + } as const; + + private subscriberConn!: Connection; + private devHubConn!: Connection; + + public async run(): Promise { + const { flags } = await this.parse(PackageBundleInstalledListCommand); + + this.subscriberConn = flags['target-org'].getConnection(flags['api-version']); + this.devHubConn = flags['target-dev-hub'].getConnection(flags['api-version']); + + let bundleVersionIds = flags.bundles ?? []; + if (bundleVersionIds.length === 0) { + // Preferred: try InstalledPackageBundleVersion if available in subscriber org + try { + const query = + 'SELECT Id, PackageBundleVersion.Id FROM InstalledPackageBundleVersion ORDER BY CreatedDate DESC'; + const ipbv = await this.subscriberConn.tooling.query<{ Id: string; PackageBundleVersion?: { Id: string } }>( + query + ); + const ids = new Set(); + for (const r of ipbv.records) { + const id = r.PackageBundleVersion?.Id; + if (id) ids.add(id); + } + bundleVersionIds = [...ids]; + } catch { + // Fallback: derive from successful install requests + const successes = await PackageBundleInstall.getInstallStatuses( + this.subscriberConn, + BundleSObjects.PkgBundleVersionInstallReqStatus.success + ); + const ids = new Set(); + for (const r of successes) { + if (r.PackageBundleVersionID) ids.add(r.PackageBundleVersionID); + } + bundleVersionIds = [...ids]; + } + } + const results = await getInstalledBundleStatuses(this.subscriberConn, this.devHubConn, bundleVersionIds); + + if (results.length === 0) { + this.log(messages.getMessage('noBundles')); + return []; + } + + this.displayResults(results, flags.verbose); + return results; + } + + private displayResults(results: BundleInstalledListResults, verbose = false): void { + for (const r of results) { + this.styledHeader(chalk.blue(`${r.bundleName} (${r.bundleVersionNumber}) [${r.status}]`)); + const componentRows = r.components.map((c: InstalledBundleStatus['components'][number]) => ({ + Package: c.packageName, + 'Expected Version': c.expectedVersionNumber, + 'Actual Version': c.actualVersionNumber ?? 'Not Installed', + Status: c.status, + })); + this.table({ data: componentRows, overflow: 'wrap', title: chalk.gray(`Bundle ID ${r.bundleId}`) }); + if (verbose) this.log(''); + } + } +} + +