From edb402e298e2d7b05d4113333dd90da09adf8b62 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Thu, 30 Nov 2023 14:51:37 -0600 Subject: [PATCH 01/16] Init WIP implementation of BaseCommand and CommandRegistry classes --- src/lib/command.ts | 111 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/lib/command.ts diff --git a/src/lib/command.ts b/src/lib/command.ts new file mode 100644 index 000000000..533a7301c --- /dev/null +++ b/src/lib/command.ts @@ -0,0 +1,111 @@ +/** + * Base Command from which every subcommand should inherit. + * + * @class BaseCommand + */ +class BaseCommand { + protected name: string; + protected usage: any = { + description: 'Base command', + examples: [ + { + description: 'Example 1', + usage: 'vip example arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example --named=arg1 --also=arg2', + }, + ], + }; + + constructor( private readonly name: string ) {} + + protected trackEvent( eventName: string, data: any ): void { + // Send tracking information to trackEvent + } + + public run( ...args: any[] ): void { + // Invoke the command and send tracking information + try { + this.trackEvent( `${ this.name }_execute`, args ); + this.execute( ...args ); + this.trackEvent( `${ this.name }_success`, args ); + } catch ( error ) { + this.trackEvent( `${ this.name }_error`, { error } ); + throw error; + } + } + + protected execute( ...args: any[] ): void { + // Implement the command logic in the derived classes + } + + protected getName(): string { + return this.name; + } + + protected getUsage(): any { + return this.usage; + } +} + +/** + * The registry that stores/invokes all the commands. + * + * The main entry point will call it. + * + * @class CommandRegistry + */ +class CommandRegistry { + private static instance: CommandRegistry; + private commands: Map< string, BaseCommand >; + + private constructor() { + this.commands = new Map< string, BaseCommand >(); + } + + public static getInstance(): CommandRegistry { + if ( ! CommandRegistry.instance ) { + CommandRegistry.instance = new CommandRegistry(); + } + return CommandRegistry.instance; + } + + public registerCommand( command: BaseCommand ): void { + this.commands.set( command.getName(), command ); + } + + public invokeCommand( commandName: string, ...args: any[] ): void { + const command = this.commands.get( commandName ); + if ( command ) { + command.run( ...args ); + } else { + throw new Error( `Command '${ commandName }' not found.` ); + } + } + + public getCommands(): Map< string, BaseCommand > { + return this.commands; + } +} + +class ExampleCommand extends BaseCommand { + constructor() { + super( 'example' ); + } + + protected execute( ...args: any[] ): void { + console.log( this.getName(), args ); + } +} + + +const registry = CommandRegistry.getInstance(); +registry.registerCommand( new ExampleCommand() ); + +for ( const [ key, command ] of registry.getCommands() ) { + console.log( `${key}`, command.getUsage() ); +} + +registry.invokeCommand( 'example', 'arg1', 'arg2', { named: 'arg' } ); From 655cfb9208c62dbb363809e243c35e71c94ad6ca Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Thu, 30 Nov 2023 15:10:11 -0600 Subject: [PATCH 02/16] getUsage method should be public --- src/lib/command.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/command.ts b/src/lib/command.ts index 533a7301c..d822297ac 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -45,7 +45,7 @@ class BaseCommand { return this.name; } - protected getUsage(): any { + public getUsage(): any { return this.usage; } } @@ -105,7 +105,7 @@ const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); for ( const [ key, command ] of registry.getCommands() ) { - console.log( `${key}`, command.getUsage() ); + console.log( `${ key }`, command.getUsage() ); } registry.invokeCommand( 'example', 'arg1', 'arg2', { named: 'arg' } ); From f458f7e70745309d9619828b481583504cabc601 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Thu, 30 Nov 2023 15:16:12 -0600 Subject: [PATCH 03/16] Remove duplicate name --- src/lib/command.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/command.ts b/src/lib/command.ts index d822297ac..118693a05 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -4,7 +4,6 @@ * @class BaseCommand */ class BaseCommand { - protected name: string; protected usage: any = { description: 'Base command', examples: [ From a50222f0b68018fc71b27c78db8116d9a1569fd9 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Thu, 30 Nov 2023 15:16:46 -0600 Subject: [PATCH 04/16] change visibility on getName method --- src/lib/command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/command.ts b/src/lib/command.ts index 118693a05..553d7e0b1 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -40,7 +40,7 @@ class BaseCommand { // Implement the command logic in the derived classes } - protected getName(): string { + public getName(): string { return this.name; } From 5017334aa42d60c5cc1cb08ffc4b4fe49b26eba1 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Fri, 1 Dec 2023 11:17:46 +0200 Subject: [PATCH 05/16] fix: TypeScript --- src/lib/command.ts | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/lib/command.ts b/src/lib/command.ts index 553d7e0b1..781726e01 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -1,10 +1,20 @@ +export interface CommandExample { + description: string; + usage: string; +} + +export interface CommandUsage { + description: string; + examples: CommandExample[]; +} + /** * Base Command from which every subcommand should inherit. * * @class BaseCommand */ -class BaseCommand { - protected usage: any = { +export abstract class BaseCommand { + protected readonly usage: CommandUsage = { description: 'Base command', examples: [ { @@ -20,31 +30,32 @@ class BaseCommand { constructor( private readonly name: string ) {} - protected trackEvent( eventName: string, data: any ): void { + protected trackEvent( eventName: string, data: unknown[] ): void { // Send tracking information to trackEvent } - public run( ...args: any[] ): void { + public run( ...args: unknown[] ): void { // Invoke the command and send tracking information try { this.trackEvent( `${ this.name }_execute`, args ); this.execute( ...args ); this.trackEvent( `${ this.name }_success`, args ); } catch ( error ) { - this.trackEvent( `${ this.name }_error`, { error } ); + const err = + error instanceof Error ? error : new Error( error?.toString() ?? 'Unknown error' ); + + this.trackEvent( `${ this.name }_error`, [ err ] ); throw error; } } - protected execute( ...args: any[] ): void { - // Implement the command logic in the derived classes - } + protected abstract execute( ...args: unknown[] ): void; public getName(): string { return this.name; } - public getUsage(): any { + public getUsage(): CommandUsage { return this.usage; } } @@ -58,7 +69,7 @@ class BaseCommand { */ class CommandRegistry { private static instance: CommandRegistry; - private commands: Map< string, BaseCommand >; + private readonly commands: Map< string, BaseCommand >; private constructor() { this.commands = new Map< string, BaseCommand >(); @@ -75,7 +86,7 @@ class CommandRegistry { this.commands.set( command.getName(), command ); } - public invokeCommand( commandName: string, ...args: any[] ): void { + public invokeCommand( commandName: string, ...args: unknown[] ): void { const command = this.commands.get( commandName ); if ( command ) { command.run( ...args ); @@ -94,12 +105,11 @@ class ExampleCommand extends BaseCommand { super( 'example' ); } - protected execute( ...args: any[] ): void { + protected execute( ...args: unknown[] ): void { console.log( this.getName(), args ); } } - const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); From e0917b5368f376c6f762365f96c9a92159356049 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Fri, 1 Dec 2023 13:04:18 -0600 Subject: [PATCH 06/16] Second pass: implement basic commander integration, clean up code structure --- npm-shrinkwrap.json | 32 +++++--- package.json | 1 + src/commands/example-command.ts | 19 +++++ src/lib/base-command.ts | 59 ++++++++++++++ src/lib/command-registry.ts | 41 ++++++++++ src/lib/command.ts | 131 +++++++++++--------------------- 6 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 src/commands/example-command.ts create mode 100644 src/lib/base-command.ts create mode 100644 src/lib/command-registry.ts diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 85eed0afc..d0f91811f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -19,6 +19,7 @@ "check-disk-space": "3.4.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", + "commander": "^11.1.0", "configstore": "5.0.1", "copy-dir": "0.4.0", "debug": "4.3.4", @@ -406,6 +407,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@babel/cli/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5632,12 +5642,11 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "engines": { - "node": ">= 6" + "node": ">=16" } }, "node_modules/comment-parser": { @@ -14036,6 +14045,12 @@ "slash": "^2.0.0" }, "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -17876,10 +17891,9 @@ } }, "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" }, "comment-parser": { "version": "1.4.0", diff --git a/package.json b/package.json index d1a0fdce5..39282bd73 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "check-disk-space": "3.4.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", + "commander": "^11.1.0", "configstore": "5.0.1", "copy-dir": "0.4.0", "debug": "4.3.4", diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts new file mode 100644 index 000000000..f31ff2377 --- /dev/null +++ b/src/commands/example-command.ts @@ -0,0 +1,19 @@ +import { BaseVIPCommand } from '../lib/base-command'; + +export class ExampleCommand extends BaseVIPCommand { + protected readonly commandOptions: CommandOption[] = [ + { + name: '--slug , -s ', + description: 'An env slug', + type: 'string', + }, + ]; + + constructor() { + super( 'example' ); + } + + protected execute( opts, ...args: unknown[] ): void { + console.log( this.getName(), opts, args ); + } +} diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts new file mode 100644 index 000000000..34725887d --- /dev/null +++ b/src/lib/base-command.ts @@ -0,0 +1,59 @@ +export abstract class BaseVIPCommand { + protected readonly commandOptions: CommandOption[] = [ + { + name: '--debug', + alias: '-d', + description: 'Show debug', + type: 'boolean', + }, + ]; + + protected readonly usage: CommandUsage = { + description: 'Base command', + examples: [ + { + description: 'Example 1', + usage: 'vip example arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example --named=arg1 --also=arg2', + }, + ], + }; + + constructor( private readonly name: string ) {} + + protected trackEvent( eventName: string, data: unknown[] ): void { + // Send tracking information to trackEvent + } + + public run( ...args: unknown[] ): void { + // Invoke the command and send tracking information + try { + this.trackEvent( `${ this.name }_execute`, args ); + this.execute( ...args ); + this.trackEvent( `${ this.name }_success`, args ); + } catch ( error ) { + const err = + error instanceof Error ? error : new Error( error?.toString() ?? 'Unknown error' ); + + this.trackEvent( `${ this.name }_error`, [ err ] ); + throw error; + } + } + + protected abstract execute( ...args: unknown[] ): void; + + public getName(): string { + return this.name; + } + + public getUsage(): CommandUsage { + return this.usage; + } + + public getOptions(): CommandOption[] { + return this.commandOptions; + } +} diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts new file mode 100644 index 000000000..a23199c5d --- /dev/null +++ b/src/lib/command-registry.ts @@ -0,0 +1,41 @@ +/** + * The registry that stores/invokes all the commands. + * + * The main entry point will call it. + * + * @class CommandRegistry + */ +import { BaseVIPCommand } from './base-command'; + +export class CommandRegistry { + private static instance: CommandRegistry; + private readonly commands: Map< string, BaseVIPCommand >; + + private constructor() { + this.commands = new Map< string, BaseVIPCommand >(); + } + + public static getInstance(): CommandRegistry { + if ( ! CommandRegistry.instance ) { + CommandRegistry.instance = new CommandRegistry(); + } + return CommandRegistry.instance; + } + + public registerCommand( command: BaseVIPCommand ): void { + this.commands.set( command.getName(), command ); + } + + public invokeCommand( commandName: string, ...args: unknown[] ): void { + const command = this.commands.get( commandName ); + if ( command ) { + command.run( ...args ); + } else { + throw new Error( `Command '${ commandName }' not found.` ); + } + } + + public getCommands(): Map< string, BaseVIPCommand > { + return this.commands; + } +} diff --git a/src/lib/command.ts b/src/lib/command.ts index 781726e01..57caa2d70 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -1,3 +1,8 @@ +import { Command } from 'commander'; + +import { BaseVIPCommand } from './base-command'; +import { CommandRegistry } from './command-registry'; +import { ExampleCommand } from '../commands/example-command'; export interface CommandExample { description: string; usage: string; @@ -8,113 +13,63 @@ export interface CommandUsage { examples: CommandExample[]; } +export interface CommandOption { + name: string; + alias?: string; + description: string; + type: 'string' | 'number' | 'boolean'; + required?: boolean; +} + +export interface CommandArgument { + name: string; + description: string; + type: 'string' | 'number' | 'boolean'; + required?: boolean; +} /** * Base Command from which every subcommand should inherit. * * @class BaseCommand */ -export abstract class BaseCommand { - protected readonly usage: CommandUsage = { - description: 'Base command', - examples: [ - { - description: 'Example 1', - usage: 'vip example arg1 arg2', - }, - { - description: 'Example 2', - usage: 'vip example --named=arg1 --also=arg2', - }, - ], - }; - constructor( private readonly name: string ) {} - protected trackEvent( eventName: string, data: unknown[] ): void { - // Send tracking information to trackEvent - } - public run( ...args: unknown[] ): void { - // Invoke the command and send tracking information - try { - this.trackEvent( `${ this.name }_execute`, args ); - this.execute( ...args ); - this.trackEvent( `${ this.name }_success`, args ); - } catch ( error ) { - const err = - error instanceof Error ? error : new Error( error?.toString() ?? 'Unknown error' ); - this.trackEvent( `${ this.name }_error`, [ err ] ); - throw error; - } +const makeVIPCommand = ( command: BaseVIPCommand ): Command => { + const usage = command.getUsage(); + const options = command.getOptions(); + const name = command.getName(); + const cmd = new Command( name ).description( usage.description ); + for ( const option of options ) { + cmd.option( option.name, option.description ); } - protected abstract execute( ...args: unknown[] ): void; + cmd.action( ( ...args ) => { + registry.invokeCommand( name, ...args ); + } ); + cmd.configureHelp( { showGlobalOptions: true } ) + return cmd; +}; - public getName(): string { - return this.name; - } +const program = new Command(); - public getUsage(): CommandUsage { - return this.usage; - } -} +const baseVIPCommand = new BaseVIPCommand( 'vip' ); -/** - * The registry that stores/invokes all the commands. - * - * The main entry point will call it. - * - * @class CommandRegistry - */ -class CommandRegistry { - private static instance: CommandRegistry; - private readonly commands: Map< string, BaseCommand >; +program + .name( 'vip' ) + .description( 'WPVIP CLI' ) + .version( '3.0.0' ) + .configureHelp( { showGlobalOptions: true } ); - private constructor() { - this.commands = new Map< string, BaseCommand >(); - } - - public static getInstance(): CommandRegistry { - if ( ! CommandRegistry.instance ) { - CommandRegistry.instance = new CommandRegistry(); - } - return CommandRegistry.instance; - } - - public registerCommand( command: BaseCommand ): void { - this.commands.set( command.getName(), command ); - } - - public invokeCommand( commandName: string, ...args: unknown[] ): void { - const command = this.commands.get( commandName ); - if ( command ) { - command.run( ...args ); - } else { - throw new Error( `Command '${ commandName }' not found.` ); - } - } - - public getCommands(): Map< string, BaseCommand > { - return this.commands; - } -} - -class ExampleCommand extends BaseCommand { - constructor() { - super( 'example' ); - } - - protected execute( ...args: unknown[] ): void { - console.log( this.getName(), args ); - } +for ( const option of baseVIPCommand.getOptions() ) { + program.option( option.name, option.description ); } const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); for ( const [ key, command ] of registry.getCommands() ) { - console.log( `${ key }`, command.getUsage() ); + program.addCommand( makeVIPCommand( command ) ); } - -registry.invokeCommand( 'example', 'arg1', 'arg2', { named: 'arg' } ); +program.parse( process.argv ); From 91d0f65e366208a1d975b330c7e0d8f540b7c64c Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Tue, 5 Dec 2023 18:48:50 -0600 Subject: [PATCH 07/16] WIP --- src/commands/example-command.ts | 53 +++++++++++++++++++++++++++++++-- src/lib/base-command.ts | 31 ++++++++++++++++++- src/lib/command-registry.ts | 1 + src/lib/command.ts | 46 +++++++++++++--------------- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts index f31ff2377..14b25010b 100644 --- a/src/commands/example-command.ts +++ b/src/commands/example-command.ts @@ -1,19 +1,68 @@ import { BaseVIPCommand } from '../lib/base-command'; +import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; +import { CommandRegistry } from '../lib/command-registry'; + +const registry = CommandRegistry.getInstance(); export class ExampleCommand extends BaseVIPCommand { + protected readonly name: string = 'example'; + protected readonly usage: CommandUsage = { + description: 'Example command', + examples: [ + { + description: 'Example 1', + usage: 'vip example arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example --named=arg1 --also=arg2', + }, + ], + }; + protected readonly commandOptions: CommandOption[] = [ { name: '--slug , -s ', description: 'An env slug', type: 'string', + required: true, }, ]; - constructor() { + protected childCommands: BaseVIPCommand[] = [ new ExampleChildCommand() ]; + + constructor( name ) { super( 'example' ); } protected execute( opts, ...args: unknown[] ): void { - console.log( this.getName(), opts, args ); + console.log( 'parent', this.getName(), opts, args ); + } +} + +export class ExampleChildCommand extends BaseVIPCommand { + protected readonly name: string = 'child'; + protected readonly usage: CommandUsage = { + description: 'Example child command', + examples: [ + { + description: 'Example 1', + usage: 'vip example child arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example child --named=arg1 --also=arg2', + }, + ], + }; + + protected execute( opts, ...args: unknown[] ): void { + console.log( this.getName(), 'what' ); } + // constructor() { + // super( 'example child' ); + // } } + + +// registry.registerCommand( new ExampleCommand() ); diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 34725887d..0fa93b2ce 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,3 +1,6 @@ +import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; +import { CommandRegistry } from './command-registry'; + export abstract class BaseVIPCommand { protected readonly commandOptions: CommandOption[] = [ { @@ -8,6 +11,15 @@ export abstract class BaseVIPCommand { }, ]; + protected readonly commandArguments: CommandArgument[] = [ + { + name: 'app', + description: 'Application id or slug', + type: 'string', + required: true, + }, + ]; + protected readonly usage: CommandUsage = { description: 'Base command', examples: [ @@ -22,7 +34,16 @@ export abstract class BaseVIPCommand { ], }; - constructor( private readonly name: string ) {} + protected childCommands: BaseVIPCommand[] = []; + + constructor( private readonly name: string ) { + const registry = CommandRegistry.getInstance(); + // registry.registerCommand( this ); + + this.childCommands.forEach( command => { + registry.registerCommand( command ); + } ); + } protected trackEvent( eventName: string, data: unknown[] ): void { // Send tracking information to trackEvent @@ -53,7 +74,15 @@ export abstract class BaseVIPCommand { return this.usage; } + public getChildCommands(): BaseVIPCommand[] { + return this.childCommands; + } + public getOptions(): CommandOption[] { return this.commandOptions; } + + public getArguments(): CommandArgument[] { + return this.commandArguments; + } } diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts index a23199c5d..b96bb93f7 100644 --- a/src/lib/command-registry.ts +++ b/src/lib/command-registry.ts @@ -31,6 +31,7 @@ export class CommandRegistry { if ( command ) { command.run( ...args ); } else { + console.log( this.commands ); throw new Error( `Command '${ commandName }' not found.` ); } } diff --git a/src/lib/command.ts b/src/lib/command.ts index 57caa2d70..b388b4673 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -3,49 +3,39 @@ import { Command } from 'commander'; import { BaseVIPCommand } from './base-command'; import { CommandRegistry } from './command-registry'; import { ExampleCommand } from '../commands/example-command'; -export interface CommandExample { - description: string; - usage: string; -} - -export interface CommandUsage { - description: string; - examples: CommandExample[]; -} - -export interface CommandOption { - name: string; - alias?: string; - description: string; - type: 'string' | 'number' | 'boolean'; - required?: boolean; -} +import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; -export interface CommandArgument { - name: string; - description: string; - type: 'string' | 'number' | 'boolean'; - required?: boolean; -} /** * Base Command from which every subcommand should inherit. * * @class BaseCommand */ - - - const makeVIPCommand = ( command: BaseVIPCommand ): Command => { const usage = command.getUsage(); const options = command.getOptions(); const name = command.getName(); + const commandArgs = command.getArguments(); const cmd = new Command( name ).description( usage.description ); + + for( const argument of commandArgs ) { + let name = argument.name; + if ( argument.required ) { + name = `<${ name }>`; + } else { + name = `[${ name }]`; + } + + cmd.argument( name, argument.description ); + } + for ( const option of options ) { cmd.option( option.name, option.description ); } + cmd.action( ( ...args ) => { + console.log( name ); registry.invokeCommand( name, ...args ); } ); cmd.configureHelp( { showGlobalOptions: true } ) @@ -71,5 +61,9 @@ registry.registerCommand( new ExampleCommand() ); for ( const [ key, command ] of registry.getCommands() ) { program.addCommand( makeVIPCommand( command ) ); + for ( const childCommand of command.getChildCommands() ) { + // const instance: BaseVIPCommand = new childCommand(); + program.addCommand( makeVIPCommand( childCommand ) ); + } } program.parse( process.argv ); From ba17e11bc43daa5f49f08409fbcf471fefa4a781 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 6 Dec 2023 10:51:35 +0200 Subject: [PATCH 08/16] Make things work --- npm-shrinkwrap.json | 249 ++++++++++++++++++++++++++++++++ package.json | 1 + src/commands/example-command.ts | 16 +- src/lib/base-command.ts | 13 +- src/lib/command.ts | 24 +-- src/lib/types/commands.ts | 24 +++ 6 files changed, 300 insertions(+), 27 deletions(-) create mode 100644 src/lib/types/commands.ts diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d0f91811f..db60b888d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -132,6 +132,7 @@ "nock": "13.3.8", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "5.0.5", + "ts-node": "^10.9.1", "typescript": "^5.2.2" }, "engines": { @@ -2326,6 +2327,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -3669,6 +3692,30 @@ "node": ">=14.16" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/args": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/args/-/args-5.0.3.tgz", @@ -4223,6 +4270,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4367,6 +4423,12 @@ "node": ">=14" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5760,6 +5822,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6019,6 +6087,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -10426,6 +10503,12 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -12801,6 +12884,49 @@ "node": ">=8" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -13251,6 +13377,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -13846,6 +13978,15 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -15392,6 +15533,27 @@ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "optional": true }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, "@es-joy/jsdoccomment": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", @@ -16415,6 +16577,30 @@ "defer-to-connect": "^2.0.1" } }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "@types/args": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/args/-/args-5.0.3.tgz", @@ -16868,6 +17054,12 @@ "dev": true, "requires": {} }, + "acorn-walk": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -16974,6 +17166,12 @@ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -17988,6 +18186,12 @@ "prompts": "^2.0.1" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -18165,6 +18369,12 @@ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -21371,6 +21581,12 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -23078,6 +23294,27 @@ "tslib": "^1.9.3" } }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -23384,6 +23621,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -23841,6 +24084,12 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 39282bd73..44078850f 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "nock": "13.3.8", "prettier": "npm:wp-prettier@2.8.5", "rimraf": "5.0.5", + "ts-node": "^10.9.1", "typescript": "^5.2.2" }, "dependencies": { diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts index 14b25010b..d4a17884b 100644 --- a/src/commands/example-command.ts +++ b/src/commands/example-command.ts @@ -1,11 +1,13 @@ import { BaseVIPCommand } from '../lib/base-command'; -import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; import { CommandRegistry } from '../lib/command-registry'; +import type { CommandOption, CommandUsage } from '../lib/types/commands'; + const registry = CommandRegistry.getInstance(); export class ExampleCommand extends BaseVIPCommand { protected readonly name: string = 'example'; + protected readonly usage: CommandUsage = { description: 'Example command', examples: [ @@ -31,11 +33,7 @@ export class ExampleCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = [ new ExampleChildCommand() ]; - constructor( name ) { - super( 'example' ); - } - - protected execute( opts, ...args: unknown[] ): void { + protected execute( opts: unknown[], ...args: unknown[] ): void { console.log( 'parent', this.getName(), opts, args ); } } @@ -56,13 +54,9 @@ export class ExampleChildCommand extends BaseVIPCommand { ], }; - protected execute( opts, ...args: unknown[] ): void { + protected execute( opts: unknown[], ...args: unknown[] ): void { console.log( this.getName(), 'what' ); } - // constructor() { - // super( 'example child' ); - // } } - // registry.registerCommand( new ExampleCommand() ); diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 0fa93b2ce..724848f5c 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,7 +1,10 @@ -import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; import { CommandRegistry } from './command-registry'; -export abstract class BaseVIPCommand { +import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; + +export class BaseVIPCommand { + protected name: string = 'vip'; + protected readonly commandOptions: CommandOption[] = [ { name: '--debug', @@ -36,7 +39,7 @@ export abstract class BaseVIPCommand { protected childCommands: BaseVIPCommand[] = []; - constructor( private readonly name: string ) { + public constructor() { const registry = CommandRegistry.getInstance(); // registry.registerCommand( this ); @@ -64,7 +67,9 @@ export abstract class BaseVIPCommand { } } - protected abstract execute( ...args: unknown[] ): void; + protected execute( ..._args: unknown[] ): void { + // Do nothing + } public getName(): string { return this.name; diff --git a/src/lib/command.ts b/src/lib/command.ts index b388b4673..e305db739 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -3,7 +3,6 @@ import { Command } from 'commander'; import { BaseVIPCommand } from './base-command'; import { CommandRegistry } from './command-registry'; import { ExampleCommand } from '../commands/example-command'; -import { CommandExample, CommandOption, CommandArgument, CommandUsage } from './types/commands'; /** * Base Command from which every subcommand should inherit. @@ -18,33 +17,32 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { const commandArgs = command.getArguments(); const cmd = new Command( name ).description( usage.description ); - for( const argument of commandArgs ) { - let name = argument.name; + for ( const argument of commandArgs ) { + let argumentName = argument.name; if ( argument.required ) { - name = `<${ name }>`; + argumentName = `<${ argumentName }>`; } else { - name = `[${ name }]`; + argumentName = `[${ argumentName }]`; } - cmd.argument( name, argument.description ); + cmd.argument( argumentName, argument.description ); } for ( const option of options ) { cmd.option( option.name, option.description ); } - - cmd.action( ( ...args ) => { + cmd.action( ( ...args: unknown[] ) => { console.log( name ); registry.invokeCommand( name, ...args ); } ); - cmd.configureHelp( { showGlobalOptions: true } ) + cmd.configureHelp( { showGlobalOptions: true } ); return cmd; }; const program = new Command(); -const baseVIPCommand = new BaseVIPCommand( 'vip' ); +const baseVIPCommand = new BaseVIPCommand(); program .name( 'vip' ) @@ -60,10 +58,12 @@ const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); for ( const [ key, command ] of registry.getCommands() ) { - program.addCommand( makeVIPCommand( command ) ); + const cmd = makeVIPCommand( command ); for ( const childCommand of command.getChildCommands() ) { // const instance: BaseVIPCommand = new childCommand(); - program.addCommand( makeVIPCommand( childCommand ) ); + cmd.addCommand( makeVIPCommand( childCommand ) ); } + + program.addCommand( cmd ); } program.parse( process.argv ); diff --git a/src/lib/types/commands.ts b/src/lib/types/commands.ts new file mode 100644 index 000000000..4cdb34a67 --- /dev/null +++ b/src/lib/types/commands.ts @@ -0,0 +1,24 @@ +export interface CommandExample { + description: string; + usage: string; +} + +export interface CommandOption { + name: string; + alias?: string; + description: string; + type: string; + required?: boolean; +} + +export interface CommandArgument { + name: string; + description: string; + type: string; + required: boolean; +} + +export interface CommandUsage { + description: string; + examples: CommandExample[]; +} From 4df3c278b05515ea4c2dda321a873b0e6d2902b5 Mon Sep 17 00:00:00 2001 From: Volodymyr Kolesnykov Date: Wed, 6 Dec 2023 12:28:17 +0200 Subject: [PATCH 09/16] Refactoring --- src/commands/example-command.ts | 5 ----- src/lib/base-command.ts | 40 ++++++++++++++++++--------------- src/lib/command-registry.ts | 4 ++-- src/lib/command.ts | 34 ++++++++++++---------------- 4 files changed, 38 insertions(+), 45 deletions(-) diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts index d4a17884b..2c63582af 100644 --- a/src/commands/example-command.ts +++ b/src/commands/example-command.ts @@ -1,10 +1,7 @@ import { BaseVIPCommand } from '../lib/base-command'; -import { CommandRegistry } from '../lib/command-registry'; import type { CommandOption, CommandUsage } from '../lib/types/commands'; -const registry = CommandRegistry.getInstance(); - export class ExampleCommand extends BaseVIPCommand { protected readonly name: string = 'example'; @@ -58,5 +55,3 @@ export class ExampleChildCommand extends BaseVIPCommand { console.log( this.getName(), 'what' ); } } - -// registry.registerCommand( new ExampleCommand() ); diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 724848f5c..43b238040 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,18 +1,12 @@ import { CommandRegistry } from './command-registry'; +import { trackEvent } from './tracker'; import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; -export class BaseVIPCommand { +export abstract class BaseVIPCommand { protected name: string = 'vip'; - protected readonly commandOptions: CommandOption[] = [ - { - name: '--debug', - alias: '-d', - description: 'Show debug', - type: 'boolean', - }, - ]; + protected readonly commandOptions: CommandOption[] = []; protected readonly commandArguments: CommandArgument[] = [ { @@ -48,28 +42,38 @@ export class BaseVIPCommand { } ); } - protected trackEvent( eventName: string, data: unknown[] ): void { - // Send tracking information to trackEvent + protected getTrackingParams( _args: Record< string, unknown > ): Record< string, unknown > { + return {}; } - public run( ...args: unknown[] ): void { + protected shouldTrackFailure( _error: Error ): boolean { + return true; + } + + public async run( ...args: unknown[] ): Promise< void > { // Invoke the command and send tracking information + const trackingParams = this.getTrackingParams( { args } ); try { - this.trackEvent( `${ this.name }_execute`, args ); + await trackEvent( `${ this.name }_execute`, trackingParams ); this.execute( ...args ); - this.trackEvent( `${ this.name }_success`, args ); + await trackEvent( `${ this.name }_success`, trackingParams ); } catch ( error ) { const err = error instanceof Error ? error : new Error( error?.toString() ?? 'Unknown error' ); - this.trackEvent( `${ this.name }_error`, [ err ] ); + if ( this.shouldTrackFailure( err ) ) { + await trackEvent( `${ this.name }_error`, { + ...trackingParams, + failure: err.message, + stack: err.stack, + } ); + } + throw error; } } - protected execute( ..._args: unknown[] ): void { - // Do nothing - } + protected abstract execute( ..._args: unknown[] ): void; public getName(): string { return this.name; diff --git a/src/lib/command-registry.ts b/src/lib/command-registry.ts index b96bb93f7..1d0371006 100644 --- a/src/lib/command-registry.ts +++ b/src/lib/command-registry.ts @@ -26,10 +26,10 @@ export class CommandRegistry { this.commands.set( command.getName(), command ); } - public invokeCommand( commandName: string, ...args: unknown[] ): void { + public async invokeCommand( commandName: string, ...args: unknown[] ): Promise< void > { const command = this.commands.get( commandName ); if ( command ) { - command.run( ...args ); + await command.run( ...args ); } else { console.log( this.commands ); throw new Error( `Command '${ commandName }' not found.` ); diff --git a/src/lib/command.ts b/src/lib/command.ts index e305db739..b58fc1426 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { BaseVIPCommand } from './base-command'; import { CommandRegistry } from './command-registry'; +import { description, version } from '../../package.json'; import { ExampleCommand } from '../commands/example-command'; /** @@ -32,38 +33,31 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { cmd.option( option.name, option.description ); } - cmd.action( ( ...args: unknown[] ) => { - console.log( name ); - registry.invokeCommand( name, ...args ); + cmd.action( async ( ...args: unknown[] ) => { + await registry.invokeCommand( name, ...args ); } ); cmd.configureHelp( { showGlobalOptions: true } ); return cmd; }; -const program = new Command(); +const processCommand = ( parent: Command, command: BaseVIPCommand ): void => { + const cmd = makeVIPCommand( command ); + command.getChildCommands().forEach( childCommand => processCommand( cmd, childCommand ) ); + parent.addCommand( cmd ); +}; -const baseVIPCommand = new BaseVIPCommand(); +const program = new Command(); program .name( 'vip' ) - .description( 'WPVIP CLI' ) - .version( '3.0.0' ) - .configureHelp( { showGlobalOptions: true } ); - -for ( const option of baseVIPCommand.getOptions() ) { - program.option( option.name, option.description ); -} + .description( description ) + .version( version ) + .configureHelp( { showGlobalOptions: true } ) + .option( '--debug, -d', 'Show debug' ); const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); -for ( const [ key, command ] of registry.getCommands() ) { - const cmd = makeVIPCommand( command ); - for ( const childCommand of command.getChildCommands() ) { - // const instance: BaseVIPCommand = new childCommand(); - cmd.addCommand( makeVIPCommand( childCommand ) ); - } +[ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); - program.addCommand( cmd ); -} program.parse( process.argv ); From 126cca0c60f625ca589a2d3830ac1bdc286d3669 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Wed, 6 Dec 2023 15:12:31 -0600 Subject: [PATCH 10/16] Move out registerCommand call from base class constructor to address an issue with insance fields: instance fields of a derived class are defined after super() returns, the base class's constructor does not have access to the derived class's fields. --- src/commands/example-command.ts | 27 +++++++++++++++++++++++++-- src/lib/base-command.ts | 9 +-------- src/lib/command.ts | 8 +++++++- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts index 2c63582af..4b8cc402f 100644 --- a/src/commands/example-command.ts +++ b/src/commands/example-command.ts @@ -31,7 +31,7 @@ export class ExampleCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = [ new ExampleChildCommand() ]; protected execute( opts: unknown[], ...args: unknown[] ): void { - console.log( 'parent', this.getName(), opts, args ); + console.log( 'parent', this.getName(), opts ); } } @@ -51,7 +51,30 @@ export class ExampleChildCommand extends BaseVIPCommand { ], }; + protected childCommands: BaseVIPCommand[] = [ new ExampleGrandChildCommand() ]; + + protected execute( opts: unknown[], ...args: unknown[] ): void { + console.log( this.getName() ); + } +} + +export class ExampleGrandChildCommand extends BaseVIPCommand { + protected readonly name: string = 'grandchild'; + protected readonly usage: CommandUsage = { + description: 'Example grandchild command', + examples: [ + { + description: 'Example 1', + usage: 'vip example child arg1 arg2', + }, + { + description: 'Example 2', + usage: 'vip example child --named=arg1 --also=arg2', + }, + ], + }; + protected execute( opts: unknown[], ...args: unknown[] ): void { - console.log( this.getName(), 'what' ); + console.log( this.getName() ); } } diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 43b238040..02b92e5df 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -33,14 +33,7 @@ export abstract class BaseVIPCommand { protected childCommands: BaseVIPCommand[] = []; - public constructor() { - const registry = CommandRegistry.getInstance(); - // registry.registerCommand( this ); - - this.childCommands.forEach( command => { - registry.registerCommand( command ); - } ); - } + public constructor() {} protected getTrackingParams( _args: Record< string, unknown > ): Record< string, unknown > { return {}; diff --git a/src/lib/command.ts b/src/lib/command.ts index b58fc1426..57561aa93 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -42,7 +42,11 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { const processCommand = ( parent: Command, command: BaseVIPCommand ): void => { const cmd = makeVIPCommand( command ); - command.getChildCommands().forEach( childCommand => processCommand( cmd, childCommand ) ); + + command.getChildCommands().forEach( childCommand => { + registry.registerCommand( childCommand ); + processCommand( cmd, childCommand ); + } ); parent.addCommand( cmd ); }; @@ -60,4 +64,6 @@ registry.registerCommand( new ExampleCommand() ); [ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); +console.log( registry.getCommands() ); + program.parse( process.argv ); From a731299683500d48534cd6c5ce42111f63d32664 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Wed, 6 Dec 2023 16:52:02 -0600 Subject: [PATCH 11/16] Implement prototype App command to see how easy/hard is to move the rest --- src/commands/app.ts | 87 +++++++++++++++++++++++++++++++++ src/commands/example-command.ts | 4 +- src/lib/command.ts | 5 +- 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/commands/app.ts diff --git a/src/commands/app.ts b/src/commands/app.ts new file mode 100644 index 000000000..565f875f1 --- /dev/null +++ b/src/commands/app.ts @@ -0,0 +1,87 @@ +import chalk from 'chalk'; + +import app from '../lib/api/app'; + +import { trackEvent } from '../lib/tracker'; +import { BaseVIPCommand } from '../lib/base-command'; +import { getEnvIdentifier } from '../lib/cli/command'; +import { formatData, formatSearchReplaceValues } from '../lib/cli/format'; + +import type { CommandOption, CommandUsage } from '../lib/types/commands'; + +export class AppCommand extends BaseVIPCommand { + protected readonly name: string = 'app'; + + protected readonly usage: CommandUsage = { + description: 'List and modify your VIP applications', + examples: [ + { + description: 'Example 1', + usage: 'vip app app', + }, + { + description: 'Example 2', + usage: 'vip example ', + }, + ], + }; + + protected readonly commandOptions: CommandOption[] = []; + + protected childCommands: BaseVIPCommand[] = []; + + protected async execute( ...arg: unknown[] ): void { + let res; + try { + res = await app( + arg[ 0 ], + 'id,repo,name,environments{id,appId,name,type,branch,currentCommit,primaryDomain{name},launched}' + ); + } catch ( err ) { + await trackEvent( 'app_command_fetch_error', { + error: `App ${ arg[ 0 ] } does not exist`, + } ); + + console.log( `App ${ chalk.blueBright( arg[ 0 ] ) } does not exist` ); + return; + } + + if ( ! res || ! res.environments ) { + await trackEvent( 'app_command_fetch_error', { + error: `App ${ arg[ 0 ] } does not exist`, + } ); + + console.log( `App ${ chalk.blueBright( arg[ 0 ] ) } does not exist` ); + return; + } + + await trackEvent( 'app_command_success' ); + + // Clone the read-only response object so we can modify it + const clonedResponse = Object.assign( {}, res ); + + const header = [ + { key: 'id', value: res.id }, + { key: 'name', value: res.name }, + { key: 'repo', value: res.repo }, + ]; + + clonedResponse.environments = clonedResponse.environments.map( env => { + const clonedEnv = Object.assign( {}, env ); + + clonedEnv.name = getEnvIdentifier( env ); + + // Use the short version of git commit hash + clonedEnv.currentCommit = clonedEnv.currentCommit.substring( 0, 7 ); + + // Flatten object + clonedEnv.primaryDomain = clonedEnv.primaryDomain.name; + delete clonedEnv.__typename; + return clonedEnv; + } ); + + console.log( formatData( header, 'keyValue' ) ); + + console.log( formatData( clonedResponse.environments, "table" ) ); + } +} diff --git a/src/commands/example-command.ts b/src/commands/example-command.ts index 4b8cc402f..f79f28794 100644 --- a/src/commands/example-command.ts +++ b/src/commands/example-command.ts @@ -30,8 +30,8 @@ export class ExampleCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = [ new ExampleChildCommand() ]; - protected execute( opts: unknown[], ...args: unknown[] ): void { - console.log( 'parent', this.getName(), opts ); + protected execute( opts: unknown[] ): void { + console.log( 'parent', this.getName() ); } } diff --git a/src/lib/command.ts b/src/lib/command.ts index 57561aa93..c067da9a5 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -4,6 +4,7 @@ import { BaseVIPCommand } from './base-command'; import { CommandRegistry } from './command-registry'; import { description, version } from '../../package.json'; import { ExampleCommand } from '../commands/example-command'; +import { AppCommand } from '../commands/app'; /** * Base Command from which every subcommand should inherit. @@ -36,6 +37,7 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { cmd.action( async ( ...args: unknown[] ) => { await registry.invokeCommand( name, ...args ); } ); + cmd.configureHelp( { showGlobalOptions: true } ); return cmd; }; @@ -61,9 +63,8 @@ program const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); +registry.registerCommand( new AppCommand() ); [ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); -console.log( registry.getCommands() ); - program.parse( process.argv ); From ef0e85b9b0e2c8bc347c5ce52d74afb3ae98ef32 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Fri, 8 Dec 2023 16:44:27 -0600 Subject: [PATCH 12/16] WIP - more argument parsing, trying to handle @app.env accordingly --- src/commands/app.ts | 2 +- src/lib/base-command.ts | 45 ++++++++++++++++++++++++++++++++++++++--- src/lib/command.ts | 14 ++++++++++--- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/commands/app.ts b/src/commands/app.ts index 565f875f1..596fed283 100644 --- a/src/commands/app.ts +++ b/src/commands/app.ts @@ -26,7 +26,7 @@ export class AppCommand extends BaseVIPCommand { ], }; - protected readonly commandOptions: CommandOption[] = []; + // protected readonly commandOptions: CommandOption[] = []; protected childCommands: BaseVIPCommand[] = []; diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 02b92e5df..68107c9d8 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,19 +1,41 @@ +import args from 'args'; +import chalk from 'chalk'; +import debugLib from 'debug'; +import { prompt } from 'enquirer'; +import gql from 'graphql-tag'; + +import { parseEnvAliasFromArgv } from '../lib/cli/envAlias'; import { CommandRegistry } from './command-registry'; import { trackEvent } from './tracker'; import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; +import { Command } from 'commander'; export abstract class BaseVIPCommand { protected name: string = 'vip'; + protected isDebugConfirmed: boolean = false; - protected readonly commandOptions: CommandOption[] = []; + protected readonly commandOptions: CommandOption[] = [ + { + name: 'app', + description: 'Application id or slug', + type: 'string', + required: false, + }, + { + name: 'env', + description: 'Application environment', + type: 'string', + required: false, + }, + ]; protected readonly commandArguments: CommandArgument[] = [ { name: 'app', description: 'Application id or slug', type: 'string', - required: true, + required: false, }, ]; @@ -43,12 +65,29 @@ export abstract class BaseVIPCommand { return true; } + // args length can vary based the number of arguments and options the command defines, the command itsrlf is always the last argument + // Can some of this logic be moved out to a hook? public async run( ...args: unknown[] ): Promise< void > { + let ret; // Invoke the command and send tracking information const trackingParams = this.getTrackingParams( { args } ); + // console.log( args ); + // let [ _args, opts, command ] = args; + let command = args[ args.length - 1 ]; + console.log( command.opts() ); + + if ( command.opts()?.inspect && ! this.isDebugConfirmed ) { + await prompt( { + type: 'confirm', + name: 'confirm', + message: "Attach the debugger, once you see 'Debugger attached' above hit 'y' to continue", + } ); + this.isDebugConfirmed = true; + } + try { await trackEvent( `${ this.name }_execute`, trackingParams ); - this.execute( ...args ); + ret = await this.execute( ...args ); await trackEvent( `${ this.name }_success`, trackingParams ); } catch ( error ) { const err = diff --git a/src/lib/command.ts b/src/lib/command.ts index c067da9a5..04d240698 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -5,6 +5,8 @@ import { CommandRegistry } from './command-registry'; import { description, version } from '../../package.json'; import { ExampleCommand } from '../commands/example-command'; import { AppCommand } from '../commands/app'; +import { parse } from 'args'; +import { parseEnvAliasFromArgv } from './cli/envAlias'; /** * Base Command from which every subcommand should inherit. @@ -34,6 +36,8 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { cmd.option( option.name, option.description ); } + cmd.option( '-d, --debug [component]', 'Show debug' ).option( '--inspect', 'Attach a debugger' ); + cmd.action( async ( ...args: unknown[] ) => { await registry.invokeCommand( name, ...args ); } ); @@ -58,8 +62,7 @@ program .name( 'vip' ) .description( description ) .version( version ) - .configureHelp( { showGlobalOptions: true } ) - .option( '--debug, -d', 'Show debug' ); + .configureHelp( { showGlobalOptions: true } ); const registry = CommandRegistry.getInstance(); registry.registerCommand( new ExampleCommand() ); @@ -67,4 +70,9 @@ registry.registerCommand( new AppCommand() ); [ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); -program.parse( process.argv ); +let { argv, ...appAlias } = parseEnvAliasFromArgv( process.argv ); + +// very very stupid +argv.push( `@${ Object.values( appAlias ).filter( e => e ).join( '.' ) }` ); + +program.parse( argv, appAlias ); From 791264571966fc6af687ae706fe4df68455d53d4 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Mon, 11 Dec 2023 14:06:17 -0600 Subject: [PATCH 13/16] Implement authenticate method and needsAuth: boolean with default of true, this property would control whether auth is needed, and if so will call the authenticate method, which is currently almost verbatim copy-pasta from bin/vip.js --- src/commands/app.ts | 7 +- src/lib/base-command.ts | 150 +++++++++++++++++++++++++++++++++++++++- src/lib/command.ts | 8 ++- 3 files changed, 157 insertions(+), 8 deletions(-) diff --git a/src/commands/app.ts b/src/commands/app.ts index 596fed283..61c187502 100644 --- a/src/commands/app.ts +++ b/src/commands/app.ts @@ -31,6 +31,7 @@ export class AppCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = []; protected async execute( ...arg: unknown[] ): void { + console.log(arg[0]); let res; try { res = await app( @@ -80,8 +81,10 @@ export class AppCommand extends BaseVIPCommand { return clonedEnv; } ); - console.log( formatData( header, 'keyValue' ) ); + return { header, data: clonedResponse.environments }; - console.log( formatData( clonedResponse.environments, "table" ) ); + // console.log( formatData( header, 'keyValue' ) ); + + // console.log( formatData( clonedResponse.environments, "table" ) ); } } diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 68107c9d8..5dcc387ab 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -3,17 +3,25 @@ import chalk from 'chalk'; import debugLib from 'debug'; import { prompt } from 'enquirer'; import gql from 'graphql-tag'; +import { Command } from 'commander'; -import { parseEnvAliasFromArgv } from '../lib/cli/envAlias'; import { CommandRegistry } from './command-registry'; -import { trackEvent } from './tracker'; +import config from './cli/config'; +import Token from './token'; +import { parseEnvAliasFromArgv } from './cli/envAlias'; +import { trackEvent, aliasUser } from './tracker'; import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; -import { Command } from 'commander'; + +// Needs to go inside the command +const debug = debugLib( '@automattic/vip:bin:vip' ); +// Config +const tokenURL = 'https://dashboard.wpvip.com/me/cli/token'; export abstract class BaseVIPCommand { protected name: string = 'vip'; protected isDebugConfirmed: boolean = false; + protected needsAuth: boolean = true; protected readonly commandOptions: CommandOption[] = [ { @@ -65,9 +73,145 @@ export abstract class BaseVIPCommand { return true; } + protected async authenticate(): Promise< void > { + /** + * @param {any[]} argv + * @param {any[]} params + * @returns {boolean} + */ + function doesArgvHaveAtLeastOneParam( argv, params ) { + return argv.some( arg => params.includes( arg ) ); + } + + let token = await Token.get(); + + const isHelpCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'help', '-h', '--help' ] ); + const isVersionCommand = doesArgvHaveAtLeastOneParam( process.argv, [ '-v', '--version' ] ); + const isLogoutCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'logout' ] ); + const isLoginCommand = doesArgvHaveAtLeastOneParam( process.argv, [ 'login' ] ); + const isDevEnvCommandWithoutEnv = + doesArgvHaveAtLeastOneParam( process.argv, [ 'dev-env' ] ) && + ! containsAppEnvArgument( process.argv ); + + debug( 'Argv:', process.argv ); + + if ( + ! isLoginCommand && + ( isLogoutCommand || + isHelpCommand || + isVersionCommand || + isDevEnvCommandWithoutEnv || + token?.valid() ) + ) { + return; + } + + console.log(); + console.log( ' _ __ ________ ________ ____' ); + console.log( ' | | / // _/ __ / ____/ / / _/' ); + console.log( ' | | / / / // /_/ /______/ / / / / / ' ); + console.log( ' | |/ /_/ // ____//_____/ /___/ /____/ / ' ); + console.log( ' |___//___/_/ ____/_____/___/ ' ); + console.log(); + + console.log( + ' VIP-CLI is your tool for interacting with and managing your VIP applications.' + ); + console.log(); + + console.log( + ' Authenticate your installation of VIP-CLI with your Personal Access Token. This URL will be opened in your web browser automatically so that you can retrieve your token: ' + + tokenURL + ); + console.log(); + + await trackEvent( 'login_command_execute' ); + + const answer = await prompt( { + type: 'confirm', + name: 'continue', + message: 'Ready to authenticate?', + } ); + + if ( ! answer.continue ) { + await trackEvent( 'login_command_browser_cancelled' ); + + return; + } + + const { default: open } = await import( 'open' ); + + await open( tokenURL, { wait: false } ); + + await trackEvent( 'login_command_browser_opened' ); + + const { token: tokenInput } = await prompt( { + type: 'password', + name: 'token', + message: 'Access Token:', + } ); + + try { + token = new Token( tokenInput ); + } catch ( err ) { + console.log( 'The token provided is malformed. Please check the token and try again.' ); + + await trackEvent( 'login_command_token_submit_error', { error: err.message } ); + + return; + } + + if ( token.expired() ) { + console.log( 'The token provided is expired. Please log in again to refresh the token.' ); + + await trackEvent( 'login_command_token_submit_error', { error: 'expired' } ); + + return; + } + + if ( ! token.valid() ) { + console.log( 'The provided token is not valid. Please log in again to refresh the token.' ); + + await trackEvent( 'login_command_token_submit_error', { error: 'invalid' } ); + + return; + } + + try { + await Token.set( token.raw ); + } catch ( err ) { + await trackEvent( 'login_command_token_submit_error', { + error: err.message, + } ); + + throw err; + } + + // De-anonymize user for tracking + await aliasUser( token.id ); + + await trackEvent( 'login_command_token_submit_success' ); + + if ( isLoginCommand ) { + console.log( 'You are now logged in - see `vip -h` for a list of available commands.' ); + + process.exit(); + } + + return; + } + // args length can vary based the number of arguments and options the command defines, the command itsrlf is always the last argument // Can some of this logic be moved out to a hook? public async run( ...args: unknown[] ): Promise< void > { + if ( this.needsAuth ) { + try { + await this.authenticate(); + } catch ( error ) { + console.log( error ); + } + } + let ret; // Invoke the command and send tracking information const trackingParams = this.getTrackingParams( { args } ); diff --git a/src/lib/command.ts b/src/lib/command.ts index 04d240698..f558d3e60 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -72,7 +72,9 @@ registry.registerCommand( new AppCommand() ); let { argv, ...appAlias } = parseEnvAliasFromArgv( process.argv ); -// very very stupid -argv.push( `@${ Object.values( appAlias ).filter( e => e ).join( '.' ) }` ); - +let appAliasString = Object.values( appAlias ).filter( e => e ).join( '.' ); +if ( appAliasString ) { + argv.push( `@${ appAliasString }` ); +} +console.log(appAlias); program.parse( argv, appAlias ); From 004c4ada209cd7e86f00a795565a7084bf7c88eb Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Mon, 11 Dec 2023 14:53:08 -0600 Subject: [PATCH 14/16] Add docblocks --- src/lib/base-command.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 5dcc387ab..99ca438e4 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -73,6 +73,14 @@ export abstract class BaseVIPCommand { return true; } + /** + * Authentication routine. + * This will prompt the user to authenticate with their VIP account. + * + * @protected + * @returns {Promise< void >} + * @memberof BaseVIPCommand + */ protected async authenticate(): Promise< void > { /** * @param {any[]} argv @@ -203,6 +211,14 @@ export abstract class BaseVIPCommand { // args length can vary based the number of arguments and options the command defines, the command itsrlf is always the last argument // Can some of this logic be moved out to a hook? + + /** + * This is a wrapper method that performs common routines before and after executing the command + * + * @param {...unknown[]} args + * @returns {Promise< void >} + * @memberof BaseVIPCommand + */ public async run( ...args: unknown[] ): Promise< void > { if ( this.needsAuth ) { try { From 86f965c2102888e3d8a371f3786a416c4f2e3864 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Tue, 13 Feb 2024 15:33:38 -0600 Subject: [PATCH 15/16] Lint and some argument dark magic --- src/commands/app.ts | 8 ++---- src/lib/base-command.ts | 11 +------- src/lib/command.ts | 62 +++++++++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/commands/app.ts b/src/commands/app.ts index 61c187502..4236f4b99 100644 --- a/src/commands/app.ts +++ b/src/commands/app.ts @@ -1,13 +1,11 @@ import chalk from 'chalk'; import app from '../lib/api/app'; - -import { trackEvent } from '../lib/tracker'; import { BaseVIPCommand } from '../lib/base-command'; import { getEnvIdentifier } from '../lib/cli/command'; -import { formatData, formatSearchReplaceValues } from '../lib/cli/format'; +import { trackEvent } from '../lib/tracker'; -import type { CommandOption, CommandUsage } from '../lib/types/commands'; +import type { CommandUsage } from '../lib/types/commands'; export class AppCommand extends BaseVIPCommand { protected readonly name: string = 'app'; @@ -31,7 +29,7 @@ export class AppCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = []; protected async execute( ...arg: unknown[] ): void { - console.log(arg[0]); + console.log( arg[ 0 ] ); let res; try { res = await app( diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 99ca438e4..3b9656725 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,14 +1,7 @@ -import args from 'args'; -import chalk from 'chalk'; import debugLib from 'debug'; import { prompt } from 'enquirer'; -import gql from 'graphql-tag'; -import { Command } from 'commander'; -import { CommandRegistry } from './command-registry'; -import config from './cli/config'; import Token from './token'; -import { parseEnvAliasFromArgv } from './cli/envAlias'; import { trackEvent, aliasUser } from './tracker'; import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; @@ -205,8 +198,6 @@ export abstract class BaseVIPCommand { process.exit(); } - - return; } // args length can vary based the number of arguments and options the command defines, the command itsrlf is always the last argument @@ -233,7 +224,7 @@ export abstract class BaseVIPCommand { const trackingParams = this.getTrackingParams( { args } ); // console.log( args ); // let [ _args, opts, command ] = args; - let command = args[ args.length - 1 ]; + const command = args[ args.length - 1 ]; console.log( command.opts() ); if ( command.opts()?.inspect && ! this.isDebugConfirmed ) { diff --git a/src/lib/command.ts b/src/lib/command.ts index f558d3e60..9773ed190 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -1,12 +1,11 @@ import { Command } from 'commander'; import { BaseVIPCommand } from './base-command'; +import { parseEnvAliasFromArgv } from './cli/envAlias'; import { CommandRegistry } from './command-registry'; import { description, version } from '../../package.json'; -import { ExampleCommand } from '../commands/example-command'; import { AppCommand } from '../commands/app'; -import { parse } from 'args'; -import { parseEnvAliasFromArgv } from './cli/envAlias'; +import { ExampleCommand } from '../commands/example-command'; /** * Base Command from which every subcommand should inherit. @@ -56,6 +55,40 @@ const processCommand = ( parent: Command, command: BaseVIPCommand ): void => { parent.addCommand( cmd ); }; +/** + * @param {string[]} args + * @param {Command} command + * @returns {string[]} + */ +function sortArguments( args, command ) { + const subcommands = command.commands.map( cmd => cmd.name() ); + if ( subcommands.length ) { + const saved = []; + while ( args.length ) { + const arg = /** @type {string} */ args.shift(); + if ( arg === '--' ) { + return [ ...saved, arg, ...args ]; + } + + if ( subcommands.includes( arg ) ) { + return [ + arg, + ...sortArguments( + [ ...saved, ...args ], + /** @type {Command} */ command.commands.find( cmd => cmd.name() === arg ) + ), + ]; + } + + saved.push( arg ); + } + + return [ ...saved ]; + } + + return args; +} + const program = new Command(); program @@ -70,11 +103,18 @@ registry.registerCommand( new AppCommand() ); [ ...registry.getCommands().values() ].map( command => processCommand( program, command ) ); -let { argv, ...appAlias } = parseEnvAliasFromArgv( process.argv ); - -let appAliasString = Object.values( appAlias ).filter( e => e ).join( '.' ); -if ( appAliasString ) { - argv.push( `@${ appAliasString }` ); -} -console.log(appAlias); -program.parse( argv, appAlias ); +const { argv, ...appAlias } = parseEnvAliasFromArgv( process.argv ); + +// let appAliasString = Object.values( appAlias ).filter( e => e ).join( '.' ); +// if ( appAliasString ) { +// argv.push( `@${ appAliasString }` ); +// } +// console.log(appAlias); +console.log( argv, sortArguments( process.argv.slice( 2 ), program ), { appAlias }, [ + ...argv.slice( 0, 2 ), + ...sortArguments( process.argv.slice( 2 ), program ), +] ); +// program.parse( sortArguments(process.argv, program ), { appAlias } ); +program.parse( [ ...argv.slice( 0, 2 ), ...sortArguments( process.argv.slice( 2 ), program ) ], { + appAlias, +} ); From 8e21583675af08b9566a6c4b971f638a44f1447a Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Tue, 13 Feb 2024 16:16:40 -0600 Subject: [PATCH 16/16] Implement format support in BaseCommand::execute, general cleanup --- src/commands/app.ts | 5 ----- src/lib/base-command.ts | 40 +++++++++++++++++++++++++++++++++++----- src/lib/command.ts | 5 ++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/commands/app.ts b/src/commands/app.ts index 4236f4b99..8e22b74d7 100644 --- a/src/commands/app.ts +++ b/src/commands/app.ts @@ -29,7 +29,6 @@ export class AppCommand extends BaseVIPCommand { protected childCommands: BaseVIPCommand[] = []; protected async execute( ...arg: unknown[] ): void { - console.log( arg[ 0 ] ); let res; try { res = await app( @@ -80,9 +79,5 @@ export class AppCommand extends BaseVIPCommand { } ); return { header, data: clonedResponse.environments }; - - // console.log( formatData( header, 'keyValue' ) ); - - // console.log( formatData( clonedResponse.environments, "table" ) ); } } diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 3b9656725..82f85a85b 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -1,11 +1,11 @@ import debugLib from 'debug'; import { prompt } from 'enquirer'; +import { formatData } from './cli/format'; import Token from './token'; import { trackEvent, aliasUser } from './tracker'; import type { CommandOption, CommandArgument, CommandUsage } from './types/commands'; - // Needs to go inside the command const debug = debugLib( '@automattic/vip:bin:vip' ); // Config @@ -219,15 +219,16 @@ export abstract class BaseVIPCommand { } } - let ret; + let res; // Invoke the command and send tracking information const trackingParams = this.getTrackingParams( { args } ); // console.log( args ); // let [ _args, opts, command ] = args; const command = args[ args.length - 1 ]; - console.log( command.opts() ); + const _opts = command.opts(); + // console.log( command.opts() ); - if ( command.opts()?.inspect && ! this.isDebugConfirmed ) { + if ( _opts?.inspect && ! this.isDebugConfirmed ) { await prompt( { type: 'confirm', name: 'confirm', @@ -238,7 +239,36 @@ export abstract class BaseVIPCommand { try { await trackEvent( `${ this.name }_execute`, trackingParams ); - ret = await this.execute( ...args ); + res = await this.execute( ...args ); + + if ( _opts.format && res ) { + if ( res.header ) { + if ( _opts.format !== 'json' ) { + console.log( formatData( res.header, 'keyValue' ) ); + } + res = res.data; + } + + res = res.map( row => { + const out = { ...row }; + if ( out.__typename ) { + // Apollo injects __typename + delete out.__typename; + } + + return out; + } ); + + await trackEvent( 'command_output', { + format: _opts.format, + } ); + + const formattedOut = formatData( res, _opts.format ); + + console.log( formattedOut ); + + return {}; + } await trackEvent( `${ this.name }_success`, trackingParams ); } catch ( error ) { const err = diff --git a/src/lib/command.ts b/src/lib/command.ts index 9773ed190..02370d0f2 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -35,7 +35,10 @@ const makeVIPCommand = ( command: BaseVIPCommand ): Command => { cmd.option( option.name, option.description ); } - cmd.option( '-d, --debug [component]', 'Show debug' ).option( '--inspect', 'Attach a debugger' ); + cmd + .option( '-d, --debug [component]', 'Show debug' ) + .option( '--inspect', 'Attach a debugger' ) + .option( '--format [json|table|csv|ids]', 'Output format' ); cmd.action( async ( ...args: unknown[] ) => { await registry.invokeCommand( name, ...args );