UNPKG

@keyshade/cli

Version:
250 lines (216 loc) 7.33 kB
import type { CommandActionData, CommandArgument, CommandOption } from '@/types/command/command.types' import { fetchProfileConfig } from '@/util/configuration' import { Logger } from '@/util/logger' import { getDefaultProfile } from '@/util/profile' import { Option, type Command } from 'commander' import ControllerInstance from '@/util/controller-instance' import { SentryInstance } from '@/util/sentry' /** * The base class for all commands. All commands should extend this class. */ export default abstract class BaseCommand { // apiKey and baseUrl are protected fields that can be accessed by subclasses // Use these fields from the subclasses to make API requests if needed protected apiKey: string | null = null protected baseUrl: string | null = null protected metricsEnabled: boolean | null = null // Headers to be used by the API requests protected headers: Record<string, string> | null = null /** * Technically the entrypoint to the entire application. This function * is used to register the various commands across the entire CLI. The * function is only called from index.ts to register the commands and * should not be overridden. * * @param program The program to add the command to. */ readonly prepare = (program: Command): void => { const argsCount = this.getArguments().length const command = program .command(this.getName()) .description(this.getDescription()) .usage(this.getUsage()) .action(async (...data) => { try { const globalOptions = program.optsWithGlobals() await this.setGlobalContextFields(globalOptions) if (this.metricsEnabled) { SentryInstance.getInstance() } if (this.canMakeHttpRequests()) { if (!this.apiKey) { throw new Error( 'API key is missing. This command requires an API key. Either specify it using --api-key, or send in a profile using --profile, or set a default profile' ) } try { await ControllerInstance.getInstance().appController.health( this.headers ) } catch { throw new Error( `Could not connect to the server: ${this.baseUrl}.` ) } } const commandOptions = data[argsCount] const args: string[] = data.slice(0, argsCount) await this.action({ args, options: commandOptions }) } catch (error) { const errorInfo = error as string Logger.error(errorInfo) if (this.metricsEnabled) Logger.report(errorInfo) } }) this.getOptions().forEach((option) => { const newOption: Option = new Option( `${option.short}, ${option.long}`, option.description ).default(option.defaultValue) option.choices && option.choices.length > 0 && newOption.choices(option.choices) command.addOption(newOption) }) this.getArguments().forEach((argument) => command.argument(argument.name, argument.description) ) this.getSubCommands().forEach((subCommand) => { subCommand.prepare(command) }) } /** * The name of the command. */ abstract getName(): string /** * A description of the command. */ abstract getDescription(): string /** * This method is overridden by the subclasses to determine if the command can make HTTP requests. * If the command can make HTTP requests, return true. Otherwise, return false. * * Up the hierarchy, it is used in the `prepare` method to check if the command can make HTTP requests. * If it can, the global context fields should be set. If they are not set, an error is thrown. * @returns */ canMakeHttpRequests(): boolean { return false } getVersion(): string { return '1' } /** * The options that the command accepts. */ getOptions(): CommandOption[] { return [] } /** * The arguments that the command accepts. */ getArguments(): CommandArgument[] { return [] } /** * The action that the command should take. * @param data The data passed to the command. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-pattern action({}: CommandActionData): Promise<void> | void {} /** * If the command has subcommands, return them here. */ getSubCommands(): BaseCommand[] { return [] } /** * Returns a string representing the usage information for the command. * This method should be overridden by subclasses to provide specific usage * instructions for each command. */ getUsage(): string { return '' } /** * Logs the error * @param message The error message * @param error The error object containing details about the error */ protected logError(error: { message: string error: string statusCode: number }): void { const { header, body } = this.extractError(error) Logger.error(`${header}: ${body}`) if (this.metricsEnabled && error?.statusCode === 500) { Logger.report(`${header}.\n` + JSON.stringify(error)) } } private extractError(error: { message: string error: string statusCode: number }) { try { const { header, body } = JSON.parse(error.message) as { header: string body: string } return { header, body } } catch { return { header: 'Faced an error processing the request', body: error.message } } } private async setGlobalContextFields( globalOptions: Record<string, string> ): Promise<void> { const profiles = await fetchProfileConfig() // We will either fetch the profile configuration if the --profile flag is provided // and the profile exists. // Or, we will use the API key and base URL provided in the global options, if provided. // If none is specified, we try to fetch the default profile configuration. // In case none of the above works, and/or `apiKey` and `baseUrl` are still null, // any of the subclasses that will need to make API requests will throw an error. if (globalOptions.profile) { if (profiles[globalOptions.profile]) { const profile = profiles[globalOptions.profile] this.apiKey = profile.apiKey this.baseUrl = profile.baseUrl this.metricsEnabled = profile.metrics_enabled } else { throw new Error('Profile not found') } } else if (globalOptions.apiKey || globalOptions.baseUrl) { if (globalOptions.apiKey) { this.apiKey = globalOptions.apiKey } if (globalOptions.baseUrl) { this.baseUrl = globalOptions.baseUrl } } else { const defaultProfileName = getDefaultProfile(profiles) const defaultProfile = profiles[defaultProfileName] if (defaultProfile) { this.apiKey = defaultProfile.apiKey this.baseUrl = defaultProfile.baseUrl this.metricsEnabled = defaultProfile.metrics_enabled } } // Initialize the header this.headers = { 'x-keyshade-token': this.apiKey } // Initialize Controller Instance ControllerInstance.initialize(this.baseUrl) } }