import { ParseArgsResult } from "../input/types";
import { CommandOptions } from "./types";
import { UserInput } from "../input/input";
import { DefaultCommandOptions, DefaultCommandValues } from "./constants";
import { getDependencies, getPackageVersion } from "../utils/fs";
import { printBanner } from "../output/common";
import { Environment } from "../utils/environment";
import {
DefaultLoggingConfig,
Logger,
Logging,
LoggingConfig,
LogLevel,
} from "@decaf-ts/logging";
/**
* @class Command
* @abstract
* @template I - The type of input options for the command.
* @template R - The return type of the command execution.
* @memberOf module:utils
* @description Abstract base class for command implementation.
* @summary Provides a structure for creating command-line interface commands with input handling, logging, and execution flow.
*
* @param {string} name - The name of the command.
* @param {CommandOptions<I>} [inputs] - The input options for the command.
* @param {string[]} [requirements] - The list of required dependencies for the command.
*/
export abstract class Command<I, R> {
/**
* @static
* @description Static logger for the Command class.
* @type {Logger}
*/
static log: Logger;
/**
* @protected
* @description Instance logger for the command.
* @type {Logger}
*/
protected log: Logger;
protected constructor(
protected name: string,
protected inputs: CommandOptions<I> = {} as unknown as CommandOptions<I>,
protected requirements: string[] = []
) {
if (!Command.log) {
Object.defineProperty(Command, "log", {
writable: false,
value: Logging.for(Command.name),
});
this.log = Command.log;
}
this.log = Command.log.for(this.name);
this.inputs = Object.assign(
{},
DefaultCommandOptions,
inputs
) as CommandOptions<I>;
}
/**
* @protected
* @async
* @description Checks if all required dependencies are present.
* @summary Retrieves the list of dependencies and compares it against the required dependencies for the command.
* @returns {Promise<void>} A promise that resolves when the check is complete.
*
* @mermaid
* sequenceDiagram
* participant Command
* participant getDependencies
* participant Set
* Command->>getDependencies: Call
* getDependencies-->>Command: Return {prod, dev, peer}
* Command->>Set: Create Set from prod, dev, peer
* Set-->>Command: Return unique dependencies
* Command->>Command: Compare against requirements
* alt Missing dependencies
* Command->>Command: Add to missing list
* end
* Note over Command: If missing.length > 0, handle missing dependencies
*/
protected async checkRequirements(): Promise<void> {
const { prod, dev, peer } = await getDependencies();
const missing = [];
const fullList = Array.from(
new Set([...prod, ...dev, ...peer]).values()
).map((d) => d.name);
for (const dep of this.requirements)
if (!fullList.includes(dep)) missing.push(dep);
if (!missing.length) return;
}
/**
* @protected
* @description Provides help information for the command.
* @summary This method should be overridden in derived classes to provide specific help information.
* @param {ParseArgsResult} args - The parsed command-line arguments.
* @returns {void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected help(args: ParseArgsResult): void {
return this.log.info(
`This is help. I'm no use because I should have been overridden.`
);
}
/**
* @protected
* @abstract
* @description Runs the command with the provided arguments.
* @summary This method should be implemented in derived classes to define the command's behavior.
* @param {ParseArgsResult} answers - The parsed command-line arguments.
* @returns {Promise<R | string | void>} A promise that resolves with the command's result.
*/
protected abstract run<R>(
answers: LoggingConfig &
typeof DefaultCommandValues & { [k in keyof I]: unknown }
): Promise<R | string | void>;
/**
* @async
* @description Executes the command.
* @summary This method handles the overall execution flow of the command, including parsing arguments,
* setting up logging, checking for version or help requests, and running the command.
* @returns {Promise<R | string | void>} A promise that resolves with the command's result.
*
* @mermaid
* sequenceDiagram
* participant Command
* participant UserInput
* participant Logging
* participant getPackageVersion
* participant printBanner
* Command->>UserInput: parseArgs(inputs)
* UserInput-->>Command: Return ParseArgsResult
* Command->>Command: Process options
* Command->>Logging: setConfig(options)
* alt version requested
* Command->>getPackageVersion: Call
* getPackageVersion-->>Command: Return version
* else help requested
* Command->>Command: help(args)
* else banner requested
* Command->>printBanner: Call
* end
* Command->>Command: run(args)
* alt error occurs
* Command->>Command: Log error
* end
* Command-->>Command: Return result
*/
async execute(): Promise<R | string | void> {
const args: ParseArgsResult = UserInput.parseArgs(this.inputs);
const env = Environment.accumulate(DefaultLoggingConfig)
.accumulate(DefaultCommandValues)
.accumulate(args.values);
const { timestamp, verbose, version, help, logLevel, logStyle, banner } =
env;
this.log.setConfig({
...env,
timestamp: !!timestamp,
level: logLevel as LogLevel,
style: !!logStyle,
verbose: (verbose as number) || 0,
});
if (version) {
return getPackageVersion();
}
if (help) {
return this.help(args);
}
if (banner)
printBanner(
this.log.for(printBanner, {
timestamp: false,
style: false,
context: false,
logLevel: false,
})
);
let result;
try {
result = await this.run(env);
} catch (e: unknown) {
this.log.error(`Error while running provided cli function: ${e}`);
throw e;
}
return result as R;
}
}
Source