Merge pull request #32275 from meganide/feat/cli-pattern

feat: add --include and --output-file to backstage-cli-info
This commit is contained in:
Fredrik Adelöw
2026-01-20 15:34:30 +01:00
committed by GitHub
5 changed files with 269 additions and 18 deletions
+2
View File
@@ -148,7 +148,9 @@ Options:
Usage: <none>
Options:
--format
--help
--include
--version
```
+170 -14
View File
@@ -19,9 +19,41 @@ 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[];
format: 'text' | 'json';
}
/**
* 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'));
@@ -33,27 +65,151 @@ export default async () => {
const backstageJson = await fs.readJSON(backstageFile);
backstageVersion = backstageJson.version ?? 'N/A';
} catch (error) {
console.warn('The "backstage.json" file is not in the expected format');
console.log();
if (options.format !== 'json') {
console.warn(
'The "backstage.json" file is not in the expected format',
);
console.log();
}
}
}
console.log(`OS: ${os.type} ${os.release} - ${os.platform}/${os.arch}`);
console.log(`node: ${process.version}`);
console.log(`yarn: ${yarnVersion}`);
// Build system info
const systemInfo = {
os: `${os.type} ${os.release} - ${os.platform}/${os.arch}`,
node: process.version,
yarn: yarnVersion,
cli: { version: cliVersion, local: isLocal },
backstage: backstageVersion,
};
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
const targetPath = paths.targetRoot;
// Get workspace package names and their versions
const workspacePackages = new Map<string, string>();
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
}
// Collect all package names from lockfile
const allPackages = [...lockfile.keys()];
const includePatterns = options.include || [];
// Collect installed (non-local) packages
const installedDeps = new Set<string>();
// Collect local workspace packages
const localDeps = new Set<string>();
// 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 versions for a package as an array of objects
const getVersions = (dep: string): Array<{ version: string }> => {
const entries = lockfile.get(dep);
if (!entries) {
return [{ version: 'unknown' }];
}
const versions = [...new Set(entries.map(i => i.version))];
return versions.map(v => ({ version: v }));
};
const sortedInstalled = [...installedDeps].sort();
const sortedLocal = [...localDeps].sort();
// If format is json, output JSON to stdout
if (options.format === 'json') {
const output = {
system: systemInfo,
dependencies: Object.fromEntries(
sortedInstalled.map(dep => [dep, getVersions(dep)]),
),
local: Object.fromEntries(
sortedLocal.map(dep => [
dep,
[{ version: workspacePackages.get(dep) ?? 'unknown' }],
]),
),
};
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
return;
}
// Print to console
console.log(`OS: ${systemInfo.os}`);
console.log(`node: ${systemInfo.node}`);
console.log(`yarn: ${systemInfo.yarn}`);
console.log(`cli: ${cliVersion} (${isLocal ? 'local' : 'installed'})`);
console.log(`backstage: ${backstageVersion}`);
console.log();
// Print installed dependencies
console.log('Dependencies:');
const lockfilePath = paths.resolveTargetRoot('yarn.lock');
const lockfile = await Lockfile.load(lockfilePath);
if (sortedInstalled.length > 0) {
const maxLength = Math.max(...sortedInstalled.map(d => d.length));
for (const dep of sortedInstalled) {
const versions = getVersions(dep)
.map(v => v.version)
.join(', ');
console.log(` ${dep.padEnd(maxLength)} ${versions}`);
}
} else {
console.log(' (no installed Backstage packages found)');
}
const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/'));
const maxLength = Math.max(...deps.map(d => d.length));
for (const dep of deps) {
const versions = new Set(lockfile.get(dep)!.map(i => i.version));
console.log(` ${dep.padEnd(maxLength)} ${[...versions].join(', ')}`);
// Print local workspace packages
if (localDeps.size > 0) {
console.log();
console.log('Local:');
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}`);
}
}
});
};
+19 -2
View File
@@ -24,8 +24,25 @@ 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*)',
},
format: {
type: 'string',
choices: ['text', 'json'],
default: 'text',
description: 'Output format (text or json)',
},
})
.help()
.parse(args);
await lazy(() => import('./commands/info'), 'default')(argv);
},
});
},