From 0be3eab18b6375b3b458c539bdd5b2d7ce23d187 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 10 Mar 2026 22:06:36 +0100 Subject: [PATCH] cli: initial cli-plugin-api Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/cli-use-cli-plugin-api.md | 5 + .changeset/create-cli-plugin-api.md | 5 + packages/cli-plugin-api/catalog-info.yaml | 10 ++ packages/cli-plugin-api/package.json | 40 ++++++++ packages/cli-plugin-api/report.api.md | 96 +++++++++++++++++++ .../cli-plugin-api/src/createCliPlugin.ts | 36 +++++++ .../src/describeParentCallSite.ts | 54 +++++++++++ packages/cli-plugin-api/src/errors.ts | 72 ++++++++++++++ .../src/index.ts} | 18 +++- packages/cli-plugin-api/src/internals.ts | 40 ++++++++ packages/cli-plugin-api/src/lazy.ts | 59 ++++++++++++ packages/cli-plugin-api/src/types.ts | 91 ++++++++++++++++++ packages/cli/package.json | 1 + packages/cli/src/modules/build/index.ts | 2 +- .../cli/src/modules/config/commands/docs.ts | 2 +- .../cli/src/modules/config/commands/print.ts | 2 +- .../cli/src/modules/config/commands/schema.ts | 2 +- .../src/modules/config/commands/validate.ts | 2 +- packages/cli/src/modules/config/index.ts | 2 +- .../src/modules/create-github-app/index.ts | 2 +- .../cli/src/modules/info/commands/info.ts | 2 +- packages/cli/src/modules/info/index.ts | 2 +- packages/cli/src/modules/lint/index.ts | 2 +- packages/cli/src/modules/maintenance/index.ts | 2 +- packages/cli/src/modules/migrate/index.ts | 2 +- packages/cli/src/modules/new/index.ts | 2 +- packages/cli/src/modules/test/index.ts | 2 +- .../modules/translations/commands/export.ts | 2 +- .../modules/translations/commands/import.ts | 2 +- .../cli/src/modules/translations/index.ts | 2 +- packages/cli/src/wiring/CliInitializer.ts | 13 ++- packages/cli/src/wiring/errors.ts | 47 +-------- packages/cli/src/wiring/factory.ts | 16 +--- packages/cli/src/wiring/lazy.ts | 32 +------ packages/cli/src/wiring/types.ts | 57 ++--------- yarn.lock | 11 +++ 36 files changed, 569 insertions(+), 168 deletions(-) create mode 100644 .changeset/cli-use-cli-plugin-api.md create mode 100644 .changeset/create-cli-plugin-api.md create mode 100644 packages/cli-plugin-api/catalog-info.yaml create mode 100644 packages/cli-plugin-api/package.json create mode 100644 packages/cli-plugin-api/report.api.md create mode 100644 packages/cli-plugin-api/src/createCliPlugin.ts create mode 100644 packages/cli-plugin-api/src/describeParentCallSite.ts create mode 100644 packages/cli-plugin-api/src/errors.ts rename packages/{cli/src/wiring/describeParentCallSite.ts => cli-plugin-api/src/index.ts} (58%) create mode 100644 packages/cli-plugin-api/src/internals.ts create mode 100644 packages/cli-plugin-api/src/lazy.ts create mode 100644 packages/cli-plugin-api/src/types.ts diff --git a/.changeset/cli-use-cli-plugin-api.md b/.changeset/cli-use-cli-plugin-api.md new file mode 100644 index 0000000000..c16d0e2c06 --- /dev/null +++ b/.changeset/cli-use-cli-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Migrated CLI plugin modules to import from the new `@backstage/cli-plugin-api` package. diff --git a/.changeset/create-cli-plugin-api.md b/.changeset/create-cli-plugin-api.md new file mode 100644 index 0000000000..0b758c76dd --- /dev/null +++ b/.changeset/create-cli-plugin-api.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-plugin-api': minor +--- + +Added a new `@backstage/cli-plugin-api` package that provides the public API for building Backstage CLI plugins. This includes `createCliPlugin`, `lazy`, `ExitCodeError`, `exitWithError`, and associated types. diff --git a/packages/cli-plugin-api/catalog-info.yaml b/packages/cli-plugin-api/catalog-info.yaml new file mode 100644 index 0000000000..3a0aca14b9 --- /dev/null +++ b/packages/cli-plugin-api/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-cli-plugin-api + title: '@backstage/cli-plugin-api' + description: API for building Backstage CLI plugins +spec: + lifecycle: experimental + type: backstage-cli-plugin + owner: tooling-maintainers diff --git a/packages/cli-plugin-api/package.json b/packages/cli-plugin-api/package.json new file mode 100644 index 0000000000..d7fb328943 --- /dev/null +++ b/packages/cli-plugin-api/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/cli-plugin-api", + "version": "0.1.0", + "description": "API for building Backstage CLI plugins", + "backstage": { + "role": "cli-plugin" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/cli-plugin-api" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/errors": "workspace:^", + "chalk": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md new file mode 100644 index 0000000000..7d5b9ed63c --- /dev/null +++ b/packages/cli-plugin-api/report.api.md @@ -0,0 +1,96 @@ +## API Report File for "@backstage/cli-plugin-api" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CustomErrorBase } from '@backstage/errors'; + +// @public +export type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ( + ...args: any[] + ) => Promise + ? KName + : never]: TModule[KName]; +}; + +// @public +export interface BackstageCommand { + // (undocumented) + deprecated?: boolean; + // (undocumented) + description: string; + // (undocumented) + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ + default: CommandExecuteFn; + }>; + }; + // (undocumented) + path: string[]; +} + +// @public +export type CliFeature = CliPlugin; + +// @public +export interface CliPlugin { + // (undocumented) + readonly $$type: '@backstage/CliPlugin'; + // (undocumented) + readonly pluginId: string; +} + +// @public +export interface CommandContext { + // (undocumented) + args: string[]; + // (undocumented) + info: { + usage: string; + description: string; + }; +} + +// @public +export type CommandExecuteFn = (context: CommandContext) => Promise; + +// @public +export function createCliPlugin(options: { + pluginId: string; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; +}): CliPlugin; + +// @public +export class ExitCodeError extends CustomErrorBase { + constructor(code: number, command?: string); + // (undocumented) + readonly code: number; +} + +// @public +export function exitWithError(error: unknown): never; + +// @public +export function initializeCliPlugin( + plugin: CliPlugin, + registry: { + addCommand: (command: BackstageCommand) => void; + }, +): Promise; + +// @public +export function isCliPlugin(value: unknown): value is CliPlugin; + +// @public +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts new file mode 100644 index 0000000000..df2d3e02fd --- /dev/null +++ b/packages/cli-plugin-api/src/createCliPlugin.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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. + */ + +import { describeParentCallSite } from './describeParentCallSite'; +import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; + +/** + * Creates a new CLI plugin that provides commands to the Backstage CLI. + * + * @public + */ +export function createCliPlugin(options: { + pluginId: string; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; +}): CliPlugin { + return OpaqueCliPlugin.createInstance('v1', { + pluginId: options.pluginId, + init: options.init, + description: `created at '${describeParentCallSite()}'`, + }); +} diff --git a/packages/cli-plugin-api/src/describeParentCallSite.ts b/packages/cli-plugin-api/src/describeParentCallSite.ts new file mode 100644 index 0000000000..641365bde8 --- /dev/null +++ b/packages/cli-plugin-api/src/describeParentCallSite.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2023 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. + */ + +const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; + +/** + * Internal helper that describes the location of the parent caller. + * @internal + */ +export function describeParentCallSite( + ErrorConstructor: { new (message: string): Error } = Error, +): string { + const { stack } = new ErrorConstructor(MESSAGE_MARKER); + if (!stack) { + return ''; + } + + const startIndex = stack.includes(MESSAGE_MARKER) + ? stack.indexOf('\n') + 1 + : 0; + const secondEntryStart = + stack.indexOf('\n', stack.indexOf('\n', startIndex) + 1) + 1; + const secondEntryEnd = stack.indexOf('\n', secondEntryStart); + + const line = stack.substring(secondEntryStart, secondEntryEnd).trim(); + if (!line) { + return 'unknown'; + } + + // Chrome + if (line.includes('(')) { + return line.substring(line.indexOf('(') + 1, line.indexOf(')')); + } + + // Safari & Firefox + if (line.includes('@')) { + return line.substring(line.indexOf('@') + 1); + } + + return line; +} diff --git a/packages/cli-plugin-api/src/errors.ts b/packages/cli-plugin-api/src/errors.ts new file mode 100644 index 0000000000..5334345608 --- /dev/null +++ b/packages/cli-plugin-api/src/errors.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2020 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. + */ + +import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; +import chalk from 'chalk'; + +/** + * An error that indicates a child process exited with a specific code. + * + * @public + */ +export class ExitCodeError extends CustomErrorBase { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +function exit(message: string, code: number = 1): never { + process.stderr.write(`\n${chalk.red(message)}\n\n`); + process.exit(code); +} + +/** + * Exits the process with an appropriate error code based on the error type. + * + * @public + */ +export function exitWithError(error: unknown): never { + if (!isError(error)) { + process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); + process.exit(1); + } + + switch (error.name) { + case 'InputError': + return exit(error.message, 74 /* input/output error */); + case 'NotFoundError': + return exit(error.message, 127 /* command not found */); + case 'NotImplementedError': + return exit(error.message, 64 /* command line usage error */); + case 'AuthenticationError': + case 'NotAllowedError': + return exit(error.message, 77 /* permission denied */); + case 'ExitCodeError': + return exit( + error.message, + 'code' in error && typeof error.code === 'number' ? error.code : 1, + ); + default: + return exit(stringifyError(error), 1); + } +} diff --git a/packages/cli/src/wiring/describeParentCallSite.ts b/packages/cli-plugin-api/src/index.ts similarity index 58% rename from packages/cli/src/wiring/describeParentCallSite.ts rename to packages/cli-plugin-api/src/index.ts index 35603e33b0..be990b7357 100644 --- a/packages/cli/src/wiring/describeParentCallSite.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * Copyright 2024 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. @@ -14,7 +14,15 @@ * limitations under the License. */ -// Single re-export to avoid doing this import in multiple places, but still -// avoid duplicate declarations because this one is a bit tricky. -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; +export { createCliPlugin } from './createCliPlugin'; +export { lazy } from './lazy'; +export type { ActionExports } from './lazy'; +export { ExitCodeError, exitWithError } from './errors'; +export type { + BackstageCommand, + CommandContext, + CommandExecuteFn, + CliPlugin, + CliFeature, +} from './types'; +export { isCliPlugin, initializeCliPlugin } from './internals'; diff --git a/packages/cli-plugin-api/src/internals.ts b/packages/cli-plugin-api/src/internals.ts new file mode 100644 index 0000000000..99c1c4d1f8 --- /dev/null +++ b/packages/cli-plugin-api/src/internals.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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. + */ + +import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; + +/** + * Checks whether a value is a {@link CliPlugin}. + * + * @public + */ +export function isCliPlugin(value: unknown): value is CliPlugin { + return OpaqueCliPlugin.isType(value); +} + +/** + * Initializes a CLI plugin by calling its init function with the provided + * command registry. + * + * @public + */ +export async function initializeCliPlugin( + plugin: CliPlugin, + registry: { addCommand: (command: BackstageCommand) => void }, +): Promise { + const internal = OpaqueCliPlugin.toInternal(plugin); + await internal.init(registry); +} diff --git a/packages/cli-plugin-api/src/lazy.ts b/packages/cli-plugin-api/src/lazy.ts new file mode 100644 index 0000000000..30b8568fb1 --- /dev/null +++ b/packages/cli-plugin-api/src/lazy.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2024 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. + */ + +import { assertError } from '@backstage/errors'; +import { exitWithError } from './errors'; + +/** + * A mapped type that extracts keys of a module whose values are async action functions. + * + * @public + */ +export type ActionExports = { + [KName in keyof TModule as TModule[KName] extends ( + ...args: any[] + ) => Promise + ? KName + : never]: TModule[KName]; +}; + +/** + * Wraps an action function so that it always exits and handles errors. + * + * @public + */ +export function lazy( + moduleLoader: () => Promise, + exportName: keyof ActionExports, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const mod = await moduleLoader(); + const actualModule = ( + mod as unknown as { default: ActionExports } + ).default; + const actionFunc = actualModule[exportName] as ( + ...args: any[] + ) => Promise; + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts new file mode 100644 index 0000000000..1736537580 --- /dev/null +++ b/packages/cli-plugin-api/src/types.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2024 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. + */ +import { OpaqueType } from '@internal/opaque'; + +/** + * The context provided to a CLI command when it is executed. + * + * @public + */ +export interface CommandContext { + args: string[]; + info: { + /** + * The usage string of the current command, for example: "backstage-cli repo test" + */ + usage: string; + /** + * The description provided for the command + */ + description: string; + }; +} + +/** + * A function that executes a CLI command. + * + * @public + */ +export type CommandExecuteFn = (context: CommandContext) => Promise; + +/** + * A command definition for a Backstage CLI plugin. + * + * @public + */ +export interface BackstageCommand { + path: string[]; + description: string; + deprecated?: boolean; + experimental?: boolean; + execute: + | CommandExecuteFn + | { + loader: () => Promise<{ default: CommandExecuteFn }>; + }; +} + +/** + * A CLI feature, currently always a CLI plugin. + * + * @public + */ +export type CliFeature = CliPlugin; + +/** + * A Backstage CLI plugin. + * + * @public + */ +export interface CliPlugin { + readonly pluginId: string; + readonly $$type: '@backstage/CliPlugin'; +} + +/** @internal */ +export const OpaqueCliPlugin = OpaqueType.create<{ + public: CliPlugin; + versions: { + readonly version: 'v1'; + readonly description: string; + init: (registry: { + addCommand: (command: BackstageCommand) => void; + }) => Promise; + }; +}>({ + type: '@backstage/CliPlugin', + versions: ['v1'], +}); diff --git a/packages/cli/package.json b/packages/cli/package.json index 8e03c60d0f..d309e0453c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/cli-node": "workspace:^", + "@backstage/cli-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/packages/cli/src/modules/build/index.ts b/packages/cli/src/modules/build/index.ts index 139c591af2..2ccf1e3b6e 100644 --- a/packages/cli/src/modules/build/index.ts +++ b/packages/cli/src/modules/build/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export const buildPlugin = createCliPlugin({ pluginId: 'build', diff --git a/packages/cli/src/modules/config/commands/docs.ts b/packages/cli/src/modules/config/commands/docs.ts index 84d2f384e1..8015947a57 100644 --- a/packages/cli/src/modules/config/commands/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -21,7 +21,7 @@ import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; import chalk from 'chalk'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/modules/config/commands/print.ts b/packages/cli/src/modules/config/commands/print.ts index 33f7245604..db681f4619 100644 --- a/packages/cli/src/modules/config/commands/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -19,7 +19,7 @@ import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; import { loadCliConfig } from '../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/schema.ts b/packages/cli/src/modules/config/commands/schema.ts index 13f651534c..1635caf7e6 100644 --- a/packages/cli/src/modules/config/commands/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -20,7 +20,7 @@ import { stringify as stringifyYaml } from 'yaml'; import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/commands/validate.ts b/packages/cli/src/modules/config/commands/validate.ts index f6cf9cc662..302c0b6480 100644 --- a/packages/cli/src/modules/config/commands/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -16,7 +16,7 @@ import { cli } from 'cleye'; import { loadCliConfig } from '../lib/config'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts index 0d90037f18..c8fb6bca7e 100644 --- a/packages/cli/src/modules/config/index.ts +++ b/packages/cli/src/modules/config/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export const configOption = [ '--config ', diff --git a/packages/cli/src/modules/create-github-app/index.ts b/packages/cli/src/modules/create-github-app/index.ts index 9dad46577e..ea6cbf55be 100644 --- a/packages/cli/src/modules/create-github-app/index.ts +++ b/packages/cli/src/modules/create-github-app/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'new', diff --git a/packages/cli/src/modules/info/commands/info.ts b/packages/cli/src/modules/info/commands/info.ts index ab26714fca..0170c4204a 100644 --- a/packages/cli/src/modules/info/commands/info.ts +++ b/packages/cli/src/modules/info/commands/info.ts @@ -25,7 +25,7 @@ import { } from '@backstage/cli-node'; import { minimatch } from 'minimatch'; import fs from 'fs-extra'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; /** * Attempts to read package.json from node_modules for a given package name. diff --git a/packages/cli/src/modules/info/index.ts b/packages/cli/src/modules/info/index.ts index 8e1fe9cbff..11ff207281 100644 --- a/packages/cli/src/modules/info/index.ts +++ b/packages/cli/src/modules/info/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'info', diff --git a/packages/cli/src/modules/lint/index.ts b/packages/cli/src/modules/lint/index.ts index 8e02b7653f..d825740b9f 100644 --- a/packages/cli/src/modules/lint/index.ts +++ b/packages/cli/src/modules/lint/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'lint', diff --git a/packages/cli/src/modules/maintenance/index.ts b/packages/cli/src/modules/maintenance/index.ts index a6597559f8..4356c4654f 100644 --- a/packages/cli/src/modules/maintenance/index.ts +++ b/packages/cli/src/modules/maintenance/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'maintenance', diff --git a/packages/cli/src/modules/migrate/index.ts b/packages/cli/src/modules/migrate/index.ts index c731479a51..87c0543e90 100644 --- a/packages/cli/src/modules/migrate/index.ts +++ b/packages/cli/src/modules/migrate/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'migrate', diff --git a/packages/cli/src/modules/new/index.ts b/packages/cli/src/modules/new/index.ts index 5b2a6562ea..2d30fb45bc 100644 --- a/packages/cli/src/modules/new/index.ts +++ b/packages/cli/src/modules/new/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; import { NotImplementedError } from '@backstage/errors'; export default createCliPlugin({ diff --git a/packages/cli/src/modules/test/index.ts b/packages/cli/src/modules/test/index.ts index 627648332b..f991d23ce2 100644 --- a/packages/cli/src/modules/test/index.ts +++ b/packages/cli/src/modules/test/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'test', diff --git a/packages/cli/src/modules/translations/commands/export.ts b/packages/cli/src/modules/translations/commands/export.ts index b6471f10f4..146e9ce121 100644 --- a/packages/cli/src/modules/translations/commands/export.ts +++ b/packages/cli/src/modules/translations/commands/export.ts @@ -33,7 +33,7 @@ import { formatMessagePath, validatePattern, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; export default async ({ args, info }: CommandContext) => { const { diff --git a/packages/cli/src/modules/translations/commands/import.ts b/packages/cli/src/modules/translations/commands/import.ts index 155724dd8c..2739dbac41 100644 --- a/packages/cli/src/modules/translations/commands/import.ts +++ b/packages/cli/src/modules/translations/commands/import.ts @@ -28,7 +28,7 @@ import { createMessagePathParser, formatMessagePath, } from '../lib/messageFilePath'; -import type { CommandContext } from '../../../wiring/types'; +import type { CommandContext } from '@backstage/cli-plugin-api'; interface ManifestRefEntry { package: string; diff --git a/packages/cli/src/modules/translations/index.ts b/packages/cli/src/modules/translations/index.ts index 43263831d5..9a001bab23 100644 --- a/packages/cli/src/modules/translations/index.ts +++ b/packages/cli/src/modules/translations/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createCliPlugin } from '../../wiring/factory'; +import { createCliPlugin } from '@backstage/cli-plugin-api'; export default createCliPlugin({ pluginId: 'translations', diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index d9af1e8a4d..b44dba862f 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -15,12 +15,16 @@ */ import { CommandGraph } from './CommandGraph'; -import { BackstageCommand, CliFeature, OpaqueCliPlugin } from './types'; +import { + isCliPlugin, + initializeCliPlugin, + exitWithError, +} from '@backstage/cli-plugin-api'; +import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; import chalk from 'chalk'; -import { exitWithError } from './errors'; import { ForwardedError } from '@backstage/errors'; import { isPromise } from 'node:util/types'; @@ -53,9 +57,8 @@ export class CliInitializer { } async #register(feature: CliFeature) { - if (OpaqueCliPlugin.isType(feature)) { - const internal = OpaqueCliPlugin.toInternal(feature); - await internal.init(this.commandRegistry); + if (isCliPlugin(feature)) { + await initializeCliPlugin(feature, this.commandRegistry); } else { throw new Error(`Unsupported feature type: ${(feature as any).$$type}`); } diff --git a/packages/cli/src/wiring/errors.ts b/packages/cli/src/wiring/errors.ts index aee6d3f575..b684f5c64a 100644 --- a/packages/cli/src/wiring/errors.ts +++ b/packages/cli/src/wiring/errors.ts @@ -14,49 +14,4 @@ * limitations under the License. */ -import { CustomErrorBase, isError, stringifyError } from '@backstage/errors'; -import chalk from 'chalk'; - -export class ExitCodeError extends CustomErrorBase { - readonly code: number; - - constructor(code: number, command?: string) { - super( - command - ? `Command '${command}' exited with code ${code}` - : `Child exited with code ${code}`, - ); - this.code = code; - } -} - -function exit(message: string, code: number = 1): never { - process.stderr.write(`\n${chalk.red(message)}\n\n`); - process.exit(code); -} - -export function exitWithError(error: unknown): never { - if (!isError(error)) { - process.stderr.write(`\n${chalk.red(stringifyError(error))}\n\n`); - process.exit(1); - } - - switch (error.name) { - case 'InputError': - return exit(error.message, 74 /* input/output error */); - case 'NotFoundError': - return exit(error.message, 127 /* command not found */); - case 'NotImplementedError': - return exit(error.message, 64 /* command line usage error */); - case 'AuthenticationError': - case 'NotAllowedError': - return exit(error.message, 77 /* permissino denied */); - case 'ExitCodeError': - return exit( - error.message, - 'code' in error && typeof error.code === 'number' ? error.code : 1, - ); - default: - return exit(stringifyError(error), 1); - } -} +export { ExitCodeError, exitWithError } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts index 6374f37c14..19c68e805f 100644 --- a/packages/cli/src/wiring/factory.ts +++ b/packages/cli/src/wiring/factory.ts @@ -14,18 +14,4 @@ * limitations under the License. */ -import { describeParentCallSite } from './describeParentCallSite'; -import { BackstageCommand, CliPlugin, OpaqueCliPlugin } from './types'; - -export function createCliPlugin(options: { - pluginId: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; -}): CliPlugin { - return OpaqueCliPlugin.createInstance('v1', { - pluginId: options.pluginId, - init: options.init, - description: `created at '${describeParentCallSite()}'`, - }); -} +export { createCliPlugin } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/lazy.ts b/packages/cli/src/wiring/lazy.ts index 64a4d43ef4..feda134d07 100644 --- a/packages/cli/src/wiring/lazy.ts +++ b/packages/cli/src/wiring/lazy.ts @@ -14,34 +14,4 @@ * limitations under the License. */ -import { assertError } from '@backstage/errors'; -import { exitWithError } from './errors'; - -type ActionFunc = (...args: any[]) => Promise; -type ActionExports = { - [KName in keyof TModule as TModule[KName] extends ActionFunc - ? KName - : never]: TModule[KName]; -}; - -// Wraps an action function so that it always exits and handles errors -export function lazy( - moduleLoader: () => Promise, - exportName: keyof ActionExports, -): (...args: any[]) => Promise { - return async (...args: any[]) => { - try { - const mod = await moduleLoader(); - const actualModule = ( - mod as unknown as { default: ActionExports } - ).default; - const actionFunc = actualModule[exportName] as ActionFunc; - await actionFunc(...args); - - process.exit(0); - } catch (error) { - assertError(error); - exitWithError(error); - } - }; -} +export { lazy } from '@backstage/cli-plugin-api'; diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index fa3d2341f0..0cf4f849a9 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -13,53 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpaqueType } from '@internal/opaque'; -export interface CommandContext { - args: string[]; - info: { - /** - * The usage string of the current command, for example: "backstage-cli repo test" - */ - usage: string; - /** - * The description provided for the command - */ - description: string; - }; -} - -export type CommandExecuteFn = (context: CommandContext) => Promise; - -export interface BackstageCommand { - path: string[]; - description: string; - deprecated?: boolean; - experimental?: boolean; - execute: - | CommandExecuteFn - | { - loader: () => Promise<{ default: CommandExecuteFn }>; - }; -} - -export type CliFeature = CliPlugin; - -export interface CliPlugin { - readonly pluginId: string; - readonly $$type: '@backstage/CliPlugin'; -} - -export const OpaqueCliPlugin = OpaqueType.create<{ - public: CliPlugin; - versions: { - readonly version: 'v1'; - readonly description: string; - init: (registry: { - addCommand: (command: BackstageCommand) => void; - }) => Promise; - }; -}>({ - type: '@backstage/CliPlugin', - versions: ['v1'], -}); +// Re-export types from the plugin API for internal use within the CLI package. +export type { + CommandContext, + CommandExecuteFn, + BackstageCommand, + CliFeature, + CliPlugin, +} from '@backstage/cli-plugin-api'; diff --git a/yarn.lock b/yarn.lock index 32b09cc5e3..6977b9161f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2822,6 +2822,16 @@ __metadata: languageName: unknown linkType: soft +"@backstage/cli-plugin-api@workspace:^, @backstage/cli-plugin-api@workspace:packages/cli-plugin-api": + version: 0.0.0-use.local + resolution: "@backstage/cli-plugin-api@workspace:packages/cli-plugin-api" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/errors": "workspace:^" + chalk: "npm:^4.0.0" + languageName: unknown + linkType: soft + "@backstage/cli@workspace:*, @backstage/cli@workspace:^, @backstage/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@backstage/cli@workspace:packages/cli" @@ -2832,6 +2842,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/cli-node": "workspace:^" + "@backstage/cli-plugin-api": "workspace:^" "@backstage/config": "workspace:^" "@backstage/config-loader": "workspace:^" "@backstage/core-app-api": "workspace:^"