Source

core/cli/base-cli.ts

import { Command } from "commander";
import { VERSION } from "../../index";
import { Logger, Logging } from "@decaf-ts/logging";
import { addFabricToPath } from "../../utils-old/path";
import { EnvVars } from "../constants/env-vars";
import { printBanner, printBorder } from "../../utils-old/banner";
import fs from "fs";
import path from "path";
import { safeParseInt } from "../../utils/parsers";

/**
 * @class BaseCLI
 * @description Base class for creating command-line interfaces using Commander
 * @summary This class provides a foundation for building CLIs in the Fabric Weaver project.
 * It sets up a Commander program with basic configuration and logging.
 *
 * @example
 * class MyCLI extends BaseCLI {
 *   constructor() {
 *     super("my-cli", "My CLI description");
 *     this.setupCommands();
 *   }
 *
 *   private setupCommands() {
 *     this.program
 *       .command("hello")
 *       .description("Say hello")
 *       .action(() => console.log("Hello, world!"));
 *   }
 * }
 *
 * const cli = new MyCLI();
 * cli.run();
 */
export abstract class BaseCLI {
  protected program: Command;
  protected log: Logger;

  /**
   * @constructor
   * @param {string} name - The name of the CLI program
   * @param {string} description - A brief description of the CLI program
   */
  constructor(name: string, description: string) {
    this.program = new Command();
    this.log = Logging.for(this.constructor.name);

    this.program
      .name(name)
      .description(description)
      .version(VERSION)
      .option("-s, --skip-banner", "Suppress the Fabric Weaver banner")
      .option("-l, --limiter", "Supress the line after the command output")
      .hook("preAction", (cmd) => {
        const skipBanner = cmd.opts().skipBanner === true;
        const skipLimiter = cmd.opts().limiter === true;
        printBanner(skipBanner);
        this.log.debug(`Skip banner: ${skipBanner}`);
        this.log.debug(`Skip Limiter: ${skipLimiter}`);
        this.log.debug(`Starting ${this.program.name()} v${VERSION}`);
        addFabricToPath(process.env[EnvVars.FABRIC_BIN_FOLDER]);
      })
      .hook("postAction", (cmd) => {
        const skipLimiter = cmd.opts().limiter === true;
        printBorder(skipLimiter);
      });

    this.sleep();
    this.copy();
  }

  private sleep() {
    this.program
      .command("sleep")
      .option("--time <number>", "sleep time in seconds", safeParseInt)
      .action(async () => {
        const time = this.program.opts().time || 120;
        const ms = time * 1000;

        await new Promise((resolve) => setTimeout(resolve, ms));
      });
  }

  private copy() {
    this.program
      .command("copy")
      .description("Copy a file from origin to destination")
      .requiredOption("--origin <string>", "Origin file path")
      .requiredOption("--dest <string>", "Destination file path")
      .action(async (options) => {
        try {
          const { origin, dest } = options;

          // Check if the origin file exists
          if (!fs.existsSync(origin)) {
            this.log.error(`Origin file does not exist: ${origin}`);
            return;
          }

          // Ensure the destination directory exists
          const destDir = path.dirname(dest);
          if (!fs.existsSync(destDir)) {
            fs.mkdirSync(destDir, { recursive: true });
          }

          // Copy the file
          fs.copyFileSync(origin, dest);

          this.log.info(`File copied successfully from ${origin} to ${dest}`);
        } catch (error: unknown) {
          this.log.error(`Error copying file: ${(error as Error).message}`);
        }
      });
  }

  /**
   * @method run
   * @description Parses the command-line arguments and executes the appropriate command
   */
  public run(): void {
    this.program.parse(process.argv);
  }
}