From a6cd70a5b25275d073e5aa5c013a831c7d973cc8 Mon Sep 17 00:00:00 2001 From: Renas Date: Fri, 9 Jan 2026 16:38:37 +0100 Subject: [PATCH] feat: add --include to backstage-cli info Signed-off-by: Renas --- packages/cli/cli-report.md | 1 + .../cli/src/modules/info/commands/info.ts | 136 +++++++++++++++++- packages/cli/src/modules/info/index.ts | 15 +- 3 files changed, 143 insertions(+), 9 deletions(-) diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index a8df5242ad..fafd8c4f08 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -149,6 +149,7 @@ Usage: Options: --help + --include --version ``` diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index 97090d8b9a..8843dd6091 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -19,9 +19,43 @@ import os from 'os'; import { runOutput } from '@backstage/cli-common'; import { paths } from '../../../lib/paths'; import { Lockfile } from '../../../lib/versioning'; +import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; +import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -export default async () => { +interface InfoOptions { + include: string[]; +} + +/** + * Attempts to read package.json from node_modules for a given package name. + * Returns undefined if the package.json cannot be read. + */ +function tryReadPackageJson( + packageName: string, + targetPath: string, +): BackstagePackageJson | undefined { + try { + return require(require.resolve(`${packageName}/package.json`, { + paths: [targetPath], + })); + } catch { + return undefined; + } +} + +/** + * Checks if a package has a backstage field in its package.json + */ +function hasBackstageField( + packageName: string, + targetPath: string, +): boolean { + const pkg = tryReadPackageJson(packageName, targetPath); + return pkg?.backstage !== undefined; +} + +export default async (options: InfoOptions) => { await new Promise(async () => { const yarnVersion = await runOutput(['yarn', '--version']); const isLocal = fs.existsSync(paths.resolveOwn('./src')); @@ -44,16 +78,104 @@ export default async () => { console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`); console.log(`backstage: ${backstageVersion}`); console.log(); - console.log('Dependencies:'); + const lockfilePath = paths.resolveTargetRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); + const targetPath = paths.targetRoot; - const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/')); - const maxLength = Math.max(...deps.map(d => d.length)); + // Get workspace package names and their versions + const workspacePackages = new Map(); + try { + const packages = await PackageGraph.listTargetPackages(); + for (const pkg of packages) { + workspacePackages.set(pkg.packageJson.name, pkg.packageJson.version); + } + } catch { + // If we can't list workspace packages, continue without them + } - for (const dep of deps) { - const versions = new Set(lockfile.get(dep)!.map(i => i.version)); - console.log(` ${dep.padEnd(maxLength)} ${[...versions].join(', ')}`); + // Collect all package names from lockfile + const allPackages = [...lockfile.keys()]; + const includePatterns = options.include || []; + + // Collect installed (non-local) packages + const installedDeps = new Set(); + // Collect local workspace packages + const localDeps = new Set(); + + // Process @backstage/* packages + for (const pkg of allPackages) { + if (pkg.startsWith('@backstage/')) { + if (workspacePackages.has(pkg)) { + localDeps.add(pkg); + } else { + installedDeps.add(pkg); + } + } + } + + // Process packages matching --include patterns + for (const pattern of includePatterns) { + for (const pkg of allPackages) { + if (minimatch(pkg, pattern)) { + if (workspacePackages.has(pkg)) { + localDeps.add(pkg); + } else { + installedDeps.add(pkg); + } + } + } + } + + // Process packages with backstage field in their package.json + for (const pkg of allPackages) { + // Skip @backstage/* packages (already processed above) + if (pkg.startsWith('@backstage/')) { + continue; + } + if (workspacePackages.has(pkg)) { + // Check if local package has backstage field + if (hasBackstageField(pkg, targetPath)) { + localDeps.add(pkg); + } + } else if (hasBackstageField(pkg, targetPath)) { + installedDeps.add(pkg); + } + } + + // Helper to get version string for a package + const getVersions = (dep: string): string => { + const entries = lockfile.get(dep); + if (!entries) { + return 'unknown'; + } + const versions = [...new Set(entries.map(i => i.version))]; + return versions.join(', '); + }; + + // Print installed dependencies + console.log('Dependencies:'); + const sortedInstalled = [...installedDeps].sort(); + if (sortedInstalled.length > 0) { + const maxLength = Math.max(...sortedInstalled.map(d => d.length)); + for (const dep of sortedInstalled) { + const versions = getVersions(dep); + console.log(` ${dep.padEnd(maxLength)} ${versions}`); + } + } else { + console.log(' (no installed Backstage packages found)'); + } + + // Print local workspace packages + if (localDeps.size > 0) { + console.log(); + console.log('Local:'); + const sortedLocal = [...localDeps].sort(); + const maxLength = Math.max(...sortedLocal.map(d => d.length)); + for (const dep of sortedLocal) { + const version = workspacePackages.get(dep) ?? 'unknown'; + console.log(` ${dep.padEnd(maxLength)} ${version}`); + } } }); }; diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index e93f008905..9ec4347763 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -24,8 +24,19 @@ export default createCliPlugin({ path: ['info'], description: 'Show helpful information for debugging and reporting bugs', execute: async ({ args }) => { - yargs().parse(args); - await lazy(() => import('./commands/info'), 'default')(args); + const argv = await yargs() + .options({ + include: { + type: 'string', + array: true, + default: [], + description: + 'Glob patterns for additional packages to include (e.g., @spotify/backstage*)', + }, + }) + .help() + .parse(args); + await lazy(() => import('./commands/info'), 'default')(argv); }, }); },