add yargs examples - added a config docs to test nesting logic
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -20,7 +20,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"bin": {
|
||||
"backstage-cli": "bin/backstage-cli"
|
||||
"backstage-cli": "bin/backstage-cli",
|
||||
"backstage-cli-alpha": "bin/backstage-cli-alpha"
|
||||
},
|
||||
"files": [
|
||||
"asset-types",
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
*/
|
||||
|
||||
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();
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { createCliPlugin } from '../../wiring/factory';
|
||||
import yargs from 'yargs';
|
||||
|
||||
export default createCliPlugin({
|
||||
pluginId: 'config',
|
||||
@@ -23,45 +22,96 @@ export default createCliPlugin({
|
||||
reg.addCommand({
|
||||
path: ['config:docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
schema: z.object({
|
||||
package: z.string().optional(),
|
||||
}),
|
||||
execute: async options => {
|
||||
const m = await import('./commands/docs');
|
||||
await m.default(options);
|
||||
execute: async ({ args }) => {
|
||||
await yargs
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
})
|
||||
.command(
|
||||
'$0',
|
||||
'',
|
||||
async () => {},
|
||||
async argv => {
|
||||
const m = await import('./commands/docs');
|
||||
await m.default(argv);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse(args);
|
||||
},
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config', 'docs'],
|
||||
description: 'Browse the configuration reference documentation',
|
||||
execute: async ({ args }) => {
|
||||
await yargs
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
})
|
||||
.command(
|
||||
'$0',
|
||||
'',
|
||||
async () => {},
|
||||
async argv => {
|
||||
const m = await import('./commands/docs');
|
||||
await m.default(argv);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse(args);
|
||||
},
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:print'],
|
||||
description: 'Print the app configuration for the current package',
|
||||
schema: z.object({
|
||||
package: z.string().optional(),
|
||||
lax: z.boolean().optional(),
|
||||
frontend: z.boolean().optional(),
|
||||
'with-secrets': z.boolean().optional(),
|
||||
format: z.enum(['json', 'yaml']).optional(),
|
||||
config: z.array(z.string()).optional(),
|
||||
}),
|
||||
execute: async options => {
|
||||
const m = await import('./commands/print');
|
||||
await m.default(options);
|
||||
execute: async ({ args }) => {
|
||||
await yargs
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
'with-secrets': { type: 'boolean' },
|
||||
format: { type: 'string' },
|
||||
config: { type: 'string', array: true },
|
||||
})
|
||||
.command(
|
||||
'$0',
|
||||
'',
|
||||
async () => {},
|
||||
async argv => {
|
||||
const m = await import('./commands/print');
|
||||
await m.default(argv);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse(args);
|
||||
},
|
||||
});
|
||||
reg.addCommand({
|
||||
path: ['config:check'],
|
||||
description:
|
||||
'Validate that the given configuration loads and matches schema',
|
||||
schema: z.object({
|
||||
package: z.string().optional(),
|
||||
lax: z.boolean().optional(),
|
||||
frontend: z.boolean().optional(),
|
||||
deprecated: z.boolean().optional(),
|
||||
strict: z.boolean().optional(),
|
||||
config: z.array(z.string()).optional(),
|
||||
}),
|
||||
execute: async options => {
|
||||
const m = await import('./commands/validate');
|
||||
await m.default(options);
|
||||
execute: async ({ args }) => {
|
||||
await yargs
|
||||
.options({
|
||||
package: { type: 'string' },
|
||||
lax: { type: 'boolean' },
|
||||
frontend: { type: 'boolean' },
|
||||
deprecated: { type: 'boolean' },
|
||||
strict: { type: 'boolean' },
|
||||
config: { type: 'string', array: true },
|
||||
})
|
||||
.command(
|
||||
'$0',
|
||||
'',
|
||||
async () => {},
|
||||
async argv => {
|
||||
const m = await import('./commands/validate');
|
||||
await m.default(argv);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.parse(args);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -18,10 +18,21 @@ import yargs from 'yargs';
|
||||
import { CommandGraph } from './CommandGraph';
|
||||
import { CliFeature, InternalCliFeature, InternalCliPlugin } from './types';
|
||||
import { CommandRegistry } from './CommandRegistry';
|
||||
import chalk from 'chalk';
|
||||
|
||||
type UninitializedFeature = CliFeature | Promise<CliFeature>;
|
||||
|
||||
function checkCommands(
|
||||
nestedYargs: yargs.Argv,
|
||||
argv: Awaited<yargs.Argv['argv']>,
|
||||
numRequired: number,
|
||||
) {
|
||||
if (argv._.length < numRequired) {
|
||||
nestedYargs.showHelp();
|
||||
} else {
|
||||
// check for unknown command
|
||||
}
|
||||
}
|
||||
|
||||
export class CliInitializer {
|
||||
private graph = new CommandGraph();
|
||||
private commandRegistry = new CommandRegistry(this.graph);
|
||||
@@ -52,31 +63,60 @@ export class CliInitializer {
|
||||
async run() {
|
||||
await this.#doInit();
|
||||
|
||||
const {
|
||||
_: commandName,
|
||||
$0: binaryName,
|
||||
...options
|
||||
} = await yargs(process.argv.slice(2)).parse();
|
||||
const command = this.graph.find(commandName.map(String));
|
||||
if (!command) {
|
||||
console.error(chalk.red(`Command not found: "${commandName.join(' ')}"`));
|
||||
const possibleCommands = this.graph.atDepth(commandName.length - 1);
|
||||
if (possibleCommands.length > 0) {
|
||||
console.log('Available commands:');
|
||||
for (const node of possibleCommands) {
|
||||
let text = `\t${node.name}`;
|
||||
if (node.$$type === '@tree/root') {
|
||||
text += ' [command]';
|
||||
} else {
|
||||
text += ` [options]\t\t${node.command.description}`;
|
||||
}
|
||||
console.log(text);
|
||||
}
|
||||
const root = yargs.usage('usage: $0 <command>').wrap(null).help('help');
|
||||
const rootArgv = process.argv.slice(2);
|
||||
|
||||
const queue = this.graph.atDepth(0).map(node => ({
|
||||
node,
|
||||
argParser: root,
|
||||
depth: 0,
|
||||
}));
|
||||
while (queue.length) {
|
||||
const { node, argParser, depth } = queue.shift()!;
|
||||
if (node.$$type === '@tree/root') {
|
||||
let commandYargs = undefined;
|
||||
argParser
|
||||
.recommendCommands()
|
||||
.demandCommand()
|
||||
.command(node.name, node.name, async nestedYargs => {
|
||||
commandYargs = nestedYargs;
|
||||
nestedYargs.usage(`usage: $0 ${node.name}`).wrap(null).help('help');
|
||||
checkCommands(
|
||||
nestedYargs,
|
||||
await nestedYargs.argv,
|
||||
node.children.length,
|
||||
);
|
||||
});
|
||||
|
||||
queue.push(
|
||||
...node.children.map(child => ({
|
||||
node: child,
|
||||
argParser,
|
||||
depth: depth + 1,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
argParser.command(
|
||||
node.name,
|
||||
node.command.description,
|
||||
async nestedYargs => {
|
||||
nestedYargs.help(false);
|
||||
},
|
||||
async argv => {
|
||||
await node.command.execute({
|
||||
args: process.argv.slice(2 + depth + 1),
|
||||
});
|
||||
checkCommands(argParser, argv, 1);
|
||||
},
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
const parsedOptions = command.schema.parse(options);
|
||||
await command?.execute(parsedOptions);
|
||||
const argv = await root
|
||||
.demandCommand()
|
||||
.recommendCommands()
|
||||
.strictCommands()
|
||||
.parse(rootArgv);
|
||||
checkCommands(root, argv, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ZodRawShape } from 'zod';
|
||||
import { BackstageCommand } from './types';
|
||||
|
||||
type Node<T extends ZodRawShape = ZodRawShape> = TreeNode | LeafNode<T>;
|
||||
type Node = TreeNode | LeafNode;
|
||||
|
||||
interface TreeNode {
|
||||
$$type: '@tree/root';
|
||||
@@ -24,10 +23,10 @@ interface TreeNode {
|
||||
children: TreeNode[];
|
||||
}
|
||||
|
||||
interface LeafNode<T extends ZodRawShape> {
|
||||
interface LeafNode {
|
||||
$$type: '@tree/leaf';
|
||||
name: string;
|
||||
command: BackstageCommand<T>;
|
||||
command: BackstageCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +39,7 @@ export class CommandGraph {
|
||||
* 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<T extends ZodRawShape>(command: BackstageCommand<T>) {
|
||||
add(command: BackstageCommand) {
|
||||
const path = command.path;
|
||||
let current = this.graph;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
@@ -56,7 +55,7 @@ export class CommandGraph {
|
||||
}
|
||||
current = next.children;
|
||||
}
|
||||
const last = current.find(n => n.name === path[path.length - 1]) as Node<T>;
|
||||
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(' ')}"`,
|
||||
@@ -66,14 +65,14 @@ export class CommandGraph {
|
||||
$$type: '@tree/leaf',
|
||||
name: path[path.length - 1],
|
||||
command,
|
||||
} as Node<any>);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a path, try to find a command that matches it.
|
||||
*/
|
||||
find<T extends ZodRawShape>(path: string[]): BackstageCommand<T> | undefined {
|
||||
find(path: string[]): BackstageCommand | undefined {
|
||||
let current = this.graph;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const name = path[i];
|
||||
@@ -85,20 +84,20 @@ export class CommandGraph {
|
||||
}
|
||||
current = next.children;
|
||||
}
|
||||
const last = current.find(n => n.name === path[path.length - 1]) as Node<T>;
|
||||
const last = current.find(n => n.name === path[path.length - 1]);
|
||||
if (!last || last.$$type === '@tree/root') {
|
||||
return undefined;
|
||||
}
|
||||
return last?.command;
|
||||
}
|
||||
|
||||
atDepth<T extends ZodRawShape>(depth: number): Node<T>[] {
|
||||
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 as Node<T>[];
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ZodRawShape } from 'zod';
|
||||
import { CommandGraph } from './CommandGraph';
|
||||
import { BackstageCommand } from './types';
|
||||
|
||||
@@ -24,7 +22,7 @@ export class CommandRegistry {
|
||||
this.graph = graph;
|
||||
}
|
||||
|
||||
addCommand<T extends ZodRawShape>(command: BackstageCommand<T>) {
|
||||
addCommand(command: BackstageCommand) {
|
||||
this.graph.add(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,12 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ZodObject, ZodRawShape } from 'zod';
|
||||
import { CommandRegistry } from './CommandRegistry';
|
||||
|
||||
export interface BackstageCommand<T extends ZodRawShape = ZodRawShape> {
|
||||
export interface BackstageCommand {
|
||||
path: string[];
|
||||
description: string;
|
||||
schema: ZodObject<T>;
|
||||
execute: (options: T) => Promise<void>;
|
||||
execute: (options: { args: string[] }) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface CliFeature {
|
||||
|
||||
Reference in New Issue
Block a user