diff --git a/packages/cli/bin/backstage-cli-alpha b/packages/cli/bin/backstage-cli-alpha new file mode 100755 index 0000000000..38a2df3319 --- /dev/null +++ b/packages/cli/bin/backstage-cli-alpha @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/* + * 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. + */ + +require('@backstage/cli/config/nodeTransform.cjs'); +require('../src/alpha'); diff --git a/packages/cli/src/alpha.ts b/packages/cli/src/alpha.ts new file mode 100644 index 0000000000..4b2cfa4842 --- /dev/null +++ b/packages/cli/src/alpha.ts @@ -0,0 +1,29 @@ +/* + * 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 { CliInitializer } from './wiring/CliInitializer'; +import chalk from 'chalk'; + +(async () => { + console.warn( + chalk.yellow( + 'THIS ENTRYPOINT IS IN ALPHA AND MAY CHANGE IN THE FUTURE - DO NOT USE THIS FOR NORMAL DEVELOPMENT', + ), + ); + const initializer = new CliInitializer(); + initializer.add(import('./modules/config/alpha').then(m => m.default)); + await initializer.run(); +})(); diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 59f3225738..0ba0669c32 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { buildBundle, getModuleFederationOptions } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; -import { loadCliConfig } from '../../lib/config'; +import { loadCliConfig } from '../../modules/config/lib/config'; interface BuildAppOptions { targetDir: string; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 2a77d527c1..f249563a34 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -15,15 +15,11 @@ */ import { Command, Option } from 'commander'; -import { assertError } from '@backstage/errors'; -import { exitWithError } from '../lib/errors'; - -const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => (opts ? [...opts, opt] : [opt]), - Array(), -] as const; +import { lazy } from '../lib/lazy'; +import { + configOption, + registerCommands as registerConfigCommands, +} from '../modules/config'; export function registerRepoCommand(program: Command) { const command = program @@ -279,66 +275,7 @@ export function registerCommands(program: Command) { .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./new/new').then(m => m.default))); - program - .command('config:docs') - .option( - '--package ', - 'Only include the schema that applies to the given package', - ) - .description('Browse the configuration reference documentation') - .action(lazy(() => import('./config/docs').then(m => m.default))); - - program - .command('config:print') - .option( - '--package ', - 'Only load config schema that applies to the given package', - ) - .option('--lax', 'Do not require environment variables to be set') - .option('--frontend', 'Print only the frontend configuration') - .option('--with-secrets', 'Include secrets in the printed configuration') - .option( - '--format ', - 'Format to print the configuration in, either json or yaml [yaml]', - ) - .option(...configOption) - .description('Print the app configuration for the current package') - .action(lazy(() => import('./config/print').then(m => m.default))); - - program - .command('config:check') - .option( - '--package ', - 'Only load config schema that applies to the given package', - ) - .option('--lax', 'Do not require environment variables to be set') - .option('--frontend', 'Only validate the frontend configuration') - .option('--deprecated', 'Output deprecated configuration settings') - .option( - '--strict', - 'Enable strict config validation, forbidding errors and unknown keys', - ) - .option(...configOption) - .description( - 'Validate that the given configuration loads and matches schema', - ) - .action(lazy(() => import('./config/validate').then(m => m.default))); - - program - .command('config:schema') - .option( - '--package ', - 'Only output config schema that applies to the given package', - ) - .option( - '--format ', - 'Format to print the schema in, either json or yaml [yaml]', - ) - .option('--merge', 'Print the config schemas merged', true) - .option('--no-merge', 'Print the config schemas not merged') - .description('Print configuration schema') - .action(lazy(() => import('./config/schema').then(m => m.default))); - + registerConfigCommands(program); registerRepoCommand(program); registerScriptCommand(program); registerMigrateCommand(program); @@ -444,20 +381,3 @@ function removed(message?: string) { process.exit(1); }; } - -// Wraps an action function so that it always exits and handles errors -function lazy( - getActionFunc: () => Promise<(...args: any[]) => Promise>, -): (...args: any[]) => Promise { - return async (...args: any[]) => { - try { - const actionFunc = await getActionFunc(); - await actionFunc(...args); - - process.exit(0); - } catch (error) { - assertError(error); - exitWithError(error); - } - }; -} diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index f8d259f467..dc16a4577d 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -22,7 +22,7 @@ import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; import { paths as libPaths } from '../../lib/paths'; -import { loadCliConfig } from '../config'; +import { loadCliConfig } from '../../modules/config/lib/config'; import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; diff --git a/packages/cli/src/lib/lazy.ts b/packages/cli/src/lib/lazy.ts new file mode 100644 index 0000000000..6d2cb1cd4f --- /dev/null +++ b/packages/cli/src/lib/lazy.ts @@ -0,0 +1,35 @@ +/* + * 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 '../lib/errors'; + +// Wraps an action function so that it always exits and handles errors +export function lazy( + getActionFunc: () => Promise<(...args: any[]) => Promise>, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const actionFunc = await getActionFunc(); + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/cli/src/modules/config/alpha.ts b/packages/cli/src/modules/config/alpha.ts new file mode 100644 index 0000000000..c32982086a --- /dev/null +++ b/packages/cli/src/modules/config/alpha.ts @@ -0,0 +1,98 @@ +/* + * 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 { createCliPlugin } from '../../wiring/factory'; +import yargs from 'yargs'; +import { Command } from 'commander'; +import { lazy } from '../../lib/lazy'; + +export default createCliPlugin({ + pluginId: 'config', + init: async reg => { + reg.addCommand({ + path: ['config:docs'], + description: 'Browse the configuration reference documentation', + execute: async ({ args }) => { + const command = new Command(); + const defaultCommand = command + .option( + '--package ', + 'Only include the schema that applies to the given package', + ) + .description('Browse the configuration reference documentation') + .action(lazy(() => import('./commands/docs').then(m => m.default))); + + await defaultCommand.parseAsync(args, { from: 'user' }); + }, + }); + reg.addCommand({ + path: ['config', 'docs'], + description: 'Browse the configuration reference documentation', + execute: async ({ args }) => { + const argv = await yargs + .options({ + package: { type: 'string' }, + }) + .help() + .parse(args); + const m = await import('./commands/docs'); + await m.default(argv); + }, + }); + reg.addCommand({ + path: ['config:print'], + description: 'Print the app configuration for the current package', + execute: async ({ args }) => { + const argv = await yargs + .options({ + package: { type: 'string' }, + lax: { type: 'boolean' }, + frontend: { type: 'boolean' }, + 'with-secrets': { type: 'boolean' }, + format: { type: 'string' }, + config: { type: 'string', array: true }, + }) + .help() + .parse(args); + const m = await import('./commands/print'); + await m.default(argv); + }, + }); + reg.addCommand({ + path: ['config:check'], + description: + 'Validate that the given configuration loads and matches schema', + execute: async ({ args }) => { + const argv = await yargs + .options({ + package: { type: 'string' }, + lax: { type: 'boolean' }, + frontend: { type: 'boolean' }, + deprecated: { type: 'boolean' }, + strict: { type: 'boolean', required: true }, + config: { + type: 'string', + array: true, + default: [], + }, + }) + .help() + .parse(args); + const m = await import('./commands/validate'); + await m.default(argv); + }, + }); + }, +}); diff --git a/packages/cli/src/commands/config/docs.ts b/packages/cli/src/modules/config/commands/docs.ts similarity index 96% rename from packages/cli/src/commands/config/docs.ts rename to packages/cli/src/modules/config/commands/docs.ts index 98e683e87c..25e3a864f4 100644 --- a/packages/cli/src/commands/config/docs.ts +++ b/packages/cli/src/modules/config/commands/docs.ts @@ -19,7 +19,7 @@ import { mergeConfigSchemas } from '@backstage/config-loader'; import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import openBrowser from 'react-dev-utils/openBrowser'; -import { loadCliConfig } from '../../lib/config'; +import { loadCliConfig } from '../lib/config'; const DOCS_URL = 'https://config.backstage.io'; diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/modules/config/commands/print.ts similarity index 97% rename from packages/cli/src/commands/config/print.ts rename to packages/cli/src/modules/config/commands/print.ts index 9ef725ebf8..98833a7946 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/modules/config/commands/print.ts @@ -17,7 +17,7 @@ import { OptionValues } from 'commander'; import { stringify as stringifyYaml } from 'yaml'; import { AppConfig, ConfigReader } from '@backstage/config'; -import { loadCliConfig } from '../../lib/config'; +import { loadCliConfig } from '../lib/config'; import { ConfigSchema, ConfigVisibility } from '@backstage/config-loader'; export default async (opts: OptionValues) => { diff --git a/packages/cli/src/commands/config/schema.ts b/packages/cli/src/modules/config/commands/schema.ts similarity index 97% rename from packages/cli/src/commands/config/schema.ts rename to packages/cli/src/modules/config/commands/schema.ts index 08bc46d1cc..73eb487d60 100644 --- a/packages/cli/src/commands/config/schema.ts +++ b/packages/cli/src/modules/config/commands/schema.ts @@ -17,7 +17,7 @@ import { OptionValues } from 'commander'; import { JSONSchema7 as JSONSchema } from 'json-schema'; import { stringify as stringifyYaml } from 'yaml'; -import { loadCliConfig } from '../../lib/config'; +import { loadCliConfig } from '../lib/config'; import { JsonObject } from '@backstage/types'; import { mergeConfigSchemas } from '@backstage/config-loader'; diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/modules/config/commands/validate.ts similarity index 94% rename from packages/cli/src/commands/config/validate.ts rename to packages/cli/src/modules/config/commands/validate.ts index c81d82832e..ac0b9d15e0 100644 --- a/packages/cli/src/commands/config/validate.ts +++ b/packages/cli/src/modules/config/commands/validate.ts @@ -15,7 +15,7 @@ */ import { OptionValues } from 'commander'; -import { loadCliConfig } from '../../lib/config'; +import { loadCliConfig } from '../lib/config'; export default async (opts: OptionValues) => { await loadCliConfig({ diff --git a/packages/cli/src/modules/config/index.ts b/packages/cli/src/modules/config/index.ts new file mode 100644 index 0000000000..0c6a79577d --- /dev/null +++ b/packages/cli/src/modules/config/index.ts @@ -0,0 +1,87 @@ +/* + * 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 { Command } from 'commander'; +import { lazy } from '../../lib/lazy'; + +export 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 registerCommands(program: Command) { + program + .command('config:docs') + .option( + '--package ', + 'Only include the schema that applies to the given package', + ) + .description('Browse the configuration reference documentation') + .action(lazy(() => import('./commands/docs').then(m => m.default))); + + program + .command('config:print') + .option( + '--package ', + 'Only load config schema that applies to the given package', + ) + .option('--lax', 'Do not require environment variables to be set') + .option('--frontend', 'Print only the frontend configuration') + .option('--with-secrets', 'Include secrets in the printed configuration') + .option( + '--format ', + 'Format to print the configuration in, either json or yaml [yaml]', + ) + .option(...configOption) + .description('Print the app configuration for the current package') + .action(lazy(() => import('./commands/print').then(m => m.default))); + + program + .command('config:check') + .option( + '--package ', + 'Only load config schema that applies to the given package', + ) + .option('--lax', 'Do not require environment variables to be set') + .option('--frontend', 'Only validate the frontend configuration') + .option('--deprecated', 'Output deprecated configuration settings') + .option( + '--strict', + 'Enable strict config validation, forbidding errors and unknown keys', + ) + .option(...configOption) + .description( + 'Validate that the given configuration loads and matches schema', + ) + .action(lazy(() => import('./commands/validate').then(m => m.default))); + + program + .command('config:schema') + .option( + '--package ', + 'Only output config schema that applies to the given package', + ) + .option( + '--format ', + 'Format to print the schema in, either json or yaml [yaml]', + ) + .option('--merge', 'Print the config schemas merged', true) + .option('--no-merge', 'Print the config schemas not merged') + .description('Print configuration schema') + .action(lazy(() => import('./commands/schema').then(m => m.default))); +} diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/modules/config/lib/config.ts similarity index 99% rename from packages/cli/src/lib/config.ts rename to packages/cli/src/modules/config/lib/config.ts index aab16a8670..afed53af6f 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/modules/config/lib/config.ts @@ -16,7 +16,7 @@ import { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; import { AppConfig, ConfigReader } from '@backstage/config'; -import { paths } from './paths'; +import { paths } from '../../../lib/paths'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; diff --git a/packages/cli/src/wiring/CliInitializer.ts b/packages/cli/src/wiring/CliInitializer.ts new file mode 100644 index 0000000000..9fe8bb8084 --- /dev/null +++ b/packages/cli/src/wiring/CliInitializer.ts @@ -0,0 +1,138 @@ +/* + * 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 { CommandGraph } from './CommandGraph'; +import { CliFeature, InternalCliFeature, InternalCliPlugin } from './types'; +import { CommandRegistry } from './CommandRegistry'; +import { program } from 'commander'; +import { version } from '../lib/version'; +import chalk from 'chalk'; +import { exitWithError } from '../lib/errors'; +import { assertError } from '@backstage/errors'; + +type UninitializedFeature = CliFeature | Promise; + +export class CliInitializer { + private graph = new CommandGraph(); + private commandRegistry = new CommandRegistry(this.graph); + #uninitiazedFeatures: Promise[] = []; + + add(module: UninitializedFeature) { + this.#uninitiazedFeatures.push(Promise.resolve(module)); + } + + async #register(feature: CliFeature) { + if (isCliPlugin(feature)) { + await feature.init(this.commandRegistry); + } else { + throw new Error(`Unsupported feature type: ${feature.$$type}`); + } + } + + async #doInit() { + const features = await Promise.all(this.#uninitiazedFeatures); + for (const feature of features) { + await this.#register(feature); + } + } + + /** + * Actually parse argv and pass it to the command. + */ + async run() { + await this.#doInit(); + program + .name('backstage-cli') + .version(version) + .allowUnknownOption(true) + .allowExcessArguments(true); + + const queue = this.graph.atDepth(0).map(node => ({ + node, + argParser: program, + })); + while (queue.length) { + const { node, argParser } = queue.shift()!; + if (node.$$type === '@tree/root') { + const treeParser = argParser + .command(`${node.name} [command]`) + .description(node.name); + + queue.push( + ...node.children.map(child => ({ + node: child, + argParser: treeParser, + })), + ); + } else { + argParser + .command(node.name) + .description(node.command.description) + .helpOption(false) + .allowUnknownOption(true) + .allowExcessArguments(true) + .action(async () => { + try { + await node.command.execute({ + args: program.parseOptions(process.argv).unknown, + }); + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }); + } + } + program.on('command:*', () => { + console.log(); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); + console.log(); + program.outputHelp(); + process.exit(1); + }); + + process.on('unhandledRejection', rejection => { + if (rejection instanceof Error) { + exitWithError(rejection); + } else { + exitWithError(new Error(`Unknown rejection: '${rejection}'`)); + } + }); + + program.parse(process.argv); + } +} + +function toInternalCliFeature(feature: CliFeature): InternalCliFeature { + if (feature.$$type !== '@backstage/CliFeature') { + throw new Error(`Invalid CliFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalCliFeature; + if (internal.version !== 'v1') { + throw new Error(`Invalid CliFeature, bad version '${internal.version}'`); + } + return internal; +} + +function isCliPlugin(feature: CliFeature): feature is InternalCliPlugin { + const internal = toInternalCliFeature(feature); + if (internal.featureType === 'plugin') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'plugin' in internal; +} diff --git a/packages/cli/src/wiring/CommandGraph.ts b/packages/cli/src/wiring/CommandGraph.ts new file mode 100644 index 0000000000..a0a2de3305 --- /dev/null +++ b/packages/cli/src/wiring/CommandGraph.ts @@ -0,0 +1,103 @@ +/* + * 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 } from './types'; + +type Node = TreeNode | LeafNode; + +interface TreeNode { + $$type: '@tree/root'; + name: string; + children: TreeNode[]; +} + +interface LeafNode { + $$type: '@tree/leaf'; + name: string; + command: BackstageCommand; +} + +/** + * A sparse graph of commands. + */ +export class CommandGraph { + private graph: Node[] = []; + + /** + * Adds a command to the graph. The graph is sparse, so we use the path to determine the nodes + * to traverse. Only leaf nodes should have a command/action. + */ + add(command: BackstageCommand) { + const path = command.path; + let current = this.graph; + for (let i = 0; i < path.length - 1; i++) { + const name = path[i]; + let next = current.find(n => n.name === name); + if (!next) { + next = { $$type: '@tree/root', name, children: [] }; + current.push(next); + } else if (next.$$type === '@tree/leaf') { + throw new Error( + `Command already exists at path: "${path.slice(0, i).join(' ')}"`, + ); + } + current = next.children; + } + const last = current.find(n => n.name === path[path.length - 1]); + if (last && last.$$type === '@tree/leaf') { + throw new Error( + `Command already exists at path: "${path.slice(0, -1).join(' ')}"`, + ); + } else { + current.push({ + $$type: '@tree/leaf', + name: path[path.length - 1], + command, + }); + } + } + + /** + * Given a path, try to find a command that matches it. + */ + find(path: string[]): BackstageCommand | undefined { + let current = this.graph; + for (let i = 0; i < path.length - 1; i++) { + const name = path[i]; + const next = current.find(n => n.name === name); + if (!next) { + return undefined; + } else if (next.$$type === '@tree/leaf') { + return undefined; + } + current = next.children; + } + const last = current.find(n => n.name === path[path.length - 1]); + if (!last || last.$$type === '@tree/root') { + return undefined; + } + return last?.command; + } + + atDepth(depth: number): Node[] { + let current = this.graph; + for (let i = 0; i < depth; i++) { + current = current.flatMap(n => + n.$$type === '@tree/root' ? n.children : [], + ); + } + return current; + } +} diff --git a/packages/cli/src/wiring/CommandRegistry.ts b/packages/cli/src/wiring/CommandRegistry.ts new file mode 100644 index 0000000000..9e8ff9646c --- /dev/null +++ b/packages/cli/src/wiring/CommandRegistry.ts @@ -0,0 +1,28 @@ +/* + * 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 { CommandGraph } from './CommandGraph'; +import { BackstageCommand } from './types'; + +export class CommandRegistry { + private graph: CommandGraph; + constructor(graph: CommandGraph) { + this.graph = graph; + } + + addCommand(command: BackstageCommand) { + this.graph.add(command); + } +} diff --git a/packages/cli/src/wiring/factory.ts b/packages/cli/src/wiring/factory.ts new file mode 100644 index 0000000000..eec058e180 --- /dev/null +++ b/packages/cli/src/wiring/factory.ts @@ -0,0 +1,32 @@ +/* + * 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 { CommandRegistry } from './CommandRegistry'; +import { InternalCliPlugin } from './types'; + +export function createCliPlugin(options: { + pluginId: string; + init: (registry: CommandRegistry) => Promise; +}): InternalCliPlugin { + return { + id: options.pluginId, + init: options.init, + $$type: '@backstage/CliFeature', + version: 'v1', + featureType: 'plugin', + description: 'A Backstage CLI plugin', + }; +} diff --git a/packages/cli/src/wiring/types.ts b/packages/cli/src/wiring/types.ts new file mode 100644 index 0000000000..d9237a4c3a --- /dev/null +++ b/packages/cli/src/wiring/types.ts @@ -0,0 +1,46 @@ +/* + * 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 { CommandRegistry } from './CommandRegistry'; + +export interface BackstageCommand { + path: string[]; + description: string; + execute: (options: { args: string[] }) => Promise; +} + +export interface CliFeature { + $$type: '@backstage/CliFeature'; +} + +export interface CliPlugin { + id: string; + init: (registry: CommandRegistry) => Promise; + $$type: '@backstage/CliFeature'; +} + +/** + * @public + */ +export interface InternalCliPlugin extends CliFeature { + version: 'v1'; + featureType: 'plugin'; + description: string; + id: string; + init: (registry: CommandRegistry) => Promise; +} + +/** @internal */ +export type InternalCliFeature = InternalCliPlugin;