From 28a438a9f2b4e3c866b82373ad26ba2d65d42962 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 11 Mar 2026 13:29:31 +0100 Subject: [PATCH] Add documentation to cli-plugin-api and remove CliFeature type Add thorough TSDoc comments to createCliPlugin, BackstageCommand, CommandContext, and CliPlugin. Remove the CliFeature type alias in favor of using CliPlugin directly. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- packages/cli-plugin-api/report.api.md | 9 +-- .../cli-plugin-api/src/createCliPlugin.ts | 27 +++++++ packages/cli-plugin-api/src/index.ts | 7 +- packages/cli-plugin-api/src/types.ts | 71 ++++++++++++++++--- packages/cli/src/wiring/CliInitializer.ts | 12 ++-- packages/cli/src/wiring/types.ts | 1 - 6 files changed, 95 insertions(+), 32 deletions(-) diff --git a/packages/cli-plugin-api/report.api.md b/packages/cli-plugin-api/report.api.md index d72169495f..0821e01085 100644 --- a/packages/cli-plugin-api/report.api.md +++ b/packages/cli-plugin-api/report.api.md @@ -5,11 +5,8 @@ ```ts // @public export interface BackstageCommand { - // (undocumented) deprecated?: boolean; - // (undocumented) description: string; - // (undocumented) execute: | ((context: CommandContext) => Promise) | { @@ -17,9 +14,7 @@ export interface BackstageCommand { default: (context: CommandContext) => Promise; }>; }; - // (undocumented) experimental?: boolean; - // (undocumented) path: string[]; } @@ -34,12 +29,10 @@ export interface CliPlugin { // @public export interface CommandContext { - // (undocumented) args: string[]; - // (undocumented) info: { usage: string; - description: string; + name: string; }; } diff --git a/packages/cli-plugin-api/src/createCliPlugin.ts b/packages/cli-plugin-api/src/createCliPlugin.ts index 04a26665a6..17ddd8ebb9 100644 --- a/packages/cli-plugin-api/src/createCliPlugin.ts +++ b/packages/cli-plugin-api/src/createCliPlugin.ts @@ -20,11 +20,38 @@ import { BackstageCommand, CliPlugin } from './types'; /** * Creates a new CLI plugin that provides commands to the Backstage CLI. * + * The `init` callback is invoked immediately at creation time and is used + * to register commands via the provided registry. The commands are then + * made available to the CLI host once the returned promise resolves. + * + * @example + * ``` + * import { createCliPlugin } from '@backstage/cli-plugin-api'; + * import packageJson from '../package.json'; + * + * export default createCliPlugin({ + * packageJson, + * init: async reg => { + * reg.addCommand({ + * path: ['repo', 'test'], + * description: 'Run tests across the repository', + * execute: { loader: () => import('./commands/test') }, + * }); + * }, + * }); + * ``` + * * @public */ export function createCliPlugin(options: { + /** The `package.json` contents of the plugin package, used to identify the plugin. */ packageJson: { name: string }; + /** + * An initialization callback that registers commands with the CLI. + * Called immediately when the plugin is created. + */ init: (registry: { + /** Registers a new command with the CLI. */ addCommand: (command: BackstageCommand) => void; }) => Promise; }): CliPlugin { diff --git a/packages/cli-plugin-api/src/index.ts b/packages/cli-plugin-api/src/index.ts index 988600280c..723b0edd55 100644 --- a/packages/cli-plugin-api/src/index.ts +++ b/packages/cli-plugin-api/src/index.ts @@ -15,9 +15,4 @@ */ export { createCliPlugin } from './createCliPlugin'; -export type { - BackstageCommand, - CommandContext, - CliPlugin, - CliFeature, -} from './types'; +export type { BackstageCommand, CommandContext, CliPlugin } from './types'; diff --git a/packages/cli-plugin-api/src/types.ts b/packages/cli-plugin-api/src/types.ts index 3bdf026490..cdb6d886ee 100644 --- a/packages/cli-plugin-api/src/types.ts +++ b/packages/cli-plugin-api/src/types.ts @@ -15,19 +15,33 @@ */ /** - * The context provided to a CLI command when it is executed. + * The context provided to a CLI command at the time of execution. + * + * Contains the parsed arguments and metadata about the command being run. * * @public */ export interface CommandContext { + /** + * The remaining arguments passed to the command after the command path + * has been resolved. This includes both positional arguments and flags. + * + * For example, running `backstage-cli repo test --verbose src/` would + * result in `args` being `['--verbose', 'src/']`. + */ args: string[]; + /** + * Metadata about the command being executed. + */ info: { /** - * The usage string of the current command, for example: "backstage-cli repo test" + * The full usage string of the command including the program name, + * for example `"backstage-cli repo test"`. */ usage: string; /** - * The name of the command, for example: "repo test" + * The name of the command as defined by its path, + * for example `"repo test"`. */ name: string; }; @@ -36,13 +50,54 @@ export interface CommandContext { /** * A command definition for a Backstage CLI plugin. * + * Each command is identified by a `path` that determines its position in + * the command tree. For example, a path of `['repo', 'test']` registers + * the command as `backstage-cli repo test`. + * + * Commands can either provide an `execute` function directly, or use a + * `loader` for deferred loading of the implementation. The loader pattern + * is recommended for commands with heavy dependencies, as it avoids + * loading the implementation until the command is actually invoked. + * * @public */ export interface BackstageCommand { + /** + * The path segments that define the command's position in the CLI tree. + * For example, `['repo', 'test']` maps to `backstage-cli repo test`. + */ path: string[]; + /** + * A short description of the command, displayed in help output. + */ description: string; + /** + * If `true`, the command is deprecated and will be hidden from help output + * but can still be invoked. + */ deprecated?: boolean; + /** + * If `true`, the command is experimental and will be hidden from help + * output but can still be invoked. + */ experimental?: boolean; + /** + * The command implementation, either as a direct function or as a loader + * that returns the implementation as a default export. The loader form + * is useful for deferring heavy imports until the command is invoked. + * + * @example + * Direct execution: + * ``` + * execute: async ({ args }) => { ... } + * ``` + * + * @example + * Deferred loading: + * ``` + * execute: { loader: () => import('./my-command') } + * ``` + */ execute: | ((context: CommandContext) => Promise) | { @@ -53,14 +108,8 @@ export interface BackstageCommand { } /** - * A CLI feature, currently always a CLI plugin. - * - * @public - */ -export type CliFeature = CliPlugin; - -/** - * A Backstage CLI plugin. + * An opaque representation of a Backstage CLI plugin, created + * using {@link createCliPlugin}. * * @public */ diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts index 2d1f94ce79..f63dc330e8 100644 --- a/packages/cli/src/wiring/CliInitializer.ts +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -16,7 +16,7 @@ import { CommandGraph } from './CommandGraph'; import { OpaqueCliPlugin } from '@internal/cli'; -import type { BackstageCommand, CliFeature } from '@backstage/cli-plugin-api'; +import type { BackstageCommand, CliPlugin } from '@backstage/cli-plugin-api'; import { CommandRegistry } from './CommandRegistry'; import { Command } from 'commander'; import { version } from './version'; @@ -36,12 +36,12 @@ function isNodeHidden( return node.children.every(child => isNodeHidden(child as any)); } -type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>; +type UninitializedFeature = CliPlugin | Promise<{ default: CliPlugin }>; export class CliInitializer { private graph = new CommandGraph(); private commandRegistry = new CommandRegistry(this.graph); - #uninitiazedFeatures: Promise[] = []; + #uninitiazedFeatures: Promise[] = []; add(feature: UninitializedFeature) { if (isPromise(feature)) { @@ -53,7 +53,7 @@ export class CliInitializer { } } - async #register(feature: CliFeature) { + async #register(feature: CliPlugin) { if (OpaqueCliPlugin.isType(feature)) { const internal = OpaqueCliPlugin.toInternal(feature); for (const command of await internal.commands) { @@ -180,8 +180,8 @@ export class CliInitializer { /** @internal */ export function unwrapFeature( - feature: CliFeature | { default: CliFeature }, -): CliFeature { + feature: CliPlugin | { default: CliPlugin }, +): CliPlugin { if ('$$type' in feature) { return feature; } diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts index b635e48ac0..98a20427b3 100644 --- a/packages/cli/src/wiring/types.ts +++ b/packages/cli/src/wiring/types.ts @@ -17,6 +17,5 @@ export type { CommandContext, BackstageCommand, - CliFeature, CliPlugin, } from '@backstage/cli-plugin-api';