From ff4a45ac015230131fea316229ef70c49d343e23 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Feb 2026 00:46:54 +0100 Subject: [PATCH 01/13] Migrate remaining CLI commands from commander to cleye All command handlers in the CLI now use cleye or node:util.parseArgs for argument parsing instead of commander. The top-level command registration in CliInitializer still uses commander, but all individual command implementations are now independent of it. Modules migrated: build, lint, test, maintenance, migrate, new, and create-github-app. Also replaced createScriptOptionsParser in both build and lint modules to use node:util.parseArgs instead of commander. Signed-off-by: Patrik Oldsberg --- .changeset/migrate-cli-commands-to-cleye.md | 5 + .../modules/build/commands/buildWorkspace.ts | 29 ++- .../build/commands/package/build/command.ts | 80 ++++++-- .../build/commands/package/build/index.ts | 2 +- .../build/commands/package/start/command.ts | 79 ++++++-- .../build/commands/package/start/index.ts | 2 +- .../src/modules/build/commands/repo/build.ts | 58 +++++- .../src/modules/build/commands/repo/start.ts | 76 +++++-- packages/cli/src/modules/build/index.ts | 187 +----------------- .../src/modules/build/lib/optionsParser.ts | 45 +---- .../cli/src/modules/build/lib/role.test.ts | 20 +- packages/cli/src/modules/build/lib/role.ts | 7 +- .../commands/create-github-app/index.ts | 15 +- .../src/modules/create-github-app/index.ts | 13 +- .../src/modules/lint/commands/package/lint.ts | 55 ++++-- .../src/modules/lint/commands/repo/lint.ts | 109 +++++++--- packages/cli/src/modules/lint/index.ts | 66 +------ .../cli/src/modules/lint/lib/optionsParser.ts | 45 +---- .../modules/maintenance/commands/repo/fix.ts | 36 +++- .../commands/repo/list-deprecations.ts | 22 ++- packages/cli/src/modules/maintenance/index.ts | 29 +-- .../migrate/commands/versions/bump.test.ts | 64 +++--- .../modules/migrate/commands/versions/bump.ts | 54 +++-- .../migrate/commands/versions/migrate.test.ts | 6 +- .../migrate/commands/versions/migrate.ts | 29 ++- packages/cli/src/modules/migrate/index.ts | 38 +--- .../cli/src/modules/new/commands/new.test.ts | 15 +- packages/cli/src/modules/new/commands/new.ts | 91 ++++++--- packages/cli/src/modules/new/index.ts | 42 +--- .../src/modules/test/commands/package/test.ts | 13 +- .../src/modules/test/commands/repo/test.ts | 36 ++-- packages/cli/src/modules/test/index.ts | 35 +--- 32 files changed, 707 insertions(+), 696 deletions(-) create mode 100644 .changeset/migrate-cli-commands-to-cleye.md diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md new file mode 100644 index 0000000000..5231353e50 --- /dev/null +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index 159c347eb8..6c27396a30 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -15,20 +15,39 @@ */ import fs from 'fs-extra'; +import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; +import type { CommandContext } from '../../../wiring/types'; -type Options = { - alwaysPack?: boolean; -}; +export default async ({ args, info }: CommandContext) => { + const { + flags: { alwaysPack }, + _: positionals, + } = cli( + { + help: info, + parameters: ['', '[packages...]'], + flags: { + alwaysPack: { + type: Boolean, + description: + 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', + }, + }, + }, + undefined, + args, + ); + + const [dir, ...packages] = positionals; -export default async (dir: string, packages: string[], options: Options) => { if (!(await fs.pathExists(dir))) { throw new Error(`Target workspace directory doesn't exist, '${dir}'`); } await createDistWorkspace(packages, { targetDir: dir, - alwaysPack: options.alwaysPack, + alwaysPack, enableFeatureDetection: true, }); }; diff --git a/packages/cli/src/modules/build/commands/package/build/command.ts b/packages/cli/src/modules/build/commands/package/build/command.ts index 944be832f0..8c802fe2b5 100644 --- a/packages/cli/src/modules/build/commands/package/build/command.ts +++ b/packages/cli/src/modules/build/commands/package/build/command.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import fs from 'fs-extra'; import { buildPackage, Output } from '../../../lib/builder'; import { findRoleFromCommand } from '../../../lib/role'; @@ -29,40 +29,90 @@ import { buildFrontend } from '../../../lib/buildFrontend'; import { buildBackend } from '../../../lib/buildBackend'; import { isValidUrl } from '../../../lib/urls'; import chalk from 'chalk'; +import type { CommandContext } from '../../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { + role, + minify, + skipBuildDependencies, + stats, + config, + moduleFederation, + }, + } = cli( + { + help: info, + flags: { + role: { + type: String, + description: 'Run the command with an explicit package role', + }, + minify: { + type: Boolean, + description: + 'Minify the generated code. Does not apply to app package (app is minified by default).', + }, + skipBuildDependencies: { + type: Boolean, + description: + 'Skip the automatic building of local dependencies. Applies to backend packages only.', + }, + stats: { + type: Boolean, + description: + 'If bundle stats are available, write them to the output directory. Applies to app packages only.', + }, + config: { + type: [String], + description: + 'Config files to load instead of app-config.yaml. Applies to app packages only.', + default: [], + }, + moduleFederation: { + type: Boolean, + description: + 'Build a package as a module federation remote. Applies to frontend plugin packages only.', + }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues): Promise { const webpack = process.env.LEGACY_WEBPACK_BUILD ? (require('webpack') as typeof import('webpack')) : undefined; - const role = await findRoleFromCommand(opts); + const resolvedRole = await findRoleFromCommand({ role }); - if (role === 'frontend' || role === 'backend') { - const configPaths = (opts.config as string[]).map(arg => { + if (resolvedRole === 'frontend' || resolvedRole === 'backend') { + const configPaths = config.map(arg => { if (isValidUrl(arg)) { return arg; } return targetPaths.resolve(arg); }); - if (role === 'frontend') { + if (resolvedRole === 'frontend') { return buildFrontend({ targetDir: targetPaths.dir, configPaths, - writeStats: Boolean(opts.stats), + writeStats: Boolean(stats), webpack, }); } return buildBackend({ targetDir: targetPaths.dir, configPaths, - skipBuildDependencies: Boolean(opts.skipBuildDependencies), - minify: Boolean(opts.minify), + skipBuildDependencies: Boolean(skipBuildDependencies), + minify: Boolean(minify), }); } let isModuleFederationRemote: boolean | undefined = undefined; - if ((role as string) === 'frontend-dynamic-container') { + if ((resolvedRole as string) === 'frontend-dynamic-container') { console.log( chalk.yellow( `⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`, @@ -70,7 +120,7 @@ export async function command(opts: OptionValues): Promise { ); isModuleFederationRemote = true; } - if (opts.moduleFederation) { + if (moduleFederation) { isModuleFederationRemote = true; } @@ -79,13 +129,13 @@ export async function command(opts: OptionValues): Promise { return buildFrontend({ targetDir: targetPaths.dir, configPaths: [], - writeStats: Boolean(opts.stats), + writeStats: Boolean(stats), isModuleFederationRemote, webpack, }); } - const roleInfo = PackageRoles.getRoleInfo(role); + const roleInfo = PackageRoles.getRoleInfo(resolvedRole); const outputs = new Set(); @@ -106,7 +156,7 @@ export async function command(opts: OptionValues): Promise { return buildPackage({ outputs, packageJson, - minify: Boolean(opts.minify), + minify: Boolean(minify), workspacePackages: await PackageGraph.listTargetPackages(), }); -} +}; diff --git a/packages/cli/src/modules/build/commands/package/build/index.ts b/packages/cli/src/modules/build/commands/package/build/index.ts index 680fe9e11d..7121eac500 100644 --- a/packages/cli/src/modules/build/commands/package/build/index.ts +++ b/packages/cli/src/modules/build/commands/package/build/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { command } from './command'; +export { default } from './command'; diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 18f06ab0d2..ae85fec56e 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -14,22 +14,79 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { startPackage } from './startPackage'; import { resolveLinkedWorkspace } from './resolveLinkedWorkspace'; import { findRoleFromCommand } from '../../../lib/role'; import { targetPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { + config, + role, + check, + inspect, + inspectBrk, + require: requirePaths, + link, + entrypoint, + }, + } = cli( + { + help: info, + flags: { + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + default: [], + }, + role: { + type: String, + description: 'Run the command with an explicit package role', + }, + check: { + type: Boolean, + description: 'Enable type checking and linting if available', + }, + inspect: { + type: String, + description: 'Enable debugger in Node.js environments', + }, + inspectBrk: { + type: String, + description: + 'Enable debugger in Node.js environments, breaking before code starts', + }, + require: { + type: [String], + description: 'Add a --require argument to the node process', + }, + link: { + type: String, + description: 'Link an external workspace for module resolution', + }, + entrypoint: { + type: String, + description: + 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', + }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues): Promise { await startPackage({ - role: await findRoleFromCommand(opts), - entrypoint: opts.entrypoint, + role: await findRoleFromCommand({ role }), + entrypoint, targetDir: targetPaths.dir, - configPaths: opts.config as string[], - checksEnabled: Boolean(opts.check), - linkedWorkspace: await resolveLinkedWorkspace(opts.link), - inspectEnabled: opts.inspect, - inspectBrkEnabled: opts.inspectBrk, - require: opts.require, + configPaths: config, + checksEnabled: Boolean(check), + linkedWorkspace: await resolveLinkedWorkspace(link), + inspectEnabled: inspect, + inspectBrkEnabled: inspectBrk, + require: requirePaths?.[0], }); -} +}; diff --git a/packages/cli/src/modules/build/commands/package/start/index.ts b/packages/cli/src/modules/build/commands/package/start/index.ts index 680fe9e11d..7121eac500 100644 --- a/packages/cli/src/modules/build/commands/package/start/index.ts +++ b/packages/cli/src/modules/build/commands/package/start/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { command } from './command'; +export { default } from './command'; diff --git a/packages/cli/src/modules/build/commands/repo/build.ts b/packages/cli/src/modules/build/commands/repo/build.ts index 59d0a839e4..b1e0239b4b 100644 --- a/packages/cli/src/modules/build/commands/repo/build.ts +++ b/packages/cli/src/modules/build/commands/repo/build.ts @@ -15,7 +15,7 @@ */ import chalk from 'chalk'; -import { Command, OptionValues } from 'commander'; +import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { buildPackages, getOutputsForRole } from '../../lib/builder'; import { targetPaths } from '@backstage/cli-common'; @@ -29,18 +29,46 @@ import { import { buildFrontend } from '../../lib/buildFrontend'; import { buildBackend } from '../../lib/buildBackend'; import { createScriptOptionsParser } from '../../lib/optionsParser'; +import type { CommandContext } from '../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { all, since, minify }, + } = cli( + { + help: info, + flags: { + all: { + type: Boolean, + description: + 'Build all packages, including bundled app and backend packages.', + }, + since: { + type: String, + description: + 'Only build packages and their dev dependents that changed since the specified ref', + }, + minify: { + type: Boolean, + description: + 'Minify the generated code. Does not apply to app package (app is minified by default).', + }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues, cmd: Command): Promise { let packages = await PackageGraph.listTargetPackages(); const webpack = process.env.LEGACY_WEBPACK_BUILD ? (require('webpack') as typeof import('webpack')) : undefined; - if (opts.since) { + if (since) { const graph = PackageGraph.fromPackages(packages); const changedPackages = await graph.listChangedPackages({ - ref: opts.since, + ref: since, analyzeLockfile: true, }); const withDevDependents = graph.collectPackageNames( @@ -53,7 +81,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const apps = new Array(); const backends = new Array(); - const parseBuildScript = createScriptOptionsParser(cmd, ['package', 'build']); + const parseBuildScript = createScriptOptionsParser(['package', 'build'], { + role: { type: 'string' }, + minify: { type: 'boolean' }, + 'skip-build-dependencies': { type: 'boolean' }, + stats: { type: 'boolean' }, + config: { type: 'string', multiple: true }, + 'module-federation': { type: 'boolean' }, + }); const options = packages.flatMap(pkg => { const role = @@ -92,14 +127,14 @@ export async function command(opts: OptionValues, cmd: Command): Promise { outputs, logPrefix: `${chalk.cyan(relativePath(targetPaths.rootDir, pkg.dir))}: `, workspacePackages: packages, - minify: opts.minify ?? buildOptions.minify, + minify: minify ?? Boolean(buildOptions.minify), }; }); console.log('Building packages'); await buildPackages(options); - if (opts.all) { + if (all) { console.log('Building apps'); await runConcurrentTasks({ items: apps, @@ -112,9 +147,12 @@ export async function command(opts: OptionValues, cmd: Command): Promise { ); return; } + const configPaths = buildOptions.config; await buildFrontend({ targetDir: pkg.dir, - configPaths: (buildOptions.config as string[]) ?? [], + configPaths: Array.isArray(configPaths) + ? (configPaths as string[]) + : [], writeStats: Boolean(buildOptions.stats), webpack, }); @@ -136,9 +174,9 @@ export async function command(opts: OptionValues, cmd: Command): Promise { await buildBackend({ targetDir: pkg.dir, skipBuildDependencies: true, - minify: opts.minify ?? buildOptions.minify, + minify: minify ?? Boolean(buildOptions.minify), }); }, }); } -} +}; diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index f7faa46eb1..5cb5e591fb 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -21,10 +21,12 @@ import { } from '@backstage/cli-node'; import { relative as relativePath } from 'node:path'; import { targetPaths } from '@backstage/cli-common'; +import { cli } from 'cleye'; import { resolveLinkedWorkspace } from '../package/start/resolveLinkedWorkspace'; import { startPackage } from '../package/start/startPackage'; import { parseArgs } from 'node:util'; +import type { CommandContext } from '../../../../wiring/types'; const ACCEPTED_PACKAGE_ROLES: Array = [ 'frontend', @@ -33,19 +35,60 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ 'backend-plugin', ]; -type CommandOptions = { - plugin: string[]; - config: string[]; - inspect?: boolean | string; - inspectBrk?: boolean | string; - require?: string; - link?: string; -}; +export default async ({ args, info }: CommandContext) => { + const { + flags: { plugin, config, inspect, inspectBrk, require: requirePath, link }, + _: namesOrPaths, + } = cli( + { + help: info, + flags: { + plugin: { + type: [String], + description: + 'Start the dev entry-point for any matching plugin package in the repo', + default: [], + }, + config: { + type: [String], + description: 'Config files to load instead of app-config.yaml', + default: [], + }, + inspect: { + type: String, + description: + 'Enable debugger in Node.js environments. Applies to backend package only', + }, + inspectBrk: { + type: String, + description: + 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', + }, + require: { + type: String, + description: + 'Add a --require argument to the node process. Applies to backend package only', + }, + link: { + type: String, + description: 'Link an external workspace for module resolution', + }, + }, + }, + undefined, + args, + ); -export async function command(namesOrPaths: string[], options: CommandOptions) { - const targetPackages = await findTargetPackages(namesOrPaths, options.plugin); + const targetPackages = await findTargetPackages(namesOrPaths, plugin); - const packageOptions = await resolvePackageOptions(targetPackages, options); + const packageOptions = await resolvePackageOptions(targetPackages, { + plugin, + config, + inspect, + inspectBrk, + require: requirePath, + link, + }); if (packageOptions.length === 0) { console.log('No packages found to start'); @@ -60,7 +103,7 @@ export async function command(namesOrPaths: string[], options: CommandOptions) { // Each of these block until interrupted by user await Promise.all(packageOptions.map(entry => startPackage(entry.options))); -} +}; export async function findTargetPackages( namesOrPaths: string[], @@ -165,6 +208,15 @@ export async function findTargetPackages( ); } +type CommandOptions = { + plugin: string[]; + config: string[]; + inspect?: string; + inspectBrk?: string; + require?: string; + link?: string; +}; + async function resolvePackageOptions( targetPackages: BackstagePackage[], options: CommandOptions, diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 1e8d1d2060..139c591af2 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -14,46 +14,7 @@ * limitations under the License. */ -import { Command, Option } from 'commander'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../wiring/lazy'; - -const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), -] as const; - -export function registerPackageCommands(command: Command) { - command - .command('build') - .description('Build a package for production deployment or publishing') - .option('--role ', 'Run the command with an explicit package role') - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies. Applies to backend packages only.', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory. Applies to app packages only.', - ) - .option( - '--config ', - 'Config files to load instead of app-config.yaml. Applies to app packages only.', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option( - '--module-federation', - 'Build a package as a module federation remote. Applies to frontend plugin packages only.', - ) - .action(lazy(() => import('./commands/package/build'), 'command')); -} export const buildPlugin = createCliPlugin({ pluginId: 'build', @@ -61,146 +22,26 @@ export const buildPlugin = createCliPlugin({ reg.addCommand({ path: ['package', 'build'], description: 'Build a package for production deployment or publishing', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option( - '--role ', - 'Run the command with an explicit package role', - ) - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .option( - '--skip-build-dependencies', - 'Skip the automatic building of local dependencies. Applies to backend packages only.', - ) - .option( - '--stats', - 'If bundle stats are available, write them to the output directory. Applies to app packages only.', - ) - .option( - '--config ', - 'Config files to load instead of app-config.yaml. Applies to app packages only.', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option( - '--module-federation', - 'Build a package as a module federation remote. Applies to frontend plugin packages only.', - ) - .action(lazy(() => import('./commands/package/build'), 'command')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/package/build') }, }); reg.addCommand({ path: ['repo', 'build'], description: 'Build packages in the project, excluding bundled app and backend packages.', - execute: async ({ args }) => { - const command = new Command(); - - // This command expect `package build` to be registered, as its used to parse - // individual plugins' package build scripts. - registerPackageCommands(command.command('package')); - - const defaultCommand = command - .option( - '--all', - 'Build all packages, including bundled app and backend packages.', - ) - .option( - '--since ', - 'Only build packages and their dev dependents that changed since the specified ref', - ) - .option( - '--minify', - 'Minify the generated code. Does not apply to app package (app is minified by default).', - ) - .action(lazy(() => import('./commands/repo/build'), 'command')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/build') }, }); reg.addCommand({ path: ['package', 'start'], description: 'Start a package for local development', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option(...configOption) - .option( - '--role ', - 'Run the command with an explicit package role', - ) - .option('--check', 'Enable type checking and linting if available') - .option('--inspect [host]', 'Enable debugger in Node.js environments') - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts', - ) - .option( - '--require ', - 'Add a --require argument to the node process', - ) - .option( - '--link ', - 'Link an external workspace for module resolution', - ) - .option( - '--entrypoint ', - 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', - ) - .action(lazy(() => import('./commands/package/start'), 'command')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/package/start') }, }); reg.addCommand({ path: ['repo', 'start'], description: 'Starts packages in the repo for local development', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .argument( - '[packageNameOrPath...]', - 'Run the specified package instead of the defaults.', - ) - .option( - '--plugin ', - 'Start the dev entry-point for any matching plugin package in the repo', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), - ) - .option(...configOption) - .option( - '--inspect [host]', - 'Enable debugger in Node.js environments. Applies to backend package only', - ) - .option( - '--inspect-brk [host]', - 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', - ) - .option( - '--require ', - 'Add a --require argument to the node process. Applies to backend package only', - ) - .option( - '--link ', - 'Link an external workspace for module resolution', - ) - .action( - lazy(() => import('../build/commands/repo/start'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/start') }, }); reg.addCommand({ @@ -239,25 +80,7 @@ export const buildPlugin = createCliPlugin({ path: ['build-workspace'], description: 'Builds a temporary dist workspace from the provided packages', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .arguments(' [packages...]') - .addOption( - new Option( - '--alwaysYarnPack', - 'Alias for --alwaysPack for backwards compatibility.', - ) - .implies({ alwaysPack: true }) - .hideHelp(true), - ) - .option( - '--alwaysPack', - 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', - ) - .action(lazy(() => import('./commands/buildWorkspace'), 'default')); - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/buildWorkspace') }, }); }, }); diff --git a/packages/cli/src/modules/build/lib/optionsParser.ts b/packages/cli/src/modules/build/lib/optionsParser.ts index 15561a2e42..964825830a 100644 --- a/packages/cli/src/modules/build/lib/optionsParser.ts +++ b/packages/cli/src/modules/build/lib/optionsParser.ts @@ -13,34 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Command } from 'commander'; +import { parseArgs, type ParseArgsConfig } from 'node:util'; export function createScriptOptionsParser( - anyCmd: Command, commandPath: string[], + options: ParseArgsConfig['options'], ) { - // Regardless of what command instance is passed in we want to find - // the root command and resolve the path from there - let rootCmd = anyCmd; - while (rootCmd.parent) { - rootCmd = rootCmd.parent; - } - - // Now find the command that was requested - let targetCmd = rootCmd as Command | undefined; - for (const name of commandPath) { - targetCmd = targetCmd?.commands.find(c => c.name() === name) as - | Command - | undefined; - } - - if (!targetCmd) { - throw new Error( - `Could not find package command '${commandPath.join(' ')}'`, - ); - } - const cmd = targetCmd; - const expectedScript = `backstage-cli ${commandPath.join(' ')}`; return (scriptStr?: string) => { @@ -49,22 +27,9 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); + const args = argsStr ? argsStr.split(/\s+/) : []; - // Can't clone or copy or even use commands as prototype, so we mutate - // the necessary members instead, and then reset them once we're done - const currentOpts = (cmd as any)._optionValues; - const currentStore = (cmd as any)._storeOptionsAsProperties; - - const result: Record = {}; - (cmd as any)._storeOptionsAsProperties = false; - (cmd as any)._optionValues = result; - - // Triggers the writing of options to the result object - cmd.parseOptions(argsStr.split(' ')); - - (cmd as any)._optionValues = currentOpts; - (cmd as any)._storeOptionsAsProperties = currentStore; - - return result; + const { values } = parseArgs({ args, strict: false, options }); + return values; }; } diff --git a/packages/cli/src/modules/build/lib/role.test.ts b/packages/cli/src/modules/build/lib/role.test.ts index dbed4310fa..b046d42432 100644 --- a/packages/cli/src/modules/build/lib/role.test.ts +++ b/packages/cli/src/modules/build/lib/role.test.ts @@ -16,20 +16,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; -import { Command } from 'commander'; import { findRoleFromCommand } from './role'; const mockDir = createMockDirectory(); overrideTargetPaths(mockDir.path); describe('findRoleFromCommand', () => { - function mkCommand(args?: string) { - const parsed = new Command() - .option('--role ', 'test role') - .parse(args?.split(' ') ?? [], { from: 'user' }); - return parsed.opts(); - } - beforeEach(() => { mockDir.setContent({ 'package.json': JSON.stringify({ @@ -42,16 +34,14 @@ describe('findRoleFromCommand', () => { }); it('provides role info by role', async () => { - await expect(findRoleFromCommand(mkCommand())).resolves.toEqual( - 'web-library', - ); + await expect(findRoleFromCommand({})).resolves.toEqual('web-library'); await expect( - findRoleFromCommand(mkCommand('--role node-library')), + findRoleFromCommand({ role: 'node-library' }), ).resolves.toEqual('node-library'); - await expect( - findRoleFromCommand(mkCommand('--role invalid')), - ).rejects.toThrow(`Unknown package role 'invalid'`); + await expect(findRoleFromCommand({ role: 'invalid' })).rejects.toThrow( + `Unknown package role 'invalid'`, + ); }); }); diff --git a/packages/cli/src/modules/build/lib/role.ts b/packages/cli/src/modules/build/lib/role.ts index 2f3960433b..bfcd1ecfb2 100644 --- a/packages/cli/src/modules/build/lib/role.ts +++ b/packages/cli/src/modules/build/lib/role.ts @@ -15,14 +15,13 @@ */ import fs from 'fs-extra'; -import { OptionValues } from 'commander'; import { targetPaths } from '@backstage/cli-common'; import { PackageRoles, PackageRole } from '@backstage/cli-node'; -export async function findRoleFromCommand( - opts: OptionValues, -): Promise { +export async function findRoleFromCommand(opts: { + role?: string; +}): Promise { if (opts.role) { return PackageRoles.getRoleInfo(opts.role)?.role; } diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts index 57aca5674f..998f8eb7ae 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts @@ -19,14 +19,27 @@ import chalk from 'chalk'; import { stringify as stringifyYaml } from 'yaml'; import inquirer, { Question, Answers } from 'inquirer'; import { targetPaths } from '@backstage/cli-common'; +import { cli } from 'cleye'; import { GithubCreateAppServer } from './GithubCreateAppServer'; import openBrowser from 'react-dev-utils/openBrowser'; +import type { CommandContext } from '../../../../wiring/types'; // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest -export default async (org: string) => { +export default async ({ args, info }: CommandContext) => { + const { _: positionals } = cli( + { + help: info, + parameters: [''], + }, + undefined, + args, + ); + + const org = positionals[0]; + const answers: Answers = await inquirer.prompt({ name: 'appType', type: 'checkbox', diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index 5a3e30e870..9dad46577e 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'new', @@ -23,16 +21,7 @@ export default createCliPlugin({ reg.addCommand({ path: ['create-github-app'], description: 'Create new GitHub App in your organization.', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .argument('') - .action( - lazy(() => import('./commands/create-github-app'), 'default'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/create-github-app') }, }); }, }); diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli/src/modules/lint/commands/package/lint.ts index 0a453ac71a..7cecee10a7 100644 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ b/packages/cli/src/modules/lint/commands/package/lint.ts @@ -15,15 +15,46 @@ */ import fs from 'fs-extra'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { targetPaths } from '@backstage/cli-common'; - import { ESLint } from 'eslint'; +import type { CommandContext } from '../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { fix, format, outputFile, maxWarnings }, + _: directories, + } = cli( + { + help: info, + flags: { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }, + }, + undefined, + args, + ); -export default async (directories: string[], opts: OptionValues) => { const eslint = new ESLint({ cwd: targetPaths.dir, - fix: opts.fix, + fix, extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], }); @@ -31,31 +62,31 @@ export default async (directories: string[], opts: OptionValues) => { directories.length ? directories : ['.'], ); - const maxWarnings = opts.maxWarnings ?? -1; - const ignoreWarnings = +maxWarnings === -1; + const maxWarningsNum = maxWarnings ? +maxWarnings : -1; + const ignoreWarnings = maxWarningsNum === -1; const failed = results.some(r => r.errorCount > 0) || (!ignoreWarnings && results.reduce((current, next) => current + next.warningCount, 0) > - maxWarnings); + maxWarningsNum); - if (opts.fix) { + if (fix) { await ESLint.outputFixes(results); } - const formatter = await eslint.loadFormatter(opts.format); + const formatter = await eslint.loadFormatter(format); // This formatter uses the cwd to format file paths, so let's have that happen from the root instead - if (opts.format === 'eslint-formatter-friendly') { + if (format === 'eslint-formatter-friendly') { process.chdir(targetPaths.rootDir); } const resultText = await formatter.format(results); if (resultText) { - if (opts.outputFile) { - await fs.writeFile(targetPaths.resolve(opts.outputFile), resultText); + if (outputFile) { + await fs.writeFile(targetPaths.resolve(outputFile), resultText); } else { console.log(resultText); } diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index a3ae69302c..513c76e0a5 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -16,7 +16,7 @@ import chalk from 'chalk'; import fs from 'fs-extra'; -import { Command, OptionValues } from 'commander'; +import { cli } from 'cleye'; import { createHash } from 'node:crypto'; import { relative as relativePath } from 'node:path'; import { @@ -29,6 +29,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; +import type { CommandContext } from '../../../../wiring/types'; function depCount(pkg: BackstagePackageJson) { const deps = pkg.dependencies ? Object.keys(pkg.dependencies).length : 0; @@ -38,24 +39,77 @@ function depCount(pkg: BackstagePackageJson) { return deps + devDeps; } -export async function command(opts: OptionValues, cmd: Command): Promise { +export default async ({ args, info }: CommandContext) => { + const { + flags: { + fix, + format, + outputFile, + successCache: useSuccessCache, + successCacheDir, + since, + maxWarnings, + }, + } = cli( + { + help: info, + flags: { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + successCache: { + type: Boolean, + description: + 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', + }, + successCacheDir: { + type: String, + description: + 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', + }, + since: { + type: String, + description: + 'Only lint packages that changed since the specified ref', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }, + }, + undefined, + args, + ); + let packages = await PackageGraph.listTargetPackages(); const cache = SuccessCache.create({ name: 'lint', - basePath: opts.successCacheDir, + basePath: successCacheDir, }); - const cacheContext = opts.successCache + const cacheContext = useSuccessCache ? { entries: await cache.read(), lockfile: await Lockfile.load(targetPaths.resolveRoot('yarn.lock')), } : undefined; - if (opts.since) { + if (since) { const graph = PackageGraph.fromPackages(packages); packages = await graph.listChangedPackages({ - ref: opts.since, + ref: since, analyzeLockfile: true, }); } @@ -65,7 +119,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { packages.sort((a, b) => depCount(b.packageJson) - depCount(a.packageJson)); // This formatter uses the cwd to format file paths, so let's have that happen from the root instead - if (opts.format === 'eslint-formatter-friendly') { + if (format === 'eslint-formatter-friendly') { process.chdir(targetPaths.rootDir); } @@ -74,7 +128,12 @@ export async function command(opts: OptionValues, cmd: Command): Promise { process.env.FORCE_COLOR = '1'; } - const parseLintScript = createScriptOptionsParser(cmd, ['package', 'lint']); + const parseLintScript = createScriptOptionsParser(['package', 'lint'], { + fix: { type: 'boolean' }, + format: { type: 'string' }, + 'output-file': { type: 'string' }, + 'max-warnings': { type: 'string' }, + }); const items = await Promise.all( packages.map(async pkg => { @@ -112,20 +171,20 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const { results: resultsList } = await runWorkerQueueThreads({ items: items.filter(item => item.lintOptions), // Filter out packages without lint script context: { - fix: Boolean(opts.fix), - format: opts.format as string | undefined, + fix: Boolean(fix), + format: format as string | undefined, shouldCache: Boolean(cacheContext), - maxWarnings: opts.maxWarnings ?? -1, + maxWarnings: maxWarnings ?? '-1', successCache: cacheContext?.entries, rootDir: targetPaths.rootDir, }, workerFactory: async ({ - fix, - format, + fix: workerFix, + format: workerFormat, shouldCache, successCache, rootDir, - maxWarnings, + maxWarnings: workerMaxWarnings, }) => { const { ESLint } = require('eslint') as typeof import('eslint'); const crypto = require('node:crypto') as typeof import('crypto'); @@ -151,7 +210,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const start = Date.now(); const eslint = new ESLint({ cwd: fullDir, - fix, + fix: workerFix, extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], }); @@ -192,7 +251,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } } - const formatter = await eslint.loadFormatter(format); + const formatter = await eslint.loadFormatter(workerFormat); const results = await eslint.lintFiles(['.']); @@ -200,18 +259,18 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const time = ((Date.now() - start) / 1000).toFixed(2); console.log(`Checked ${count} files in ${relativeDir} ${time}s`); - if (fix) { + if (workerFix) { await ESLint.outputFixes(results); } - const ignoreWarnings = +maxWarnings === -1; + const ignoreWarnings = +workerMaxWarnings === -1; const resultText = formatter.format(results) as string; const failed = results.some(r => r.errorCount > 0) || (!ignoreWarnings && results.reduce((current, next) => current + next.warningCount, 0) > - maxWarnings); + +workerMaxWarnings); return { relativeDir, @@ -242,8 +301,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { // When doing repo lint, only list the results if the lint failed to avoid a log // dump of all warnings that might be irrelevant if (resultText) { - if (opts.outputFile) { - if (opts.format === 'json') { + if (outputFile) { + if (format === 'json') { jsonResults.push(resultText); } else { errorOutput += `${resultText}\n`; @@ -258,7 +317,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } } - if (opts.format === 'json') { + if (format === 'json') { let mergedJsonResults: any[] = []; for (const jsonResult of jsonResults) { mergedJsonResults = mergedJsonResults.concat(JSON.parse(jsonResult)); @@ -266,8 +325,8 @@ export async function command(opts: OptionValues, cmd: Command): Promise { errorOutput = JSON.stringify(mergedJsonResults, null, 2); } - if (opts.outputFile && errorOutput) { - await fs.writeFile(targetPaths.resolveRoot(opts.outputFile), errorOutput); + if (outputFile && errorOutput) { + await fs.writeFile(targetPaths.resolveRoot(outputFile), errorOutput); } if (cacheContext) { @@ -277,4 +336,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise { if (failed) { process.exit(1); } -} +}; diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 40c374aa3b..8e02b7653f 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -14,28 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../wiring/lazy'; - -export function registerPackageLintCommand(command: Command) { - command.arguments('[directories...]'); - command.option('--fix', 'Attempt to automatically fix violations'); - command.option( - '--format ', - 'Lint report output format', - 'eslint-formatter-friendly', - ); - command.option( - '--output-file ', - 'Write the lint report to a file instead of stdout', - ); - command.option( - '--max-warnings ', - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - ); - command.description('Lint a package'); - command.action(lazy(() => import('./commands/package/lint'), 'default')); -} export default createCliPlugin({ pluginId: 'lint', @@ -43,53 +21,13 @@ export default createCliPlugin({ reg.addCommand({ path: ['package', 'lint'], description: 'Lint a package', - execute: async ({ args }) => { - const command = new Command(); - registerPackageLintCommand(command); - - await command.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/package/lint') }, }); reg.addCommand({ path: ['repo', 'lint'], description: 'Lint a repository', - execute: async ({ args }) => { - const command = new Command(); - - registerPackageLintCommand(command.command('package').command('lint')); - - command.option('--fix', 'Attempt to automatically fix violations'); - command.option( - '--format ', - 'Lint report output format', - 'eslint-formatter-friendly', - ); - command.option( - '--output-file ', - 'Write the lint report to a file instead of stdout', - ); - command.option( - '--successCache', - 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', - ); - command.option( - '--successCacheDir ', - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - ); - command.option( - '--since ', - 'Only lint packages that changed since the specified ref', - ); - command.option( - '--max-warnings ', - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - ); - command.description('Lint a repository'); - command.action(lazy(() => import('./commands/repo/lint'), 'command')); - - await command.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/lint') }, }); }, }); diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli/src/modules/lint/lib/optionsParser.ts index 15561a2e42..964825830a 100644 --- a/packages/cli/src/modules/lint/lib/optionsParser.ts +++ b/packages/cli/src/modules/lint/lib/optionsParser.ts @@ -13,34 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Command } from 'commander'; +import { parseArgs, type ParseArgsConfig } from 'node:util'; export function createScriptOptionsParser( - anyCmd: Command, commandPath: string[], + options: ParseArgsConfig['options'], ) { - // Regardless of what command instance is passed in we want to find - // the root command and resolve the path from there - let rootCmd = anyCmd; - while (rootCmd.parent) { - rootCmd = rootCmd.parent; - } - - // Now find the command that was requested - let targetCmd = rootCmd as Command | undefined; - for (const name of commandPath) { - targetCmd = targetCmd?.commands.find(c => c.name() === name) as - | Command - | undefined; - } - - if (!targetCmd) { - throw new Error( - `Could not find package command '${commandPath.join(' ')}'`, - ); - } - const cmd = targetCmd; - const expectedScript = `backstage-cli ${commandPath.join(' ')}`; return (scriptStr?: string) => { @@ -49,22 +27,9 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); + const args = argsStr ? argsStr.split(/\s+/) : []; - // Can't clone or copy or even use commands as prototype, so we mutate - // the necessary members instead, and then reset them once we're done - const currentOpts = (cmd as any)._optionValues; - const currentStore = (cmd as any)._storeOptionsAsProperties; - - const result: Record = {}; - (cmd as any)._storeOptionsAsProperties = false; - (cmd as any)._optionValues = result; - - // Triggers the writing of options to the result object - cmd.parseOptions(argsStr.split(' ')); - - (cmd as any)._optionValues = currentOpts; - (cmd as any)._storeOptionsAsProperties = currentStore; - - return result; + const { values } = parseArgs({ args, strict: false, options }); + return values; }; } diff --git a/packages/cli/src/modules/maintenance/commands/repo/fix.ts b/packages/cli/src/modules/maintenance/commands/repo/fix.ts index de09d8b490..ea367d813a 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/fix.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/fix.ts @@ -21,7 +21,7 @@ import { PackageRole, PackageRoles, } from '@backstage/cli-node'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import fs from 'fs-extra'; import { resolve as resolvePath, @@ -492,14 +492,38 @@ export function fixPeerModules(pkg: FixablePackage) { type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void; -export async function command(opts: OptionValues): Promise { +export default async ({ + args, + info, +}: import('../../../../wiring/types').CommandContext) => { + const { + flags: { publish, check }, + } = cli( + { + help: info, + flags: { + publish: { + type: Boolean, + description: + 'Enable additional fixes that only apply when publishing packages', + }, + check: { + type: Boolean, + description: + 'Fail if any packages would have been changed by the command', + }, + }, + }, + undefined, + args, + ); + const packages = await readFixablePackages(); const fixRepositoryField = createRepositoryFieldFixer(); const fixers: PackageFixer[] = [fixPackageExports, fixSideEffects]; - // Fixers that only apply to repos that publish packages - if (opts.publish) { + if (publish) { fixers.push( fixRepositoryField, fixPluginId, @@ -514,11 +538,11 @@ export async function command(opts: OptionValues): Promise { } } - if (opts.check) { + if (check) { if (printPackageFixHint(packages)) { process.exit(1); } } else { await writeFixedPackages(packages); } -} +}; diff --git a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts b/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts index f4114838a1..3b081119ee 100644 --- a/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts +++ b/packages/cli/src/modules/maintenance/commands/repo/list-deprecations.ts @@ -16,12 +16,26 @@ import chalk from 'chalk'; import { ESLint } from 'eslint'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { relative as relativePath } from 'node:path'; import { PackageGraph } from '@backstage/cli-node'; import { targetPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; + +export default async ({ args, info }: CommandContext) => { + const { + flags: { json }, + } = cli( + { + help: info, + flags: { + json: { type: Boolean, description: 'Output as JSON' }, + }, + }, + undefined, + args, + ); -export async function command(opts: OptionValues) { const packages = await PackageGraph.listTargetPackages(); const eslint = new ESLint({ @@ -74,7 +88,7 @@ export async function command(opts: OptionValues) { stderr.cursorTo(0); } - if (opts.json) { + if (json) { console.log(JSON.stringify(deprecations, null, 2)); } else { for (const d of deprecations) { @@ -87,4 +101,4 @@ export async function command(opts: OptionValues) { if (deprecations.length > 0) { process.exit(1); } -} +}; diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index e2584527f0..a6597559f8 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Command } from 'commander'; import { createCliPlugin } from '../../wiring/factory'; -import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'maintenance', @@ -23,36 +21,13 @@ export default createCliPlugin({ reg.addCommand({ path: ['repo', 'fix'], description: 'Automatically fix packages in the project', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--publish', - 'Enable additional fixes that only apply when publishing packages', - ) - .option( - '--check', - 'Fail if any packages would have been changed by the command', - ) - .action(lazy(() => import('./commands/repo/fix'), 'command')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/fix') }, }); reg.addCommand({ path: ['repo', 'list-deprecations'], description: 'List deprecations', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option('--json', 'Output as JSON') - .action( - lazy(() => import('./commands/repo/list-deprecations'), 'command'), - ); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/list-deprecations') }, }); }, }); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index c80af07c16..88e10e2607 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ import fs from 'fs-extra'; -import { Command } from 'commander'; import * as runObj from '@backstage/cli-common'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; +import type { CommandContext } from '../../../../wiring/types'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; @@ -126,6 +126,28 @@ const expectLogsToMatch = ( expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort()); }; +function callBump(flags: Record) { + const args: string[] = []; + for (const [key, value] of Object.entries(flags)) { + if (value === null || value === undefined) { + continue; + } + const flag = `--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`; + if (typeof value === 'boolean') { + if (value) { + args.push(flag); + } + } else { + args.push(flag, value); + } + } + const context: CommandContext = { + args, + info: { usage: 'backstage-cli versions:bump', description: 'test' }, + }; + return bump(context); +} + describe('bump', () => { const mockDir = createMockDirectory(); @@ -190,7 +212,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -283,11 +305,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ - pattern: null, - release: 'main', - skipInstall: true, - } as unknown as Command); + await callBump({ release: 'main', skipInstall: true }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -389,7 +407,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -493,7 +511,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -588,9 +606,9 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await expect( - bump({ pattern: null, release: '999.0.1' } as unknown as Command), - ).rejects.toThrow('No release found for 999.0.1 version'); + await expect(callBump({ release: '999.0.1' })).rejects.toThrow( + 'No release found for 999.0.1 version', + ); }); expect(logs.filter(Boolean)).toEqual([ 'Using default pattern glob @backstage/*', @@ -694,7 +712,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'next' } as unknown as Command); + await callBump({ release: 'next' }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -773,10 +791,10 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ + await callBump({ pattern: '@{backstage,backstage-extra}/*', release: 'main', - } as any); + }); }); expectLogsToMatch(logs, [ 'Using custom pattern glob @{backstage,backstage-extra}/*', @@ -882,7 +900,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -1121,7 +1139,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ @@ -1195,7 +1213,7 @@ describe('environment variables', () => { } as any); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ @@ -1280,7 +1298,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); + await callBump({ release: 'main' }); }); expectLogsToMatch(logs, [ @@ -1329,9 +1347,7 @@ describe('environment variables', () => { }, }); - await expect( - bump({ pattern: null, release: 'main' } as unknown as Command), - ).rejects.toThrow(); + await expect(callBump({ release: 'main' })).rejects.toThrow(); }); it('should handle network errors when using custom base URL', async () => { @@ -1359,8 +1375,6 @@ describe('environment variables', () => { ), ); - await expect( - bump({ pattern: null, release: 'main' } as unknown as Command), - ).rejects.toThrow(); + await expect(callBump({ release: 'main' })).rejects.toThrow(); }); }); diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.ts b/packages/cli/src/modules/migrate/commands/versions/bump.ts index 8fe7013ac8..a20cf1494a 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.ts @@ -26,7 +26,7 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import { minimatch } from 'minimatch'; import semver from 'semver'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'node:path'; @@ -48,6 +48,7 @@ import { import { migrateMovedPackages } from './migrate'; import { runYarnInstall } from '../../lib/utils'; import { run } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; const DEP_TYPES = [ 'dependencies', @@ -73,12 +74,41 @@ function extendsDefaultPattern(pattern: string): boolean { return minimatch('@backstage/', pattern.slice(0, -1)); } -export default async (opts: OptionValues) => { +export default async ({ args, info }: CommandContext) => { + const { + flags: { pattern: patternFlag, release, skipInstall, skipMigrate }, + } = cli( + { + help: info, + flags: { + pattern: { + type: String, + description: 'Override glob for matching packages to upgrade', + }, + release: { + type: String, + description: 'Bump to a specific Backstage release line or version', + default: 'main', + }, + skipInstall: { + type: Boolean, + description: 'Skips yarn install step', + }, + skipMigrate: { + type: Boolean, + description: 'Skips migration of any moved packages', + }, + }, + }, + undefined, + args, + ); + const lockfilePath = targetPaths.resolveRoot('yarn.lock'); const lockfile = await Lockfile.load(lockfilePath); const yarnPluginEnabled = await hasBackstageYarnPlugin(); - let pattern = opts.pattern; + let pattern = patternFlag; if (!pattern) { console.log(`Using default pattern glob ${DEFAULT_PATTERN_GLOB}`); @@ -97,15 +127,15 @@ export default async (opts: OptionValues) => { findTargetVersion = createStrictVersionFinder({ releaseManifest, }); - } else if (semver.valid(opts.release)) { + } else if (semver.valid(release)) { // Specific release specified. Be strict when resolving versions - releaseManifest = await getManifestByVersion({ version: opts.release }); + releaseManifest = await getManifestByVersion({ version: release! }); findTargetVersion = createStrictVersionFinder({ releaseManifest, }); } else { // Release line specified. Be lenient when resolving versions. - if (opts.release === 'next') { + if (release === 'next') { const next = await getManifestByReleaseLine({ releaseLine: 'next', versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL, @@ -120,12 +150,12 @@ export default async (opts: OptionValues) => { : main; } else { releaseManifest = await getManifestByReleaseLine({ - releaseLine: opts.release, + releaseLine: release!, versionsBaseUrl: env.BACKSTAGE_VERSIONS_BASE_URL, }); } findTargetVersion = createVersionFinder({ - releaseLine: opts.releaseLine, + releaseLine: release, releaseManifest, }); } @@ -264,7 +294,7 @@ export default async (opts: OptionValues) => { ); } - if (!opts.skipInstall) { + if (!skipInstall) { await runYarnInstall(); } else { console.log(); @@ -272,14 +302,14 @@ export default async (opts: OptionValues) => { console.log(chalk.yellow(`Skipping yarn install`)); } - if (!opts.skipMigrate) { + if (!skipMigrate) { console.log(); const changed = await migrateMovedPackages({ - pattern: opts.pattern, + pattern: patternFlag, }); - if (changed && !opts.skipInstall) { + if (changed && !skipInstall) { await runYarnInstall(); } } diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts index 6748f8c1ff..fc50a5c491 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.test.ts @@ -123,7 +123,7 @@ describe('versions:migrate', () => { }); const { warn, log: logs } = await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', description: 'test' } }); }); expectLogsToMatch(logs, [ @@ -229,7 +229,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', description: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); @@ -311,7 +311,7 @@ describe('versions:migrate', () => { }); await withLogCollector(async () => { - await migrate({}); + await migrate({ args: [], info: { usage: 'test', description: 'test' } }); }); expect(runObj.run).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/modules/migrate/commands/versions/migrate.ts b/packages/cli/src/modules/migrate/commands/versions/migrate.ts index e5d4f1b2d3..4db800641a 100644 --- a/packages/cli/src/modules/migrate/commands/versions/migrate.ts +++ b/packages/cli/src/modules/migrate/commands/versions/migrate.ts @@ -16,11 +16,12 @@ import { BackstagePackageJson, PackageGraph } from '@backstage/cli-node'; import chalk from 'chalk'; import { resolve as resolvePath, join as joinPath } from 'node:path'; -import { OptionValues } from 'commander'; +import { cli } from 'cleye'; import { readJson, writeJson } from 'fs-extra'; import { minimatch } from 'minimatch'; import { runYarnInstall } from '../../lib/utils'; import replace from 'replace-in-file'; +import type { CommandContext } from '../../../../wiring/types'; declare module 'replace-in-file' { export default function (config: { @@ -38,10 +39,30 @@ declare module 'replace-in-file' { >; } -export default async (options: OptionValues) => { +export default async ({ args, info }: CommandContext) => { + const { + flags: { pattern, skipCodeChanges }, + } = cli( + { + help: info, + flags: { + pattern: { + type: String, + description: 'Override glob for matching packages to upgrade', + }, + skipCodeChanges: { + type: Boolean, + description: 'Skip code changes and only update package.json files', + }, + }, + }, + undefined, + args, + ); + const changed = await migrateMovedPackages({ - pattern: options.pattern, - skipCodeChanges: options.skipCodeChanges, + pattern, + skipCodeChanges, }); if (changed) { diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index 2dc4ed452d..c731479a51 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'migrate', @@ -24,45 +22,13 @@ export default createCliPlugin({ path: ['versions:migrate'], description: 'Migrate any plugins that have been moved to the @backstage-community namespace automatically', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .option( - '--pattern ', - 'Override glob for matching packages to upgrade', - ) - .option( - '--skip-code-changes', - 'Skip code changes and only update package.json files', - ) - .action(lazy(() => import('./commands/versions/migrate'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/versions/migrate') }, }); reg.addCommand({ path: ['versions:bump'], description: 'Bump Backstage packages to the latest versions', - execute: async ({ args }) => { - const command = new Command(); - - const defaultCommand = command - .option( - '--pattern ', - 'Override glob for matching packages to upgrade', - ) - .option( - '--release ', - 'Bump to a specific Backstage release line or version', - 'main', - ) - .option('--skip-install', 'Skips yarn install step') - .option('--skip-migrate', 'Skips migration of any moved packages') - .action(lazy(() => import('./commands/versions/bump'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/versions/bump') }, }); reg.addCommand({ diff --git a/packages/cli/src/modules/new/commands/new.test.ts b/packages/cli/src/modules/new/commands/new.test.ts index 2183d80062..0da42e7019 100644 --- a/packages/cli/src/modules/new/commands/new.test.ts +++ b/packages/cli/src/modules/new/commands/new.test.ts @@ -16,6 +16,7 @@ import { createNewPackage } from '../lib/createNewPackage'; import { default as newCommand } from './new'; +import type { CommandContext } from '../../../wiring/types'; jest.mock('../lib/createNewPackage'); @@ -34,13 +35,21 @@ describe.each([ }); it(`should generate naming options for --scope=${scope}`, async () => { - await newCommand({ scope, option: [], skipInstall: false }); + const args = ['--skip-install']; + if (scope) { + args.push('--scope', scope); + } + const context: CommandContext = { + args, + info: { usage: 'backstage-cli new', description: 'test' }, + }; + await newCommand(context); expect(createNewPackage).toHaveBeenCalledWith( expect.objectContaining({ - configOverrides: { + configOverrides: expect.objectContaining({ packageNamePrefix: prefix, packageNamePluginInfix: infix, - }, + }), }), ); }); diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index a38316dd62..e5c674c50c 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -14,28 +14,67 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; +import type { CommandContext } from '../../../wiring/types'; -type ArgOptions = { - option: string[]; - select?: string; - skipInstall: boolean; - private?: boolean; - npmRegistry?: string; - scope?: string; - license?: string; - baseVersion?: string; -}; - -export default async (opts: ArgOptions) => { +export default async ({ args, info }: CommandContext) => { const { - option: rawArgOptions, - select: preselectedTemplateId, - skipInstall, - scope, - private: isPrivate, - ...otherGlobals - } = opts; + flags: { + select, + option: rawArgOptions, + skipInstall, + scope, + npmRegistry, + baseVersion, + license, + private: isPrivate, + }, + } = cli( + { + help: info, + flags: { + select: { + type: String, + description: 'Select the thing you want to be creating upfront', + }, + option: { + type: [String], + description: 'Pre-fill options for the creation process', + default: [], + }, + skipInstall: { + type: Boolean, + description: `Skips running 'yarn install' and 'yarn lint --fix'`, + }, + scope: { + type: String, + description: 'The scope to use for new packages', + }, + npmRegistry: { + type: String, + description: 'The package registry to use for new packages', + }, + baseVersion: { + type: String, + description: + 'The version to use for any new packages (default: 0.1.0)', + }, + license: { + type: String, + description: + 'The license to use for any new packages (default: Apache-2.0)', + }, + private: { + type: Boolean, + description: 'Mark new packages as private', + default: true, + }, + }, + }, + undefined, + args, + ); const prefilledParams = parseParams(rawArgOptions); @@ -50,8 +89,8 @@ export default async (opts: ArgOptions) => { } if ( - isPrivate === false || // set to false with --no-private flag - Object.values(otherGlobals).filter(Boolean).length !== 0 + isPrivate === false || + [npmRegistry, baseVersion, license].filter(Boolean).length !== 0 ) { console.warn( `Global template configuration via CLI flags is deprecated, see https://backstage.io/docs/cli/new for information on how to configure package templating`, @@ -60,16 +99,16 @@ export default async (opts: ArgOptions) => { await createNewPackage({ prefilledParams, - preselectedTemplateId, + preselectedTemplateId: select, configOverrides: { - license: otherGlobals.license, - version: otherGlobals.baseVersion, + license, + version: baseVersion, private: isPrivate, - publishRegistry: otherGlobals.npmRegistry, + publishRegistry: npmRegistry, packageNamePrefix: packagePrefix, packageNamePluginInfix: pluginInfix, }, - skipInstall, + skipInstall: Boolean(skipInstall), }); }; diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index ac21b825fe..5b2a6562ea 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../wiring/lazy'; import { NotImplementedError } from '@backstage/errors'; export default createCliPlugin({ @@ -25,45 +23,7 @@ export default createCliPlugin({ path: ['new'], description: 'Open up an interactive guide to creating new things in your app', - execute: async ({ args }) => { - const command = new Command(); - const defaultCommand = command - .storeOptionsAsProperties(false) - .description( - 'Open up an interactive guide to creating new things in your app', - ) - .option( - '--select ', - 'Select the thing you want to be creating upfront', - ) - .option( - '--option =', - 'Pre-fill options for the creation process', - (opt, arr: string[]) => [...arr, opt], - [], - ) - .option( - '--skip-install', - `Skips running 'yarn install' and 'yarn lint --fix'`, - ) - .option('--scope ', 'The scope to use for new packages') - .option( - '--npm-registry ', - 'The package registry to use for new packages', - ) - .option( - '--baseVersion ', - 'The version to use for any new packages (default: 0.1.0)', - ) - .option( - '--license ', - 'The license to use for any new packages (default: Apache-2.0)', - ) - .option('--no-private', 'Do not mark new packages as private') - .action(lazy(() => import('./commands/new'), 'default')); - - await defaultCommand.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/new') }, }); reg.addCommand({ diff --git a/packages/cli/src/modules/test/commands/package/test.ts b/packages/cli/src/modules/test/commands/package/test.ts index 9c13019bb5..687b66dbfd 100644 --- a/packages/cli/src/modules/test/commands/package/test.ts +++ b/packages/cli/src/modules/test/commands/package/test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { Command, OptionValues } from 'commander'; - import { runCheck, findOwnPaths } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; function includesAnyOf(hayStack: string[], ...needles: string[]) { for (const needle of needles) { @@ -27,15 +26,7 @@ function includesAnyOf(hayStack: string[], ...needles: string[]) { return false; } -export default async (_opts: OptionValues, cmd: Command) => { - // all args are forwarded to jest - let parent = cmd; - while (parent.parent) { - parent = parent.parent; - } - const allArgs = parent.args as string[]; - const args = allArgs.slice(allArgs.indexOf('test') + 1); - +export default async ({ args }: CommandContext) => { // Only include our config if caller isn't passing their own config if (!includesAnyOf(args, '-c', '--config')) { /* eslint-disable-next-line no-restricted-syntax */ diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index bb614798b4..ececc5940c 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -16,12 +16,12 @@ import os from 'node:os'; import crypto from 'node:crypto'; +import { parseArgs } from 'node:util'; import yargs from 'yargs'; // 'jest-cli' is included with jest and should be kept in sync with the installed jest version // eslint-disable-next-line @backstage/no-undeclared-imports import { run as runJest, yargsOptions as jestYargsOptions } from 'jest-cli'; import { relative as relativePath } from 'node:path'; -import { Command, OptionValues } from 'commander'; import { Lockfile, PackageGraph, SuccessCache } from '@backstage/cli-node'; import { @@ -31,6 +31,7 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; +import type { CommandContext } from '../../../../wiring/types'; type JestProject = { displayName: string; @@ -148,18 +149,23 @@ function removeOptionArg(args: string[], option: string, size: number = 2) { } while (changed); } -export async function command(opts: OptionValues, cmd: Command): Promise { +export default async ({ args }: CommandContext) => { const testGlobal = global as TestGlobal; - // all args are forwarded to jest - let parent = cmd; - while (parent.parent) { - parent = parent.parent; - } - const allArgs = parent.args as string[]; - const args = allArgs.slice(allArgs.indexOf('test') + 1); + // Parse our own flags from the raw args using strict: false to allow Jest flags through + const { values: opts } = parseArgs({ + args, + strict: false, + options: { + since: { type: 'string' }, + successCache: { type: 'boolean' }, + successCacheDir: { type: 'string' }, + 'jest-help': { type: 'boolean' }, + }, + }); const hasFlags = createFlagFinder(args); + const sinceRef = typeof opts.since === 'string' ? opts.since : undefined; // Parse the args to ensure that no file filters are provided, in which case we refuse to run const { _: parsedArgs } = await yargs(args).options(jestYargsOptions).argv; @@ -177,7 +183,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { // Run in watch mode unless in CI, coverage mode, or running all tests let isSingleWatchMode = args.includes('--watch'); if ( - !opts.since && + !sinceRef && !process.env.CI && !hasFlags('--coverage', '--watch', '--watchAll') ) { @@ -261,10 +267,10 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } let selectedProjects: string[] | undefined = undefined; - if (opts.since && !hasFlags('--selectProjects')) { + if (sinceRef && !hasFlags('--selectProjects')) { const graph = await getPackageGraph(); const changedPackages = await graph.listChangedPackages({ - ref: opts.since, + ref: sinceRef, analyzeLockfile: true, }); @@ -304,7 +310,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { }--no-node-snapshot`; } - if (args.includes('--jest-help')) { + if (opts['jest-help']) { removeOptionArg(args, '--jest-help'); args.push('--help'); } @@ -332,7 +338,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise { const cache = SuccessCache.create({ name: 'test', - basePath: opts.successCacheDir, + basePath: opts.successCacheDir as string | undefined, }); const graph = await getPackageGraph(); @@ -439,4 +445,4 @@ export async function command(opts: OptionValues, cmd: Command): Promise { } await runJest(args); -} +}; diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index f003d9c054..627648332b 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -14,8 +14,6 @@ * limitations under the License. */ import { createCliPlugin } from '../../wiring/factory'; -import { Command } from 'commander'; -import { lazy } from '../../wiring/lazy'; export default createCliPlugin({ pluginId: 'test', @@ -24,43 +22,14 @@ export default createCliPlugin({ path: ['repo', 'test'], description: 'Run tests, forwarding args to Jest, defaulting to watch mode', - execute: async ({ args }) => { - const command = new Command(); - command.allowUnknownOption(true); - command.allowExcessArguments(true); - command.option( - '--since ', - 'Only test packages that changed since the specified ref', - ); - command.option('--successCache', 'Enable success caching'); - command.option( - '--successCacheDir ', - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - ); - command.option( - '--jest-help', - 'Show help for Jest CLI options, which are passed through', - ); - command.action(lazy(() => import('./commands/repo/test'), 'command')); - await command.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/repo/test') }, }); reg.addCommand({ path: ['package', 'test'], description: 'Run tests, forwarding args to Jest, defaulting to watch mode', - execute: async ({ args }) => { - const command = new Command(); - - command.allowUnknownOption(true); - command.allowExcessArguments(true); - command.helpOption('--backstage-cli-help'); - command.action( - lazy(() => import('./commands/package/test'), 'default'), - ); - await command.parseAsync(args, { from: 'user' }); - }, + execute: { loader: () => import('./commands/package/test') }, }); }, }); From eb73feac1539197bfe8a230fe841fc28b439cf27 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Feb 2026 11:08:57 +0100 Subject: [PATCH 02/13] Address review feedback and regenerate API reports - Handle quoted arguments in createScriptOptionsParser using a proper shell-like splitter instead of naive whitespace splitting - Pass CommandContext directly in bump tests instead of using a helper - Regenerate cli-report.md Signed-off-by: Patrik Oldsberg --- packages/cli/cli-report.md | 210 +++++++++++++----- .../src/modules/build/lib/optionsParser.ts | 36 ++- .../cli/src/modules/lint/lib/optionsParser.ts | 36 ++- .../migrate/commands/versions/bump.test.ts | 62 ++---- 4 files changed, 248 insertions(+), 96 deletions(-) diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 591b32ead7..74972b901a 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -33,10 +33,10 @@ Commands: ### `backstage-cli build-workspace` ``` -Usage: program [options] [packages...] +Usage: backstage-cli build-workspace Options: - --alwaysPack + --always-pack -h, --help ``` @@ -131,7 +131,7 @@ Options: ### `backstage-cli create-github-app` ``` -Usage: program [options] +Usage: backstage-cli create-github-app Options: -h, --help @@ -213,16 +213,16 @@ Options: ### `backstage-cli new` ``` -Usage: program [options] +Usage: backstage-cli new Options: - --baseVersion - --license - --no-private - --npm-registry - --option = - --scope - --select + --base-version + --license + --npm-registry + --option + --private + --scope + --select --skip-install -h, --help ``` @@ -249,13 +249,13 @@ Commands: ### `backstage-cli package build` ``` -Usage: program [options] +Usage: backstage-cli package build Options: - --config + --config --minify --module-federation - --role + --role --skip-build-dependencies --stats -h, --help @@ -273,13 +273,13 @@ Options: ### `backstage-cli package lint` ``` -Usage: program [options] [directories...] +Usage: backstage-cli package lint Options: --fix - --format - --max-warnings - --output-file + --format + --max-warnings + --output-file -h, --help ``` @@ -304,17 +304,17 @@ Options: ### `backstage-cli package start` ``` -Usage: program [options] +Usage: backstage-cli package start Options: --check - --config - --entrypoint - --inspect [host] - --inspect-brk [host] - --link - --require - --role + --config + --entrypoint + --inspect + --inspect-brk + --link + --require + --role -h, --help ``` @@ -453,12 +453,12 @@ Commands: ### `backstage-cli repo build` ``` -Usage: program [options] [command] +Usage: backstage-cli repo build Options: --all --minify - --since + --since -h, --help ``` @@ -474,7 +474,7 @@ Options: ### `backstage-cli repo fix` ``` -Usage: program [options] +Usage: backstage-cli repo fix Options: --check @@ -485,23 +485,23 @@ Options: ### `backstage-cli repo lint` ``` -Usage: program [options] [command] +Usage: backstage-cli repo lint Options: --fix - --format - --max-warnings - --output-file - --since - --successCache - --successCacheDir + --format + --max-warnings + --output-file + --since + --success-cache + --success-cache-dir -h, --help ``` ### `backstage-cli repo list-deprecations` ``` -Usage: program [options] +Usage: backstage-cli repo list-deprecations Options: --json @@ -511,29 +511,129 @@ Options: ### `backstage-cli repo start` ``` -Usage: program [options] [packageNameOrPath...] +Usage: backstage-cli repo start Options: - --config - --inspect [host] - --inspect-brk [host] - --link - --plugin - --require + --config + --inspect + --inspect-brk + --link + --plugin + --require -h, --help ``` ### `backstage-cli repo test` ``` -Usage: program [options] +Usage: Options: - --jest-help - --since - --successCache - --successCacheDir - -h, --help + --all + --automock + --cache + --cacheDirectory + --changedFilesWithAncestor + --changedSince + --ci + --clearCache + --clearMocks + --collectCoverage + --collectCoverageFrom + --color + --colors + --coverage + --coverageDirectory + --coveragePathIgnorePatterns + --coverageProvider + --coverageReporters + --coverageThreshold + --debug + --detectLeaks + --detectOpenHandles + --errorOnDeprecated + --filter + --findRelatedTests + --forceExit + --globalSetup + --globalTeardown + --globals + --haste + --help + --ignoreProjects + --injectGlobals + --json + --lastCommit + --listTests + --logHeapUsage + --maxConcurrency + --moduleDirectories + --moduleFileExtensions + --moduleNameMapper + --modulePathIgnorePatterns + --modulePaths + --noStackTrace + --notify + --notifyMode + --openHandlesTimeout + --outputFile + --passWithNoTests + --preset + --prettierPath + --projects + --randomize + --reporters + --resetMocks + --resetModules + --resolver + --restoreMocks + --rootDir + --roots + --runTestsByPath + --runner + --seed + --selectProjects + --setupFiles + --setupFilesAfterEnv + --shard + --showConfig + --showSeed + --silent + --skipFilter + --snapshotSerializers + --testEnvironment, --env + --testEnvironmentOptions + --testFailureExitCode + --testLocationInResults + --testMatch + --testPathIgnorePatterns + --testPathPatterns + --testRegex + --testResultsProcessor + --testRunner + --testSequencer + --testTimeout + --transform + --transformIgnorePatterns + --unmockedModulePathPatterns + --useStderr + --verbose + --version + --waitForUnhandledRejections + --watch + --watchAll + --watchPathIgnorePatterns + --watchman + --workerThreads + -b, --bail + -c, --config + -e, --expand + -f, --onlyFailures + -i, --runInBand + -o, --onlyChanged + -t, --testNamePattern + -u, --updateSnapshot + -w, --maxWorkers ``` ### `backstage-cli translations` @@ -575,11 +675,11 @@ Options: ### `backstage-cli versions:bump` ``` -Usage: program [options] +Usage: backstage-cli versions:bump Options: - --pattern - --release + --pattern + --release --skip-install --skip-migrate -h, --help @@ -588,10 +688,10 @@ Options: ### `backstage-cli versions:migrate` ``` -Usage: program [options] +Usage: backstage-cli versions:migrate Options: - --pattern + --pattern --skip-code-changes -h, --help ``` diff --git a/packages/cli/src/modules/build/lib/optionsParser.ts b/packages/cli/src/modules/build/lib/optionsParser.ts index 964825830a..ced48b4c91 100644 --- a/packages/cli/src/modules/build/lib/optionsParser.ts +++ b/packages/cli/src/modules/build/lib/optionsParser.ts @@ -15,6 +15,40 @@ */ import { parseArgs, type ParseArgsConfig } from 'node:util'; +// Splits a shell-like argument string, respecting single and double quotes +function splitShellArgs(str: string): string[] { + const args: string[] = []; + let current = ''; + let quote: string | undefined; + + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + + if (quote) { + if (ch === quote) { + quote = undefined; + } else { + current += ch; + } + } else if (ch === '"' || ch === "'") { + quote = ch; + } else if (/\s/.test(ch)) { + if (current) { + args.push(current); + current = ''; + } + } else { + current += ch; + } + } + + if (current) { + args.push(current); + } + + return args; +} + export function createScriptOptionsParser( commandPath: string[], options: ParseArgsConfig['options'], @@ -27,7 +61,7 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); - const args = argsStr ? argsStr.split(/\s+/) : []; + const args = argsStr ? splitShellArgs(argsStr) : []; const { values } = parseArgs({ args, strict: false, options }); return values; diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli/src/modules/lint/lib/optionsParser.ts index 964825830a..ced48b4c91 100644 --- a/packages/cli/src/modules/lint/lib/optionsParser.ts +++ b/packages/cli/src/modules/lint/lib/optionsParser.ts @@ -15,6 +15,40 @@ */ import { parseArgs, type ParseArgsConfig } from 'node:util'; +// Splits a shell-like argument string, respecting single and double quotes +function splitShellArgs(str: string): string[] { + const args: string[] = []; + let current = ''; + let quote: string | undefined; + + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + + if (quote) { + if (ch === quote) { + quote = undefined; + } else { + current += ch; + } + } else if (ch === '"' || ch === "'") { + quote = ch; + } else if (/\s/.test(ch)) { + if (current) { + args.push(current); + current = ''; + } + } else { + current += ch; + } + } + + if (current) { + args.push(current); + } + + return args; +} + export function createScriptOptionsParser( commandPath: string[], options: ParseArgsConfig['options'], @@ -27,7 +61,7 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); - const args = argsStr ? argsStr.split(/\s+/) : []; + const args = argsStr ? splitShellArgs(argsStr) : []; const { values } = parseArgs({ args, strict: false, options }); return values; diff --git a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts index 88e10e2607..f119a0bedb 100644 --- a/packages/cli/src/modules/migrate/commands/versions/bump.test.ts +++ b/packages/cli/src/modules/migrate/commands/versions/bump.test.ts @@ -17,7 +17,6 @@ import fs from 'fs-extra'; import * as runObj from '@backstage/cli-common'; import { overrideTargetPaths } from '@backstage/cli-common/testUtils'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; -import type { CommandContext } from '../../../../wiring/types'; import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; @@ -126,27 +125,7 @@ const expectLogsToMatch = ( expect(receivedLogs.filter(Boolean).sort()).toEqual(expected.sort()); }; -function callBump(flags: Record) { - const args: string[] = []; - for (const [key, value] of Object.entries(flags)) { - if (value === null || value === undefined) { - continue; - } - const flag = `--${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`; - if (typeof value === 'boolean') { - if (value) { - args.push(flag); - } - } else { - args.push(flag, value); - } - } - const context: CommandContext = { - args, - info: { usage: 'backstage-cli versions:bump', description: 'test' }, - }; - return bump(context); -} +const info = { usage: 'backstage-cli versions:bump', description: '' }; describe('bump', () => { const mockDir = createMockDirectory(); @@ -212,7 +191,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -305,7 +284,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main', skipInstall: true }); + await bump({ args: ['--release', 'main', '--skip-install'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -407,7 +386,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -511,7 +490,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -606,9 +585,9 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await expect(callBump({ release: '999.0.1' })).rejects.toThrow( - 'No release found for 999.0.1 version', - ); + await expect( + bump({ args: ['--release', '999.0.1'], info }), + ).rejects.toThrow('No release found for 999.0.1 version'); }); expect(logs.filter(Boolean)).toEqual([ 'Using default pattern glob @backstage/*', @@ -712,7 +691,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'next' }); + await bump({ args: ['--release', 'next'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -791,9 +770,14 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ - pattern: '@{backstage,backstage-extra}/*', - release: 'main', + await bump({ + args: [ + '--pattern', + '@{backstage,backstage-extra}/*', + '--release', + 'main', + ], + info, }); }); expectLogsToMatch(logs, [ @@ -900,7 +884,7 @@ describe('bump', () => { ), ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ 'Using default pattern glob @backstage/*', @@ -1139,7 +1123,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1213,7 +1197,7 @@ describe('environment variables', () => { } as any); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1298,7 +1282,7 @@ describe('environment variables', () => { ); const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await callBump({ release: 'main' }); + await bump({ args: ['--release', 'main'], info }); }); expectLogsToMatch(logs, [ @@ -1347,7 +1331,7 @@ describe('environment variables', () => { }, }); - await expect(callBump({ release: 'main' })).rejects.toThrow(); + await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow(); }); it('should handle network errors when using custom base URL', async () => { @@ -1375,6 +1359,6 @@ describe('environment variables', () => { ), ); - await expect(callBump({ release: 'main' })).rejects.toThrow(); + await expect(bump({ args: ['--release', 'main'], info })).rejects.toThrow(); }); }); From f01dbf301e342c91a1c0974d9d1f2ea92a68c60d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Feb 2026 11:46:33 +0100 Subject: [PATCH 03/13] Use shell-quote for script argument parsing Replace custom splitShellArgs with shell-quote's parse() for proper shell argument tokenization in createScriptOptionsParser. Signed-off-by: Patrik Oldsberg --- packages/cli/package.json | 2 + .../src/modules/build/lib/optionsParser.ts | 41 +++---------------- .../cli/src/modules/lint/lib/optionsParser.ts | 41 +++---------------- yarn.lock | 9 ++++ 4 files changed, 23 insertions(+), 70 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 17d2189beb..bf287758ab 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -131,6 +131,7 @@ "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", "semver": "^7.5.3", + "shell-quote": "^1.8.1", "style-loader": "^3.3.1", "sucrase": "^3.20.2", "swc-loader": "^0.2.3", @@ -177,6 +178,7 @@ "@types/recursive-readdir": "^2.2.0", "@types/rollup-plugin-peer-deps-external": "^2.2.0", "@types/rollup-plugin-postcss": "^3.1.4", + "@types/shell-quote": "^1.7.5", "@types/svgo": "^2.6.2", "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", diff --git a/packages/cli/src/modules/build/lib/optionsParser.ts b/packages/cli/src/modules/build/lib/optionsParser.ts index ced48b4c91..62c3b7ce71 100644 --- a/packages/cli/src/modules/build/lib/optionsParser.ts +++ b/packages/cli/src/modules/build/lib/optionsParser.ts @@ -14,40 +14,7 @@ * limitations under the License. */ import { parseArgs, type ParseArgsConfig } from 'node:util'; - -// Splits a shell-like argument string, respecting single and double quotes -function splitShellArgs(str: string): string[] { - const args: string[] = []; - let current = ''; - let quote: string | undefined; - - for (let i = 0; i < str.length; i++) { - const ch = str[i]; - - if (quote) { - if (ch === quote) { - quote = undefined; - } else { - current += ch; - } - } else if (ch === '"' || ch === "'") { - quote = ch; - } else if (/\s/.test(ch)) { - if (current) { - args.push(current); - current = ''; - } - } else { - current += ch; - } - } - - if (current) { - args.push(current); - } - - return args; -} +import { parse as parseShellArgs } from 'shell-quote'; export function createScriptOptionsParser( commandPath: string[], @@ -61,7 +28,11 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); - const args = argsStr ? splitShellArgs(argsStr) : []; + const args = argsStr + ? parseShellArgs(argsStr).filter( + (e): e is string => typeof e === 'string', + ) + : []; const { values } = parseArgs({ args, strict: false, options }); return values; diff --git a/packages/cli/src/modules/lint/lib/optionsParser.ts b/packages/cli/src/modules/lint/lib/optionsParser.ts index ced48b4c91..62c3b7ce71 100644 --- a/packages/cli/src/modules/lint/lib/optionsParser.ts +++ b/packages/cli/src/modules/lint/lib/optionsParser.ts @@ -14,40 +14,7 @@ * limitations under the License. */ import { parseArgs, type ParseArgsConfig } from 'node:util'; - -// Splits a shell-like argument string, respecting single and double quotes -function splitShellArgs(str: string): string[] { - const args: string[] = []; - let current = ''; - let quote: string | undefined; - - for (let i = 0; i < str.length; i++) { - const ch = str[i]; - - if (quote) { - if (ch === quote) { - quote = undefined; - } else { - current += ch; - } - } else if (ch === '"' || ch === "'") { - quote = ch; - } else if (/\s/.test(ch)) { - if (current) { - args.push(current); - current = ''; - } - } else { - current += ch; - } - } - - if (current) { - args.push(current); - } - - return args; -} +import { parse as parseShellArgs } from 'shell-quote'; export function createScriptOptionsParser( commandPath: string[], @@ -61,7 +28,11 @@ export function createScriptOptionsParser( } const argsStr = scriptStr.slice(expectedScript.length).trim(); - const args = argsStr ? splitShellArgs(argsStr) : []; + const args = argsStr + ? parseShellArgs(argsStr).filter( + (e): e is string => typeof e === 'string', + ) + : []; const { values } = parseArgs({ args, strict: false, options }); return values; diff --git a/yarn.lock b/yarn.lock index 3956198af0..984468f6c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3341,6 +3341,7 @@ __metadata: "@types/recursive-readdir": "npm:^2.2.0" "@types/rollup-plugin-peer-deps-external": "npm:^2.2.0" "@types/rollup-plugin-postcss": "npm:^3.1.4" + "@types/shell-quote": "npm:^1.7.5" "@types/svgo": "npm:^2.6.2" "@types/tar": "npm:^6.1.1" "@types/terser-webpack-plugin": "npm:^5.0.4" @@ -3411,6 +3412,7 @@ __metadata: rollup-plugin-postcss: "npm:^4.0.0" rollup-pluginutils: "npm:^2.8.2" semver: "npm:^7.5.3" + shell-quote: "npm:^1.8.1" style-loader: "npm:^3.3.1" sucrase: "npm:^3.20.2" swc-loader: "npm:^0.2.3" @@ -22660,6 +22662,13 @@ __metadata: languageName: node linkType: hard +"@types/shell-quote@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/shell-quote@npm:1.7.5" + checksum: 10/32b4d697c7d23dbadf40713692c47f1595f083a3b3deea76cb18e30a05d197aa9205d2b87f6d92edb60cda120b51e35d32bda96ed9b0a7e32921eed2deb4559e + languageName: node + linkType: hard + "@types/sinon@npm:^17.0.3": version: 17.0.3 resolution: "@types/sinon@npm:17.0.3" From 18d09fe581f0d21e91a64442b68e0f6e962bd6ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Feb 2026 13:00:30 +0100 Subject: [PATCH 04/13] Address Copilot review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle --inspect/--inspect-brk optional host value by extracting them from raw args before passing to cleye (supports both bare --inspect and --inspect=host:port forms) - Change require flag from [String] to String in package start since downstream only accepts a single value - Preserve legacy --alwaysYarnPack alias in build-workspace command - Update changeset to mention camelCase → kebab-case flag changes Signed-off-by: Patrik Oldsberg --- .changeset/migrate-cli-commands-to-cleye.md | 2 +- packages/cli/cli-report.md | 4 -- .../modules/build/commands/buildWorkspace.ts | 7 ++- .../build/commands/package/start/command.ts | 63 ++++++++++++------- .../src/modules/build/commands/repo/start.ts | 55 +++++++++++----- 5 files changed, 85 insertions(+), 46 deletions(-) diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md index 5231353e50..b057acfe0b 100644 --- a/.changeset/migrate-cli-commands-to-cleye.md +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. +Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. A few CLI flags that were previously camelCase have been normalized to kebab-case to match standard CLI conventions. Affected flags: `--baseVersion` → `--base-version`, `--successCache` → `--success-cache`, `--successCacheDir` → `--success-cache-dir`, `--alwaysPack` → `--always-pack`. diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 74972b901a..8a8296b574 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -310,8 +310,6 @@ Options: --check --config --entrypoint - --inspect - --inspect-brk --link --require --role @@ -515,8 +513,6 @@ Usage: backstage-cli repo start Options: --config - --inspect - --inspect-brk --link --plugin --require diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index 6c27396a30..1f9bdff45c 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -20,6 +20,11 @@ import { createDistWorkspace } from '../lib/packager'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { + // Support legacy --alwaysYarnPack alias + const normalizedArgs = args.map(a => + a === '--alwaysYarnPack' ? '--always-pack' : a, + ); + const { flags: { alwaysPack }, _: positionals, @@ -36,7 +41,7 @@ export default async ({ args, info }: CommandContext) => { }, }, undefined, - args, + normalizedArgs, ); const [dir, ...packages] = positionals; diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index ae85fec56e..f0454f2d3d 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -22,17 +22,11 @@ import { targetPaths } from '@backstage/cli-common'; import type { CommandContext } from '../../../../../wiring/types'; export default async ({ args, info }: CommandContext) => { + const { inspectEnabled, inspectBrkEnabled, filteredArgs } = + extractInspectFlags(args); + const { - flags: { - config, - role, - check, - inspect, - inspectBrk, - require: requirePaths, - link, - entrypoint, - }, + flags: { config, role, check, require: requirePath, link, entrypoint }, } = cli( { help: info, @@ -50,17 +44,8 @@ export default async ({ args, info }: CommandContext) => { type: Boolean, description: 'Enable type checking and linting if available', }, - inspect: { - type: String, - description: 'Enable debugger in Node.js environments', - }, - inspectBrk: { - type: String, - description: - 'Enable debugger in Node.js environments, breaking before code starts', - }, require: { - type: [String], + type: String, description: 'Add a --require argument to the node process', }, link: { @@ -75,7 +60,7 @@ export default async ({ args, info }: CommandContext) => { }, }, undefined, - args, + filteredArgs, ); await startPackage({ @@ -85,8 +70,38 @@ export default async ({ args, info }: CommandContext) => { configPaths: config, checksEnabled: Boolean(check), linkedWorkspace: await resolveLinkedWorkspace(link), - inspectEnabled: inspect, - inspectBrkEnabled: inspectBrk, - require: requirePaths?.[0], + inspectEnabled, + inspectBrkEnabled, + require: requirePath, }); }; + +// --inspect and --inspect-brk accept an optional host value, which cleye +// can't express (it only supports required or no value). We extract them +// from the raw args before passing the rest to cleye. +function extractInspectFlags(args: string[]) { + let inspectEnabled: boolean | string | undefined; + let inspectBrkEnabled: boolean | string | undefined; + const filteredArgs: string[] = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--inspect' || arg === '--inspect-brk') { + const next = args[i + 1]; + const value = next && !next.startsWith('-') ? args[++i] : true; + if (arg === '--inspect') { + inspectEnabled = value; + } else { + inspectBrkEnabled = value; + } + } else if (arg.startsWith('--inspect=')) { + inspectEnabled = arg.slice('--inspect='.length); + } else if (arg.startsWith('--inspect-brk=')) { + inspectBrkEnabled = arg.slice('--inspect-brk='.length); + } else { + filteredArgs.push(arg); + } + } + + return { inspectEnabled, inspectBrkEnabled, filteredArgs }; +} diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index 5cb5e591fb..dd10197c4c 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -36,8 +36,11 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ ]; export default async ({ args, info }: CommandContext) => { + const { inspectEnabled, inspectBrkEnabled, filteredArgs } = + extractInspectFlags(args); + const { - flags: { plugin, config, inspect, inspectBrk, require: requirePath, link }, + flags: { plugin, config, require: requirePath, link }, _: namesOrPaths, } = cli( { @@ -54,16 +57,6 @@ export default async ({ args, info }: CommandContext) => { description: 'Config files to load instead of app-config.yaml', default: [], }, - inspect: { - type: String, - description: - 'Enable debugger in Node.js environments. Applies to backend package only', - }, - inspectBrk: { - type: String, - description: - 'Enable debugger in Node.js environments, breaking before code starts. Applies to backend package only', - }, require: { type: String, description: @@ -76,7 +69,7 @@ export default async ({ args, info }: CommandContext) => { }, }, undefined, - args, + filteredArgs, ); const targetPackages = await findTargetPackages(namesOrPaths, plugin); @@ -84,8 +77,8 @@ export default async ({ args, info }: CommandContext) => { const packageOptions = await resolvePackageOptions(targetPackages, { plugin, config, - inspect, - inspectBrk, + inspect: inspectEnabled, + inspectBrk: inspectBrkEnabled, require: requirePath, link, }); @@ -211,8 +204,8 @@ export async function findTargetPackages( type CommandOptions = { plugin: string[]; config: string[]; - inspect?: string; - inspectBrk?: string; + inspect?: boolean | string; + inspectBrk?: boolean | string; require?: string; link?: string; }; @@ -270,3 +263,33 @@ async function resolvePackageOptions( ]; }); } + +// --inspect and --inspect-brk accept an optional host value, which cleye +// can't express (it only supports required or no value). We extract them +// from the raw args before passing the rest to cleye. +function extractInspectFlags(args: string[]) { + let inspectEnabled: boolean | string | undefined; + let inspectBrkEnabled: boolean | string | undefined; + const filteredArgs: string[] = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--inspect' || arg === '--inspect-brk') { + const next = args[i + 1]; + const value = next && !next.startsWith('-') ? args[++i] : true; + if (arg === '--inspect') { + inspectEnabled = value; + } else { + inspectBrkEnabled = value; + } + } else if (arg.startsWith('--inspect=')) { + inspectEnabled = arg.slice('--inspect='.length); + } else if (arg.startsWith('--inspect-brk=')) { + inspectBrkEnabled = arg.slice('--inspect-brk='.length); + } else { + filteredArgs.push(arg); + } + } + + return { inspectEnabled, inspectBrkEnabled, filteredArgs }; +} From 3fc996fd27b41b9cf4bf922071bff79cf84f7037 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Feb 2026 14:05:49 +0100 Subject: [PATCH 05/13] Mark cleye migration as a breaking change (minor bump) Signed-off-by: Patrik Oldsberg --- .changeset/migrate-cli-commands-to-cleye.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md index b057acfe0b..9d52fb2260 100644 --- a/.changeset/migrate-cli-commands-to-cleye.md +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -1,5 +1,13 @@ --- -'@backstage/cli': patch +'@backstage/cli': minor --- -Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. A few CLI flags that were previously camelCase have been normalized to kebab-case to match standard CLI conventions. Affected flags: `--baseVersion` → `--base-version`, `--successCache` → `--success-cache`, `--successCacheDir` → `--success-cache-dir`, `--alwaysPack` → `--always-pack`. +**BREAKING**: Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. The following CLI flags have been renamed from camelCase to kebab-case to match standard CLI conventions: + +- `--baseVersion` → `--base-version` +- `--successCache` → `--success-cache` +- `--successCacheDir` → `--success-cache-dir` +- `--alwaysPack` → `--always-pack` +- `--alwaysYarnPack` → `--always-pack` (hidden legacy alias preserved) + +If you have scripts or CI configurations that use any of the above flags, update them to the new kebab-case spelling. From 2946e8e4c69924b81a12d97487ec18d83c434910 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 1 Mar 2026 15:02:02 +0100 Subject: [PATCH 06/13] Remove custom inspect flag handling, switch repo test to cleye - Use cleye's `type: String` for --inspect/--inspect-brk instead of custom extractInspectFlags pre-processing in both package start and repo start commands. - Switch repo test from node:util parseArgs to cleye so that --help shows Backstage-specific flags rather than dumping Jest's full help. - Fix create-github-app help output to include positional. - Update downstream inspect types from `boolean | string` to `string`. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/cli-report.md | 118 ++---------------- .../build/commands/package/start/command.ts | 60 ++++----- .../commands/package/start/startBackend.ts | 4 +- .../commands/package/start/startPackage.ts | 4 +- .../src/modules/build/commands/repo/start.ts | 55 +++----- .../build/lib/runner/runBackend.test.ts | 2 +- .../modules/build/lib/runner/runBackend.ts | 24 ++-- .../commands/create-github-app/index.ts | 2 +- .../src/modules/test/commands/repo/test.ts | 74 +++++------ 9 files changed, 101 insertions(+), 242 deletions(-) diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 8a8296b574..f8d1db7019 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -131,7 +131,7 @@ Options: ### `backstage-cli create-github-app` ``` -Usage: backstage-cli create-github-app +Usage: backstage-cli create-github-app Options: -h, --help @@ -310,6 +310,8 @@ Options: --check --config --entrypoint + --inspect + --inspect-brk --link --require --role @@ -513,6 +515,8 @@ Usage: backstage-cli repo start Options: --config + --inspect + --inspect-brk --link --plugin --require @@ -522,114 +526,14 @@ Options: ### `backstage-cli repo test` ``` -Usage: +Usage: backstage-cli repo test Options: - --all - --automock - --cache - --cacheDirectory - --changedFilesWithAncestor - --changedSince - --ci - --clearCache - --clearMocks - --collectCoverage - --collectCoverageFrom - --color - --colors - --coverage - --coverageDirectory - --coveragePathIgnorePatterns - --coverageProvider - --coverageReporters - --coverageThreshold - --debug - --detectLeaks - --detectOpenHandles - --errorOnDeprecated - --filter - --findRelatedTests - --forceExit - --globalSetup - --globalTeardown - --globals - --haste - --help - --ignoreProjects - --injectGlobals - --json - --lastCommit - --listTests - --logHeapUsage - --maxConcurrency - --moduleDirectories - --moduleFileExtensions - --moduleNameMapper - --modulePathIgnorePatterns - --modulePaths - --noStackTrace - --notify - --notifyMode - --openHandlesTimeout - --outputFile - --passWithNoTests - --preset - --prettierPath - --projects - --randomize - --reporters - --resetMocks - --resetModules - --resolver - --restoreMocks - --rootDir - --roots - --runTestsByPath - --runner - --seed - --selectProjects - --setupFiles - --setupFilesAfterEnv - --shard - --showConfig - --showSeed - --silent - --skipFilter - --snapshotSerializers - --testEnvironment, --env - --testEnvironmentOptions - --testFailureExitCode - --testLocationInResults - --testMatch - --testPathIgnorePatterns - --testPathPatterns - --testRegex - --testResultsProcessor - --testRunner - --testSequencer - --testTimeout - --transform - --transformIgnorePatterns - --unmockedModulePathPatterns - --useStderr - --verbose - --version - --waitForUnhandledRejections - --watch - --watchAll - --watchPathIgnorePatterns - --watchman - --workerThreads - -b, --bail - -c, --config - -e, --expand - -f, --onlyFailures - -i, --runInBand - -o, --onlyChanged - -t, --testNamePattern - -u, --updateSnapshot - -w, --maxWorkers + --jest-help + --since + --success-cache + --success-cache-dir + -h, --help ``` ### `backstage-cli translations` diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index f0454f2d3d..0d169af5ef 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -22,11 +22,17 @@ import { targetPaths } from '@backstage/cli-common'; import type { CommandContext } from '../../../../../wiring/types'; export default async ({ args, info }: CommandContext) => { - const { inspectEnabled, inspectBrkEnabled, filteredArgs } = - extractInspectFlags(args); - const { - flags: { config, role, check, require: requirePath, link, entrypoint }, + flags: { + config, + role, + check, + require: requirePath, + link, + entrypoint, + inspect, + inspectBrk, + }, } = cli( { help: info, @@ -57,10 +63,20 @@ export default async ({ args, info }: CommandContext) => { description: 'The entrypoint to start from, relative to the package root. Can point to either a file (without extension) or a directory (in which case the index file in that directory is used). Defaults to "dev"', }, + inspect: { + type: String, + description: + 'Enable the Node.js inspector, optionally at a specific host:port', + }, + inspectBrk: { + type: String, + description: + 'Enable the Node.js inspector and break before user code starts', + }, }, }, undefined, - filteredArgs, + args, ); await startPackage({ @@ -70,38 +86,8 @@ export default async ({ args, info }: CommandContext) => { configPaths: config, checksEnabled: Boolean(check), linkedWorkspace: await resolveLinkedWorkspace(link), - inspectEnabled, - inspectBrkEnabled, + inspectEnabled: inspect, + inspectBrkEnabled: inspectBrk, require: requirePath, }); }; - -// --inspect and --inspect-brk accept an optional host value, which cleye -// can't express (it only supports required or no value). We extract them -// from the raw args before passing the rest to cleye. -function extractInspectFlags(args: string[]) { - let inspectEnabled: boolean | string | undefined; - let inspectBrkEnabled: boolean | string | undefined; - const filteredArgs: string[] = []; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === '--inspect' || arg === '--inspect-brk') { - const next = args[i + 1]; - const value = next && !next.startsWith('-') ? args[++i] : true; - if (arg === '--inspect') { - inspectEnabled = value; - } else { - inspectBrkEnabled = value; - } - } else if (arg.startsWith('--inspect=')) { - inspectEnabled = arg.slice('--inspect='.length); - } else if (arg.startsWith('--inspect-brk=')) { - inspectBrkEnabled = arg.slice('--inspect-brk='.length); - } else { - filteredArgs.push(arg); - } - } - - return { inspectEnabled, inspectBrkEnabled, filteredArgs }; -} diff --git a/packages/cli/src/modules/build/commands/package/start/startBackend.ts b/packages/cli/src/modules/build/commands/package/start/startBackend.ts index a36a93b8ff..b9eb8709f3 100644 --- a/packages/cli/src/modules/build/commands/package/start/startBackend.ts +++ b/packages/cli/src/modules/build/commands/package/start/startBackend.ts @@ -23,8 +23,8 @@ import { runBackend } from '../../../lib/runner'; interface StartBackendOptions { targetDir: string; checksEnabled: boolean; - inspectEnabled?: boolean | string; - inspectBrkEnabled?: boolean | string; + inspectEnabled?: string; + inspectBrkEnabled?: string; linkedWorkspace?: string; require?: string; } diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index e0ab13facc..084a4670b4 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -38,8 +38,8 @@ export async function startPackage(options: { targetDir: string; configPaths: string[]; checksEnabled: boolean; - inspectEnabled?: boolean | string; - inspectBrkEnabled?: boolean | string; + inspectEnabled?: string; + inspectBrkEnabled?: string; linkedWorkspace?: string; require?: string; }): Promise { diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index dd10197c4c..f2f6b7c412 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -36,11 +36,8 @@ const ACCEPTED_PACKAGE_ROLES: Array = [ ]; export default async ({ args, info }: CommandContext) => { - const { inspectEnabled, inspectBrkEnabled, filteredArgs } = - extractInspectFlags(args); - const { - flags: { plugin, config, require: requirePath, link }, + flags: { plugin, config, require: requirePath, link, inspect, inspectBrk }, _: namesOrPaths, } = cli( { @@ -66,10 +63,20 @@ export default async ({ args, info }: CommandContext) => { type: String, description: 'Link an external workspace for module resolution', }, + inspect: { + type: String, + description: + 'Enable the Node.js inspector, optionally at a specific host:port', + }, + inspectBrk: { + type: String, + description: + 'Enable the Node.js inspector and break before user code starts', + }, }, }, undefined, - filteredArgs, + args, ); const targetPackages = await findTargetPackages(namesOrPaths, plugin); @@ -77,8 +84,8 @@ export default async ({ args, info }: CommandContext) => { const packageOptions = await resolvePackageOptions(targetPackages, { plugin, config, - inspect: inspectEnabled, - inspectBrk: inspectBrkEnabled, + inspect, + inspectBrk, require: requirePath, link, }); @@ -204,8 +211,8 @@ export async function findTargetPackages( type CommandOptions = { plugin: string[]; config: string[]; - inspect?: boolean | string; - inspectBrk?: boolean | string; + inspect?: string; + inspectBrk?: string; require?: string; link?: string; }; @@ -263,33 +270,3 @@ async function resolvePackageOptions( ]; }); } - -// --inspect and --inspect-brk accept an optional host value, which cleye -// can't express (it only supports required or no value). We extract them -// from the raw args before passing the rest to cleye. -function extractInspectFlags(args: string[]) { - let inspectEnabled: boolean | string | undefined; - let inspectBrkEnabled: boolean | string | undefined; - const filteredArgs: string[] = []; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (arg === '--inspect' || arg === '--inspect-brk') { - const next = args[i + 1]; - const value = next && !next.startsWith('-') ? args[++i] : true; - if (arg === '--inspect') { - inspectEnabled = value; - } else { - inspectBrkEnabled = value; - } - } else if (arg.startsWith('--inspect=')) { - inspectEnabled = arg.slice('--inspect='.length); - } else if (arg.startsWith('--inspect-brk=')) { - inspectBrkEnabled = arg.slice('--inspect-brk='.length); - } else { - filteredArgs.push(arg); - } - } - - return { inspectEnabled, inspectBrkEnabled, filteredArgs }; -} diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts index 2b97ae352c..5fc3f448c9 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts @@ -163,7 +163,7 @@ describe('runBackend', () => { runBackend({ entry: 'src/index', - inspectEnabled: true, + inspectEnabled: '', }); // Fast-forward past the debounce delay (100ms) diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli/src/modules/build/lib/runner/runBackend.ts index 22fd50b3f4..adaa9ca58f 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.ts @@ -38,9 +38,9 @@ export type RunBackendOptions = { /** relative entry point path without extension, e.g. 'src/index' */ entry: string; /** Whether to forward the --inspect flag to the node process */ - inspectEnabled?: boolean | string; + inspectEnabled?: string; /** Whether to forward the --inspect-brk flag to the node process */ - inspectBrkEnabled?: boolean | string; + inspectBrkEnabled?: string; /** Additional module to require via the --require flag to the node process */ require?: string | string[]; /** An external linked workspace to override module resolution towards */ @@ -96,18 +96,18 @@ export async function runBackend(options: RunBackendOptions) { } const optionArgs = new Array(); - if (options.inspectEnabled) { - const inspect = - typeof options.inspectEnabled === 'string' + if (options.inspectEnabled !== undefined) { + optionArgs.push( + options.inspectEnabled ? `--inspect=${options.inspectEnabled}` - : '--inspect'; - optionArgs.push(inspect); - } else if (options.inspectBrkEnabled) { - const inspect = - typeof options.inspectBrkEnabled === 'string' + : '--inspect', + ); + } else if (options.inspectBrkEnabled !== undefined) { + optionArgs.push( + options.inspectBrkEnabled ? `--inspect-brk=${options.inspectBrkEnabled}` - : '--inspect-brk'; - optionArgs.push(inspect); + : '--inspect-brk', + ); } if (options.require) { const requires = [options.require].flat(); diff --git a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts index 998f8eb7ae..1cd51e541b 100644 --- a/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/commands/create-github-app/index.ts @@ -31,7 +31,7 @@ import type { CommandContext } from '../../../../wiring/types'; export default async ({ args, info }: CommandContext) => { const { _: positionals } = cli( { - help: info, + help: { ...info, usage: `${info.usage} ` }, parameters: [''], }, undefined, diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index ececc5940c..743aa87c3d 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -16,7 +16,7 @@ import os from 'node:os'; import crypto from 'node:crypto'; -import { parseArgs } from 'node:util'; +import { cli } from 'cleye'; import yargs from 'yargs'; // 'jest-cli' is included with jest and should be kept in sync with the installed jest version // eslint-disable-next-line @backstage/no-undeclared-imports @@ -131,41 +131,41 @@ export function createFlagFinder(args: string[]) { }; } -function removeOptionArg(args: string[], option: string, size: number = 2) { - let changed = false; - do { - changed = false; - - const index = args.indexOf(option); - if (index >= 0) { - changed = true; - args.splice(index, size); - } - const indexEq = args.findIndex(arg => arg.startsWith(`${option}=`)); - if (indexEq >= 0) { - changed = true; - args.splice(indexEq, 1); - } - } while (changed); -} - -export default async ({ args }: CommandContext) => { +export default async ({ args, info }: CommandContext) => { const testGlobal = global as TestGlobal; - // Parse our own flags from the raw args using strict: false to allow Jest flags through - const { values: opts } = parseArgs({ - args, - strict: false, - options: { - since: { type: 'string' }, - successCache: { type: 'boolean' }, - successCacheDir: { type: 'string' }, - 'jest-help': { type: 'boolean' }, + // Parse Backstage-specific flags; unknown flags and arguments are left in + // args so they can be forwarded to Jest. + const { flags: opts } = cli( + { + help: info, + flags: { + since: { + type: String, + description: + 'Only include test packages changed since the specified ref', + }, + successCache: { + type: Boolean, + description: 'Cache and skip tests for unchanged packages', + }, + successCacheDir: { + type: String, + description: 'Directory for the success cache', + }, + jestHelp: { + type: Boolean, + description: "Show Jest's own help output", + }, + }, + ignoreArgv: type => type === 'unknown-flag' || type === 'argument', }, - }); + undefined, + args, + ); const hasFlags = createFlagFinder(args); - const sinceRef = typeof opts.since === 'string' ? opts.since : undefined; + const sinceRef = opts.since || undefined; // Parse the args to ensure that no file filters are provided, in which case we refuse to run const { _: parsedArgs } = await yargs(args).options(jestYargsOptions).argv; @@ -252,10 +252,6 @@ export default async ({ args }: CommandContext) => { args.push('--maxWorkers=2'); } - if (opts.since) { - removeOptionArg(args, '--since'); - } - let packageGraph: PackageGraph | undefined; async function getPackageGraph() { if (packageGraph) { @@ -310,17 +306,13 @@ export default async ({ args }: CommandContext) => { }--no-node-snapshot`; } - if (opts['jest-help']) { - removeOptionArg(args, '--jest-help'); + if (opts.jestHelp) { args.push('--help'); } // This code path is enabled by the --successCache flag, which is specific to // the `repo test` command in the Backstage CLI. if (opts.successCache) { - removeOptionArg(args, '--successCache', 1); - removeOptionArg(args, '--successCacheDir'); - // Refuse to run if file filters are provided if (parsedArgs.length > 0) { throw new Error( @@ -338,7 +330,7 @@ export default async ({ args }: CommandContext) => { const cache = SuccessCache.create({ name: 'test', - basePath: opts.successCacheDir as string | undefined, + basePath: opts.successCacheDir, }); const graph = await getPackageGraph(); From b45ee8010490754f594d36274b0169edc48860db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 1 Mar 2026 15:19:09 +0100 Subject: [PATCH 07/13] Keep boolean | string for inspect types, map in command handlers Move the empty-string-to-boolean conversion from cleye's String type into the command handlers so that startPackage/runBackend keep their existing boolean | string interface. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../build/commands/package/start/command.ts | 4 ++-- .../commands/package/start/startBackend.ts | 4 ++-- .../commands/package/start/startPackage.ts | 4 ++-- .../src/modules/build/commands/repo/start.ts | 8 +++---- .../build/lib/runner/runBackend.test.ts | 2 +- .../modules/build/lib/runner/runBackend.ts | 24 +++++++++---------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/modules/build/commands/package/start/command.ts b/packages/cli/src/modules/build/commands/package/start/command.ts index 0d169af5ef..143385e4a6 100644 --- a/packages/cli/src/modules/build/commands/package/start/command.ts +++ b/packages/cli/src/modules/build/commands/package/start/command.ts @@ -86,8 +86,8 @@ export default async ({ args, info }: CommandContext) => { configPaths: config, checksEnabled: Boolean(check), linkedWorkspace: await resolveLinkedWorkspace(link), - inspectEnabled: inspect, - inspectBrkEnabled: inspectBrk, + inspectEnabled: inspect || (inspect === '' ? true : undefined), + inspectBrkEnabled: inspectBrk || (inspectBrk === '' ? true : undefined), require: requirePath, }); }; diff --git a/packages/cli/src/modules/build/commands/package/start/startBackend.ts b/packages/cli/src/modules/build/commands/package/start/startBackend.ts index b9eb8709f3..a36a93b8ff 100644 --- a/packages/cli/src/modules/build/commands/package/start/startBackend.ts +++ b/packages/cli/src/modules/build/commands/package/start/startBackend.ts @@ -23,8 +23,8 @@ import { runBackend } from '../../../lib/runner'; interface StartBackendOptions { targetDir: string; checksEnabled: boolean; - inspectEnabled?: string; - inspectBrkEnabled?: string; + inspectEnabled?: boolean | string; + inspectBrkEnabled?: boolean | string; linkedWorkspace?: string; require?: string; } diff --git a/packages/cli/src/modules/build/commands/package/start/startPackage.ts b/packages/cli/src/modules/build/commands/package/start/startPackage.ts index 084a4670b4..e0ab13facc 100644 --- a/packages/cli/src/modules/build/commands/package/start/startPackage.ts +++ b/packages/cli/src/modules/build/commands/package/start/startPackage.ts @@ -38,8 +38,8 @@ export async function startPackage(options: { targetDir: string; configPaths: string[]; checksEnabled: boolean; - inspectEnabled?: string; - inspectBrkEnabled?: string; + inspectEnabled?: boolean | string; + inspectBrkEnabled?: boolean | string; linkedWorkspace?: string; require?: string; }): Promise { diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index f2f6b7c412..8d36d0732b 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -84,8 +84,8 @@ export default async ({ args, info }: CommandContext) => { const packageOptions = await resolvePackageOptions(targetPackages, { plugin, config, - inspect, - inspectBrk, + inspect: inspect || (inspect === '' ? true : undefined), + inspectBrk: inspectBrk || (inspectBrk === '' ? true : undefined), require: requirePath, link, }); @@ -211,8 +211,8 @@ export async function findTargetPackages( type CommandOptions = { plugin: string[]; config: string[]; - inspect?: string; - inspectBrk?: string; + inspect?: boolean | string; + inspectBrk?: boolean | string; require?: string; link?: string; }; diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts index 5fc3f448c9..2b97ae352c 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.test.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.test.ts @@ -163,7 +163,7 @@ describe('runBackend', () => { runBackend({ entry: 'src/index', - inspectEnabled: '', + inspectEnabled: true, }); // Fast-forward past the debounce delay (100ms) diff --git a/packages/cli/src/modules/build/lib/runner/runBackend.ts b/packages/cli/src/modules/build/lib/runner/runBackend.ts index adaa9ca58f..22fd50b3f4 100644 --- a/packages/cli/src/modules/build/lib/runner/runBackend.ts +++ b/packages/cli/src/modules/build/lib/runner/runBackend.ts @@ -38,9 +38,9 @@ export type RunBackendOptions = { /** relative entry point path without extension, e.g. 'src/index' */ entry: string; /** Whether to forward the --inspect flag to the node process */ - inspectEnabled?: string; + inspectEnabled?: boolean | string; /** Whether to forward the --inspect-brk flag to the node process */ - inspectBrkEnabled?: string; + inspectBrkEnabled?: boolean | string; /** Additional module to require via the --require flag to the node process */ require?: string | string[]; /** An external linked workspace to override module resolution towards */ @@ -96,18 +96,18 @@ export async function runBackend(options: RunBackendOptions) { } const optionArgs = new Array(); - if (options.inspectEnabled !== undefined) { - optionArgs.push( - options.inspectEnabled + if (options.inspectEnabled) { + const inspect = + typeof options.inspectEnabled === 'string' ? `--inspect=${options.inspectEnabled}` - : '--inspect', - ); - } else if (options.inspectBrkEnabled !== undefined) { - optionArgs.push( - options.inspectBrkEnabled + : '--inspect'; + optionArgs.push(inspect); + } else if (options.inspectBrkEnabled) { + const inspect = + typeof options.inspectBrkEnabled === 'string' ? `--inspect-brk=${options.inspectBrkEnabled}` - : '--inspect-brk', - ); + : '--inspect-brk'; + optionArgs.push(inspect); } if (options.require) { const requires = [options.require].flat(); From 629e9f555847a58c72acd44dd5b690b04b43add5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 1 Mar 2026 16:54:31 +0100 Subject: [PATCH 08/13] Add deprecation warnings for old camelCase flag names Instead of a hard breaking change, the old camelCase flag names (--baseVersion, --successCache, --successCacheDir, --alwaysPack) still work via type-flag's built-in camelCase/kebab-case mapping, but now print a deprecation warning pointing to the new kebab-case spelling. Downgrades the changeset from minor (breaking) to patch. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/migrate-cli-commands-to-cleye.md | 6 +- packages/cli/src/lib/warnDeprecatedFlags.ts | 38 ++++++++ .../modules/build/commands/buildWorkspace.ts | 25 +++--- .../src/modules/lint/commands/repo/lint.ts | 81 +++++++++-------- packages/cli/src/modules/new/commands/new.ts | 87 +++++++++---------- .../src/modules/test/commands/repo/test.ts | 42 +++++---- 6 files changed, 160 insertions(+), 119 deletions(-) create mode 100644 packages/cli/src/lib/warnDeprecatedFlags.ts diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md index 9d52fb2260..34b595e1a5 100644 --- a/.changeset/migrate-cli-commands-to-cleye.md +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -1,8 +1,8 @@ --- -'@backstage/cli': minor +'@backstage/cli': patch --- -**BREAKING**: Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. The following CLI flags have been renamed from camelCase to kebab-case to match standard CLI conventions: +Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. The following CLI flags have been renamed from camelCase to kebab-case to match standard CLI conventions: - `--baseVersion` → `--base-version` - `--successCache` → `--success-cache` @@ -10,4 +10,4 @@ - `--alwaysPack` → `--always-pack` - `--alwaysYarnPack` → `--always-pack` (hidden legacy alias preserved) -If you have scripts or CI configurations that use any of the above flags, update them to the new kebab-case spelling. +The old camelCase flag names still work but will print a deprecation warning. Please update any scripts or CI configurations to use the new kebab-case spelling. diff --git a/packages/cli/src/lib/warnDeprecatedFlags.ts b/packages/cli/src/lib/warnDeprecatedFlags.ts new file mode 100644 index 0000000000..5b7b82794f --- /dev/null +++ b/packages/cli/src/lib/warnDeprecatedFlags.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2026 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Scans args for deprecated camelCase flag names and logs a warning for each + * match. Since type-flag accepts both camelCase and kebab-case, the old names + * still work — this just nudges users toward the new spelling. + */ +export function warnDeprecatedFlags( + args: string[], + flags: Record, +) { + for (const key of Object.keys(flags)) { + const kebab = key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`); + if (kebab === key) { + continue; + } + const deprecated = `--${key}`; + if (args.some(a => a === deprecated || a.startsWith(`${deprecated}=`))) { + process.stderr.write( + `DEPRECATION WARNING: ${deprecated} has been renamed to --${kebab}\n`, + ); + } + } +} diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index 1f9bdff45c..ccb8267503 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -17,28 +17,33 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; +import { warnDeprecatedFlags } from '../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { - // Support legacy --alwaysYarnPack alias + // Support legacy --alwaysYarnPack and --alwaysPack aliases const normalizedArgs = args.map(a => - a === '--alwaysYarnPack' ? '--always-pack' : a, + a === '--alwaysYarnPack' || a === '--alwaysPack' ? '--always-pack' : a, ); + const flagDefs = { + alwaysPack: { + type: Boolean, + description: + 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', + }, + }; + + warnDeprecatedFlags(args, flagDefs); + const { flags: { alwaysPack }, _: positionals, } = cli( { - help: info, + help: { ...info, usage: `${info.usage} [packages...]` }, parameters: ['', '[packages...]'], - flags: { - alwaysPack: { - type: Boolean, - description: - 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', - }, - }, + flags: flagDefs, }, undefined, normalizedArgs, diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 513c76e0a5..80df175315 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -29,6 +29,7 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; +import { warnDeprecatedFlags } from '../../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../../wiring/types'; function depCount(pkg: BackstagePackageJson) { @@ -40,6 +41,43 @@ function depCount(pkg: BackstagePackageJson) { } export default async ({ args, info }: CommandContext) => { + const flagDefs = { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + successCache: { + type: Boolean, + description: + 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', + }, + successCacheDir: { + type: String, + description: + 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', + }, + since: { + type: String, + description: 'Only lint packages that changed since the specified ref', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }; + + warnDeprecatedFlags(args, flagDefs); + const { flags: { fix, @@ -50,48 +88,7 @@ export default async ({ args, info }: CommandContext) => { since, maxWarnings, }, - } = cli( - { - help: info, - flags: { - fix: { - type: Boolean, - description: 'Attempt to automatically fix violations', - }, - format: { - type: String, - description: 'Lint report output format', - default: 'eslint-formatter-friendly', - }, - outputFile: { - type: String, - description: 'Write the lint report to a file instead of stdout', - }, - successCache: { - type: Boolean, - description: - 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', - }, - successCacheDir: { - type: String, - description: - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - }, - since: { - type: String, - description: - 'Only lint packages that changed since the specified ref', - }, - maxWarnings: { - type: String, - description: - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - }, - }, - }, - undefined, - args, - ); + } = cli({ help: info, flags: flagDefs }, undefined, args); let packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index e5c674c50c..837fb3e50e 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -16,9 +16,50 @@ import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; +import { warnDeprecatedFlags } from '../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { + const flagDefs = { + select: { + type: String, + description: 'Select the thing you want to be creating upfront', + }, + option: { + type: [String] as const, + description: 'Pre-fill options for the creation process', + default: [] as string[], + }, + skipInstall: { + type: Boolean, + description: `Skips running 'yarn install' and 'yarn lint --fix'`, + }, + scope: { + type: String, + description: 'The scope to use for new packages', + }, + npmRegistry: { + type: String, + description: 'The package registry to use for new packages', + }, + baseVersion: { + type: String, + description: 'The version to use for any new packages (default: 0.1.0)', + }, + license: { + type: String, + description: + 'The license to use for any new packages (default: Apache-2.0)', + }, + private: { + type: Boolean, + description: 'Mark new packages as private', + default: true, + }, + }; + + warnDeprecatedFlags(args, flagDefs); + const { flags: { select, @@ -30,51 +71,7 @@ export default async ({ args, info }: CommandContext) => { license, private: isPrivate, }, - } = cli( - { - help: info, - flags: { - select: { - type: String, - description: 'Select the thing you want to be creating upfront', - }, - option: { - type: [String], - description: 'Pre-fill options for the creation process', - default: [], - }, - skipInstall: { - type: Boolean, - description: `Skips running 'yarn install' and 'yarn lint --fix'`, - }, - scope: { - type: String, - description: 'The scope to use for new packages', - }, - npmRegistry: { - type: String, - description: 'The package registry to use for new packages', - }, - baseVersion: { - type: String, - description: - 'The version to use for any new packages (default: 0.1.0)', - }, - license: { - type: String, - description: - 'The license to use for any new packages (default: Apache-2.0)', - }, - private: { - type: Boolean, - description: 'Mark new packages as private', - default: true, - }, - }, - }, - undefined, - args, - ); + } = cli({ help: info, flags: flagDefs }, undefined, args); const prefilledParams = parseParams(rawArgOptions); diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 743aa87c3d..2d639d61e5 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -31,6 +31,7 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; +import { warnDeprecatedFlags } from '../../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../../wiring/types'; type JestProject = { @@ -136,28 +137,31 @@ export default async ({ args, info }: CommandContext) => { // Parse Backstage-specific flags; unknown flags and arguments are left in // args so they can be forwarded to Jest. + const flagDefs = { + since: { + type: String, + description: 'Only include test packages changed since the specified ref', + }, + successCache: { + type: Boolean, + description: 'Cache and skip tests for unchanged packages', + }, + successCacheDir: { + type: String, + description: 'Directory for the success cache', + }, + jestHelp: { + type: Boolean, + description: "Show Jest's own help output", + }, + }; + + warnDeprecatedFlags(args, flagDefs); + const { flags: opts } = cli( { help: info, - flags: { - since: { - type: String, - description: - 'Only include test packages changed since the specified ref', - }, - successCache: { - type: Boolean, - description: 'Cache and skip tests for unchanged packages', - }, - successCacheDir: { - type: String, - description: 'Directory for the success cache', - }, - jestHelp: { - type: Boolean, - description: "Show Jest's own help output", - }, - }, + flags: flagDefs, ignoreArgv: type => type === 'unknown-flag' || type === 'argument', }, undefined, From e8173b3012297a239087e92090e47db02da65b3c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 10:10:31 +0100 Subject: [PATCH 09/13] Address review feedback: role.ts, repo start params, test coverage - Remove unnecessary optional chaining on getRoleInfo().role since it throws for unknown roles. - Add parameters declaration to repo start so [packages...] shows in help output. - Add tests verifying cleye strips Backstage flags from args before forwarding to Jest, including legacy camelCase flag support. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/cli-report.md | 4 +- .../src/modules/build/commands/repo/start.ts | 3 +- packages/cli/src/modules/build/lib/role.ts | 2 +- .../modules/test/commands/repo/test.test.ts | 56 +++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index f8d1db7019..88b82dda21 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -33,7 +33,7 @@ Commands: ### `backstage-cli build-workspace` ``` -Usage: backstage-cli build-workspace +Usage: backstage-cli build-workspace [packages...] Options: --always-pack @@ -511,7 +511,7 @@ Options: ### `backstage-cli repo start` ``` -Usage: backstage-cli repo start +Usage: backstage-cli repo start [packages...] Options: --config diff --git a/packages/cli/src/modules/build/commands/repo/start.ts b/packages/cli/src/modules/build/commands/repo/start.ts index 8d36d0732b..6acd4f1463 100644 --- a/packages/cli/src/modules/build/commands/repo/start.ts +++ b/packages/cli/src/modules/build/commands/repo/start.ts @@ -41,7 +41,8 @@ export default async ({ args, info }: CommandContext) => { _: namesOrPaths, } = cli( { - help: info, + help: { ...info, usage: `${info.usage} [packages...]` }, + parameters: ['[packages...]'], flags: { plugin: { type: [String], diff --git a/packages/cli/src/modules/build/lib/role.ts b/packages/cli/src/modules/build/lib/role.ts index bfcd1ecfb2..26c9da7cd0 100644 --- a/packages/cli/src/modules/build/lib/role.ts +++ b/packages/cli/src/modules/build/lib/role.ts @@ -23,7 +23,7 @@ export async function findRoleFromCommand(opts: { role?: string; }): Promise { if (opts.role) { - return PackageRoles.getRoleInfo(opts.role)?.role; + return PackageRoles.getRoleInfo(opts.role).role; } const pkg = await fs.readJson(targetPaths.resolve('package.json')); diff --git a/packages/cli/src/modules/test/commands/repo/test.test.ts b/packages/cli/src/modules/test/commands/repo/test.test.ts index d2f2485717..fe0e733f8c 100644 --- a/packages/cli/src/modules/test/commands/repo/test.test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { cli } from 'cleye'; import { createFlagFinder } from './test'; describe('createFlagFinder', () => { @@ -45,3 +46,58 @@ describe('createFlagFinder', () => { expect(find('--qux')).toBe(true); }); }); + +describe('repo test arg forwarding', () => { + // Mirrors the cleye configuration used in the repo test command handler + function parseRepoTestArgs(args: string[]) { + return cli( + { + help: false, + flags: { + since: { type: String }, + successCache: { type: Boolean }, + successCacheDir: { type: String }, + jestHelp: { type: Boolean }, + }, + ignoreArgv: type => type === 'unknown-flag' || type === 'argument', + }, + undefined, + args, + ); + } + + it('strips Backstage flags from args while preserving Jest flags and arguments', () => { + const args = [ + '--since', + 'main', + '--success-cache', + '--coverage', + '--watch', + 'path/to/test', + ]; + + const { flags } = parseRepoTestArgs(args); + + expect(flags.since).toBe('main'); + expect(flags.successCache).toBe(true); + expect(args).toEqual(['--coverage', '--watch', 'path/to/test']); + }); + + it('supports legacy camelCase flag names', () => { + const args = ['--successCache', '--successCacheDir', '/tmp/cache']; + + const { flags } = parseRepoTestArgs(args); + + expect(flags.successCache).toBe(true); + expect(flags.successCacheDir).toBe('/tmp/cache'); + expect(args).toEqual([]); + }); + + it('leaves args untouched when no Backstage flags are present', () => { + const args = ['--coverage', '--verbose', '--bail']; + + parseRepoTestArgs(args); + + expect(args).toEqual(['--coverage', '--verbose', '--bail']); + }); +}); From 80f17799809a3ee8802509956b352fbd2e9800b0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Mar 2026 10:13:57 +0100 Subject: [PATCH 10/13] Fix buildWorkspace flag normalization for =value forms Handle --alwaysPack=value and --alwaysYarnPack=value variants in the legacy flag normalization, not just the bare flag forms. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../src/modules/build/commands/buildWorkspace.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index ccb8267503..a949abe441 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -21,10 +21,18 @@ import { warnDeprecatedFlags } from '../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { - // Support legacy --alwaysYarnPack and --alwaysPack aliases - const normalizedArgs = args.map(a => - a === '--alwaysYarnPack' || a === '--alwaysPack' ? '--always-pack' : a, - ); + // Support legacy --alwaysYarnPack and --alwaysPack aliases (including =value form) + const normalizedArgs = args.map(a => { + for (const old of ['--alwaysYarnPack', '--alwaysPack']) { + if (a === old) { + return '--always-pack'; + } + if (a.startsWith(`${old}=`)) { + return `--always-pack${a.substring(old.length)}`; + } + } + return a; + }); const flagDefs = { alwaysPack: { From 8da18175840d61afafeda56419b78b3b052e5903 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Mar 2026 13:50:43 +0100 Subject: [PATCH 11/13] Inline deprecation warnings and fix package lint parameters Remove the central warnDeprecatedFlags helper and replace with module-specific deprecation checks at each call site. This avoids cross-module dependencies and makes the deprecated flag mappings explicit where they are consumed. Also fix buildWorkspace.ts to emit the deprecation warning during flag normalization (where the old flags are actually intercepted), and add positional parameter declaration to package lint so that directories show in help output. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli/cli-report.md | 2 +- packages/cli/src/lib/warnDeprecatedFlags.ts | 38 ------------------- .../modules/build/commands/buildWorkspace.ts | 27 ++++++------- .../src/modules/lint/commands/package/lint.ts | 3 +- .../src/modules/lint/commands/repo/lint.ts | 14 ++++++- packages/cli/src/modules/new/commands/new.ts | 13 ++++++- .../src/modules/test/commands/repo/test.ts | 13 ++++++- 7 files changed, 48 insertions(+), 62 deletions(-) delete mode 100644 packages/cli/src/lib/warnDeprecatedFlags.ts diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 88b82dda21..de2ded54d6 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -273,7 +273,7 @@ Options: ### `backstage-cli package lint` ``` -Usage: backstage-cli package lint +Usage: backstage-cli package lint [directories...] Options: --fix diff --git a/packages/cli/src/lib/warnDeprecatedFlags.ts b/packages/cli/src/lib/warnDeprecatedFlags.ts deleted file mode 100644 index 5b7b82794f..0000000000 --- a/packages/cli/src/lib/warnDeprecatedFlags.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2026 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Scans args for deprecated camelCase flag names and logs a warning for each - * match. Since type-flag accepts both camelCase and kebab-case, the old names - * still work — this just nudges users toward the new spelling. - */ -export function warnDeprecatedFlags( - args: string[], - flags: Record, -) { - for (const key of Object.keys(flags)) { - const kebab = key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`); - if (kebab === key) { - continue; - } - const deprecated = `--${key}`; - if (args.some(a => a === deprecated || a.startsWith(`${deprecated}=`))) { - process.stderr.write( - `DEPRECATION WARNING: ${deprecated} has been renamed to --${kebab}\n`, - ); - } - } -} diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index a949abe441..6641734a2a 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -17,33 +17,22 @@ import fs from 'fs-extra'; import { cli } from 'cleye'; import { createDistWorkspace } from '../lib/packager'; -import { warnDeprecatedFlags } from '../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { // Support legacy --alwaysYarnPack and --alwaysPack aliases (including =value form) const normalizedArgs = args.map(a => { for (const old of ['--alwaysYarnPack', '--alwaysPack']) { - if (a === old) { - return '--always-pack'; - } - if (a.startsWith(`${old}=`)) { + if (a === old || a.startsWith(`${old}=`)) { + process.stderr.write( + `DEPRECATION WARNING: ${old} has been renamed to --always-pack\n`, + ); return `--always-pack${a.substring(old.length)}`; } } return a; }); - const flagDefs = { - alwaysPack: { - type: Boolean, - description: - 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', - }, - }; - - warnDeprecatedFlags(args, flagDefs); - const { flags: { alwaysPack }, _: positionals, @@ -51,7 +40,13 @@ export default async ({ args, info }: CommandContext) => { { help: { ...info, usage: `${info.usage} [packages...]` }, parameters: ['', '[packages...]'], - flags: flagDefs, + flags: { + alwaysPack: { + type: Boolean, + description: + 'Force workspace output to be a result of running `yarn pack` on each package (warning: very slow)', + }, + }, }, undefined, normalizedArgs, diff --git a/packages/cli/src/modules/lint/commands/package/lint.ts b/packages/cli/src/modules/lint/commands/package/lint.ts index 7cecee10a7..40062726f1 100644 --- a/packages/cli/src/modules/lint/commands/package/lint.ts +++ b/packages/cli/src/modules/lint/commands/package/lint.ts @@ -26,7 +26,8 @@ export default async ({ args, info }: CommandContext) => { _: directories, } = cli( { - help: info, + help: { ...info, usage: `${info.usage} [directories...]` }, + parameters: ['[directories...]'], flags: { fix: { type: Boolean, diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 80df175315..ea7b183ba8 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -29,7 +29,6 @@ import { import { targetPaths } from '@backstage/cli-common'; import { createScriptOptionsParser } from '../../lib/optionsParser'; -import { warnDeprecatedFlags } from '../../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../../wiring/types'; function depCount(pkg: BackstagePackageJson) { @@ -76,7 +75,18 @@ export default async ({ args, info }: CommandContext) => { }, }; - warnDeprecatedFlags(args, flagDefs); + for (const [old, replacement] of Object.entries({ + '--outputFile': '--output-file', + '--successCache': '--success-cache', + '--successCacheDir': '--success-cache-dir', + '--maxWarnings': '--max-warnings', + })) { + if (args.some(a => a === old || a.startsWith(`${old}=`))) { + process.stderr.write( + `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, + ); + } + } const { flags: { diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index 837fb3e50e..0fb49e6b29 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -16,7 +16,6 @@ import { cli } from 'cleye'; import { createNewPackage } from '../lib/createNewPackage'; -import { warnDeprecatedFlags } from '../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { @@ -58,7 +57,17 @@ export default async ({ args, info }: CommandContext) => { }, }; - warnDeprecatedFlags(args, flagDefs); + for (const [old, replacement] of Object.entries({ + '--skipInstall': '--skip-install', + '--npmRegistry': '--npm-registry', + '--baseVersion': '--base-version', + })) { + if (args.some(a => a === old || a.startsWith(`${old}=`))) { + process.stderr.write( + `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, + ); + } + } const { flags: { diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 2d639d61e5..8d6287676e 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -31,7 +31,6 @@ import { findOwnPaths, isChildPath, } from '@backstage/cli-common'; -import { warnDeprecatedFlags } from '../../../../lib/warnDeprecatedFlags'; import type { CommandContext } from '../../../../wiring/types'; type JestProject = { @@ -156,7 +155,17 @@ export default async ({ args, info }: CommandContext) => { }, }; - warnDeprecatedFlags(args, flagDefs); + for (const [old, replacement] of Object.entries({ + '--successCache': '--success-cache', + '--successCacheDir': '--success-cache-dir', + '--jestHelp': '--jest-help', + })) { + if (args.some(a => a === old || a.startsWith(`${old}=`))) { + process.stderr.write( + `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, + ); + } + } const { flags: opts } = cli( { From c28e6a7b1ca3e7dd3c9711646790aac5659abb0a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Mar 2026 14:52:11 +0100 Subject: [PATCH 12/13] Remove unnecessary deprecation warnings for camelCase flags type-flag natively supports both camelCase and kebab-case for all flags, so no deprecation warnings or normalization are needed for casing variants. Only the --alwaysYarnPack alias (a genuinely different name) still requires normalization in buildWorkspace. Also inlined flagDefs at each call site and updated the changeset to reflect that both flag forms work transparently. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/migrate-cli-commands-to-cleye.md | 10 +- .../modules/build/commands/buildWorkspace.ts | 15 ++- .../src/modules/lint/commands/repo/lint.ts | 91 ++++++++---------- packages/cli/src/modules/new/commands/new.ts | 96 +++++++++---------- .../src/modules/test/commands/repo/test.ts | 51 ++++------ 5 files changed, 114 insertions(+), 149 deletions(-) diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md index 34b595e1a5..45f76fb6af 100644 --- a/.changeset/migrate-cli-commands-to-cleye.md +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -2,12 +2,4 @@ '@backstage/cli': patch --- -Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. The following CLI flags have been renamed from camelCase to kebab-case to match standard CLI conventions: - -- `--baseVersion` → `--base-version` -- `--successCache` → `--success-cache` -- `--successCacheDir` → `--success-cache-dir` -- `--alwaysPack` → `--always-pack` -- `--alwaysYarnPack` → `--always-pack` (hidden legacy alias preserved) - -The old camelCase flag names still work but will print a deprecation warning. Please update any scripts or CI configurations to use the new kebab-case spelling. +Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. CLI flags are now displayed in kebab-case in help output (e.g. `--success-cache` instead of `--successCache`), but both forms are accepted transparently — no changes needed in existing scripts or CI configurations. diff --git a/packages/cli/src/modules/build/commands/buildWorkspace.ts b/packages/cli/src/modules/build/commands/buildWorkspace.ts index 6641734a2a..649171d7d8 100644 --- a/packages/cli/src/modules/build/commands/buildWorkspace.ts +++ b/packages/cli/src/modules/build/commands/buildWorkspace.ts @@ -20,15 +20,14 @@ import { createDistWorkspace } from '../lib/packager'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { - // Support legacy --alwaysYarnPack and --alwaysPack aliases (including =value form) + // Normalize legacy --alwaysYarnPack alias (a genuinely different name, not + // just a casing variant — type-flag handles camelCase/kebab-case natively) const normalizedArgs = args.map(a => { - for (const old of ['--alwaysYarnPack', '--alwaysPack']) { - if (a === old || a.startsWith(`${old}=`)) { - process.stderr.write( - `DEPRECATION WARNING: ${old} has been renamed to --always-pack\n`, - ); - return `--always-pack${a.substring(old.length)}`; - } + if (a === '--alwaysYarnPack') { + return '--always-pack'; + } + if (a.startsWith('--alwaysYarnPack=')) { + return `--always-pack${a.substring('--alwaysYarnPack'.length)}`; } return a; }); diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index ea7b183ba8..513c76e0a5 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -40,54 +40,6 @@ function depCount(pkg: BackstagePackageJson) { } export default async ({ args, info }: CommandContext) => { - const flagDefs = { - fix: { - type: Boolean, - description: 'Attempt to automatically fix violations', - }, - format: { - type: String, - description: 'Lint report output format', - default: 'eslint-formatter-friendly', - }, - outputFile: { - type: String, - description: 'Write the lint report to a file instead of stdout', - }, - successCache: { - type: Boolean, - description: - 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', - }, - successCacheDir: { - type: String, - description: - 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', - }, - since: { - type: String, - description: 'Only lint packages that changed since the specified ref', - }, - maxWarnings: { - type: String, - description: - 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', - }, - }; - - for (const [old, replacement] of Object.entries({ - '--outputFile': '--output-file', - '--successCache': '--success-cache', - '--successCacheDir': '--success-cache-dir', - '--maxWarnings': '--max-warnings', - })) { - if (args.some(a => a === old || a.startsWith(`${old}=`))) { - process.stderr.write( - `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, - ); - } - } - const { flags: { fix, @@ -98,7 +50,48 @@ export default async ({ args, info }: CommandContext) => { since, maxWarnings, }, - } = cli({ help: info, flags: flagDefs }, undefined, args); + } = cli( + { + help: info, + flags: { + fix: { + type: Boolean, + description: 'Attempt to automatically fix violations', + }, + format: { + type: String, + description: 'Lint report output format', + default: 'eslint-formatter-friendly', + }, + outputFile: { + type: String, + description: 'Write the lint report to a file instead of stdout', + }, + successCache: { + type: Boolean, + description: + 'Enable success caching, which skips running tests for unchanged packages that were successful in the previous run', + }, + successCacheDir: { + type: String, + description: + 'Set the success cache location, (default: node_modules/.cache/backstage-cli)', + }, + since: { + type: String, + description: + 'Only lint packages that changed since the specified ref', + }, + maxWarnings: { + type: String, + description: + 'Fail if more than this number of warnings. -1 allows warnings. (default: -1)', + }, + }, + }, + undefined, + args, + ); let packages = await PackageGraph.listTargetPackages(); diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index 0fb49e6b29..a4899cf509 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -19,56 +19,6 @@ import { createNewPackage } from '../lib/createNewPackage'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { - const flagDefs = { - select: { - type: String, - description: 'Select the thing you want to be creating upfront', - }, - option: { - type: [String] as const, - description: 'Pre-fill options for the creation process', - default: [] as string[], - }, - skipInstall: { - type: Boolean, - description: `Skips running 'yarn install' and 'yarn lint --fix'`, - }, - scope: { - type: String, - description: 'The scope to use for new packages', - }, - npmRegistry: { - type: String, - description: 'The package registry to use for new packages', - }, - baseVersion: { - type: String, - description: 'The version to use for any new packages (default: 0.1.0)', - }, - license: { - type: String, - description: - 'The license to use for any new packages (default: Apache-2.0)', - }, - private: { - type: Boolean, - description: 'Mark new packages as private', - default: true, - }, - }; - - for (const [old, replacement] of Object.entries({ - '--skipInstall': '--skip-install', - '--npmRegistry': '--npm-registry', - '--baseVersion': '--base-version', - })) { - if (args.some(a => a === old || a.startsWith(`${old}=`))) { - process.stderr.write( - `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, - ); - } - } - const { flags: { select, @@ -80,7 +30,51 @@ export default async ({ args, info }: CommandContext) => { license, private: isPrivate, }, - } = cli({ help: info, flags: flagDefs }, undefined, args); + } = cli( + { + help: info, + flags: { + select: { + type: String, + description: 'Select the thing you want to be creating upfront', + }, + option: { + type: [String] as const, + description: 'Pre-fill options for the creation process', + default: [] as string[], + }, + skipInstall: { + type: Boolean, + description: `Skips running 'yarn install' and 'yarn lint --fix'`, + }, + scope: { + type: String, + description: 'The scope to use for new packages', + }, + npmRegistry: { + type: String, + description: 'The package registry to use for new packages', + }, + baseVersion: { + type: String, + description: + 'The version to use for any new packages (default: 0.1.0)', + }, + license: { + type: String, + description: + 'The license to use for any new packages (default: Apache-2.0)', + }, + private: { + type: Boolean, + description: 'Mark new packages as private', + default: true, + }, + }, + }, + undefined, + args, + ); const prefilledParams = parseParams(rawArgOptions); diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 8d6287676e..743aa87c3d 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -136,41 +136,28 @@ export default async ({ args, info }: CommandContext) => { // Parse Backstage-specific flags; unknown flags and arguments are left in // args so they can be forwarded to Jest. - const flagDefs = { - since: { - type: String, - description: 'Only include test packages changed since the specified ref', - }, - successCache: { - type: Boolean, - description: 'Cache and skip tests for unchanged packages', - }, - successCacheDir: { - type: String, - description: 'Directory for the success cache', - }, - jestHelp: { - type: Boolean, - description: "Show Jest's own help output", - }, - }; - - for (const [old, replacement] of Object.entries({ - '--successCache': '--success-cache', - '--successCacheDir': '--success-cache-dir', - '--jestHelp': '--jest-help', - })) { - if (args.some(a => a === old || a.startsWith(`${old}=`))) { - process.stderr.write( - `DEPRECATION WARNING: ${old} has been renamed to ${replacement}\n`, - ); - } - } - const { flags: opts } = cli( { help: info, - flags: flagDefs, + flags: { + since: { + type: String, + description: + 'Only include test packages changed since the specified ref', + }, + successCache: { + type: Boolean, + description: 'Cache and skip tests for unchanged packages', + }, + successCacheDir: { + type: String, + description: 'Directory for the success cache', + }, + jestHelp: { + type: Boolean, + description: "Show Jest's own help output", + }, + }, ignoreArgv: type => type === 'unknown-flag' || type === 'argument', }, undefined, From 08b0a7f933ce53163c97eab7b15c0a6d916e23f6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Mar 2026 15:13:58 +0100 Subject: [PATCH 13/13] Add deprecation warnings for camelCase flag usage Log a warning to stderr when users pass the camelCase form of flags that now have kebab-case as the canonical spelling. The old forms still work (type-flag handles both natively) but users should migrate. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/migrate-cli-commands-to-cleye.md | 2 +- packages/cli/src/modules/lint/commands/repo/lint.ts | 13 +++++++++++++ packages/cli/src/modules/new/commands/new.ts | 8 ++++++++ packages/cli/src/modules/test/commands/repo/test.ts | 8 ++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.changeset/migrate-cli-commands-to-cleye.md b/.changeset/migrate-cli-commands-to-cleye.md index 45f76fb6af..e6792423ee 100644 --- a/.changeset/migrate-cli-commands-to-cleye.md +++ b/.changeset/migrate-cli-commands-to-cleye.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. CLI flags are now displayed in kebab-case in help output (e.g. `--success-cache` instead of `--successCache`), but both forms are accepted transparently — no changes needed in existing scripts or CI configurations. +Migrated remaining CLI command handlers from `commander` to `cleye` for argument parsing. Several camelCase CLI flags have been deprecated in favor of their kebab-case equivalents (e.g. `--successCache` → `--success-cache`). The old camelCase forms still work but will now log a deprecation warning. Please update any scripts or CI configurations to use the kebab-case versions. diff --git a/packages/cli/src/modules/lint/commands/repo/lint.ts b/packages/cli/src/modules/lint/commands/repo/lint.ts index 513c76e0a5..174282ffcf 100644 --- a/packages/cli/src/modules/lint/commands/repo/lint.ts +++ b/packages/cli/src/modules/lint/commands/repo/lint.ts @@ -40,6 +40,19 @@ function depCount(pkg: BackstagePackageJson) { } export default async ({ args, info }: CommandContext) => { + for (const flag of [ + 'outputFile', + 'successCache', + 'successCacheDir', + 'maxWarnings', + ]) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } + } + const { flags: { fix, diff --git a/packages/cli/src/modules/new/commands/new.ts b/packages/cli/src/modules/new/commands/new.ts index a4899cf509..082d0ca028 100644 --- a/packages/cli/src/modules/new/commands/new.ts +++ b/packages/cli/src/modules/new/commands/new.ts @@ -19,6 +19,14 @@ import { createNewPackage } from '../lib/createNewPackage'; import type { CommandContext } from '../../../wiring/types'; export default async ({ args, info }: CommandContext) => { + for (const flag of ['skipInstall', 'npmRegistry', 'baseVersion']) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } + } + const { flags: { select, diff --git a/packages/cli/src/modules/test/commands/repo/test.ts b/packages/cli/src/modules/test/commands/repo/test.ts index 743aa87c3d..c331caf994 100644 --- a/packages/cli/src/modules/test/commands/repo/test.ts +++ b/packages/cli/src/modules/test/commands/repo/test.ts @@ -134,6 +134,14 @@ export function createFlagFinder(args: string[]) { export default async ({ args, info }: CommandContext) => { const testGlobal = global as TestGlobal; + for (const flag of ['successCache', 'successCacheDir', 'jestHelp']) { + if (args.some(a => a === `--${flag}` || a.startsWith(`--${flag}=`))) { + process.stderr.write( + `DEPRECATION WARNING: --${flag} is deprecated, use the kebab-case form instead\n`, + ); + } + } + // Parse Backstage-specific flags; unknown flags and arguments are left in // args so they can be forwarded to Jest. const { flags: opts } = cli(