Add support for experimental commands in the CLI module API
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Internal refactor of the CLI command registration
|
||||
@@ -92,6 +92,124 @@ describe('CliInitializer', () => {
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('should run experimental commands but exclude them from help output', async () => {
|
||||
expect.assertions(3);
|
||||
process.argv = ['node', 'cli', 'secret'];
|
||||
const initializer = new CliInitializer();
|
||||
initializer.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['visible'],
|
||||
description: 'A visible command',
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['secret'],
|
||||
description: 'An experimental command',
|
||||
experimental: true,
|
||||
execute: ({ args }) => {
|
||||
expect(args).toEqual([]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await initializer.run();
|
||||
expect(process.exit).toHaveBeenCalledWith(0);
|
||||
|
||||
process.argv = ['node', 'cli', '--help'];
|
||||
const writeSpy = jest.spyOn(process.stdout, 'write');
|
||||
const initializer2 = new CliInitializer();
|
||||
initializer2.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['visible'],
|
||||
description: 'A visible command',
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['secret'],
|
||||
description: 'An experimental command',
|
||||
experimental: true,
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await initializer2.run();
|
||||
const helpOutput = writeSpy.mock.calls.map(c => c[0]).join('');
|
||||
expect(helpOutput).not.toContain('secret');
|
||||
writeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should hide tree nodes when all children are experimental', async () => {
|
||||
process.argv = ['node', 'cli', '--help'];
|
||||
const writeSpy = jest.spyOn(process.stdout, 'write');
|
||||
const initializer = new CliInitializer();
|
||||
initializer.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['visible'],
|
||||
description: 'A visible command',
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['group', 'alpha'],
|
||||
description: 'First experimental command',
|
||||
experimental: true,
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['group', 'beta'],
|
||||
description: 'Second experimental command',
|
||||
experimental: true,
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await initializer.run();
|
||||
const helpOutput = writeSpy.mock.calls.map(c => c[0]).join('');
|
||||
expect(helpOutput).toContain('visible');
|
||||
expect(helpOutput).not.toContain('group');
|
||||
writeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should show tree nodes when some children are visible', async () => {
|
||||
process.argv = ['node', 'cli', '--help'];
|
||||
const writeSpy = jest.spyOn(process.stdout, 'write');
|
||||
const initializer = new CliInitializer();
|
||||
initializer.add(
|
||||
createCliPlugin({
|
||||
pluginId: 'test',
|
||||
init: async reg => {
|
||||
reg.addCommand({
|
||||
path: ['group', 'alpha'],
|
||||
description: 'A visible nested command',
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['group', 'beta'],
|
||||
description: 'An experimental nested command',
|
||||
experimental: true,
|
||||
execute: () => Promise.resolve(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
await initializer.run();
|
||||
const helpOutput = writeSpy.mock.calls.map(c => c[0]).join('');
|
||||
expect(helpOutput).toContain('group');
|
||||
writeSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should pass positional args to the subcommand if nested', async () => {
|
||||
expect.assertions(2);
|
||||
process.argv = [
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { CommandGraph } from './CommandGraph';
|
||||
import { CliFeature, OpaqueCliPlugin } from './types';
|
||||
import { BackstageCommand, CliFeature, OpaqueCliPlugin } from './types';
|
||||
import { CommandRegistry } from './CommandRegistry';
|
||||
import { Command } from 'commander';
|
||||
import { version } from './version';
|
||||
@@ -24,6 +24,17 @@ import { exitWithError } from './errors';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { isPromise } from 'node:util/types';
|
||||
|
||||
function isNodeHidden(
|
||||
node:
|
||||
| { $$type: '@tree/leaf'; command: BackstageCommand }
|
||||
| { $$type: '@tree/root'; children: unknown[] },
|
||||
): boolean {
|
||||
if (node.$$type === '@tree/leaf') {
|
||||
return !!node.command.deprecated || !!node.command.experimental;
|
||||
}
|
||||
return node.children.every(child => isNodeHidden(child as any));
|
||||
}
|
||||
|
||||
type UninitializedFeature = CliFeature | Promise<{ default: CliFeature }>;
|
||||
|
||||
export class CliInitializer {
|
||||
@@ -80,7 +91,9 @@ export class CliInitializer {
|
||||
const { node, argParser } = queue.shift()!;
|
||||
if (node.$$type === '@tree/root') {
|
||||
const treeParser = argParser
|
||||
.command(`${node.name} [command]`)
|
||||
.command(`${node.name} [command]`, {
|
||||
hidden: isNodeHidden(node),
|
||||
})
|
||||
.description(node.name);
|
||||
|
||||
queue.push(
|
||||
@@ -91,7 +104,9 @@ export class CliInitializer {
|
||||
);
|
||||
} else {
|
||||
argParser
|
||||
.command(node.name, { hidden: !!node.command.deprecated })
|
||||
.command(node.name, {
|
||||
hidden: !!node.command.deprecated || !!node.command.experimental,
|
||||
})
|
||||
.description(node.command.description)
|
||||
.helpOption(false)
|
||||
.allowUnknownOption(true)
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface BackstageCommand {
|
||||
path: string[];
|
||||
description: string;
|
||||
deprecated?: boolean;
|
||||
experimental?: boolean;
|
||||
execute:
|
||||
| CommandExecuteFn
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user