All files / cli Cli.ts

100% Statements 37/37
100% Branches 40/40
100% Functions 2/2
100% Lines 37/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141                                                          24x     24x 1x 1x     23x 1x 1x       22x 1x                   21x     24x     24x     24x 2x 2x       19x 1x 1x     1x     18x 1x 1x 1x     17x 17x 1x 1x     16x 1x     1x 1x       15x 1x         1x     14x                     21x                                 21x          
/**
 * Cli
 * Main CLI orchestrator that handles argument parsing, config loading,
 * and dispatching to appropriate commands
 */
 
import { dirname, resolve } from "node:path";
import { existsSync, statSync } from "node:fs";
import ArgParser from "./ArgParser";
import ConfigLoader from "./ConfigLoader";
import ConfigPrinter from "./ConfigPrinter";
import PlatformIOCommand from "./PlatformIOCommand";
import CleanCommand from "./CleanCommand";
import PathNormalizer from "./PathNormalizer";
import ICliResult from "./types/ICliResult";
import ICliConfig from "./types/ICliConfig";
import IParsedArgs from "./types/IParsedArgs";
import IFileConfig from "./types/IFileConfig";
 
/**
 * Main CLI orchestrator
 */
class Cli {
  /**
   * Run the CLI
   * @returns CLI result with configuration for Runner if transpilation should run
   */
  static run(): ICliResult {
    // Parse arguments (yargs handles --help and --version automatically)
    const args = ArgParser.parse(process.argv);
 
    // Early exits for PlatformIO commands
    if (args.pioInstall) {
      PlatformIOCommand.install();
      return { shouldRun: false, exitCode: 0 };
    }
 
    if (args.pioUninstall) {
      PlatformIOCommand.uninstall();
      return { shouldRun: false, exitCode: 0 };
    }
 
    // Early exit for serve mode (JSON-RPC server)
    if (args.serveMode) {
      return {
        shouldRun: false,
        exitCode: 0,
        serveMode: true,
        serveDebug: args.verbose,
      };
    }
 
    // Load config file (searches up from input file directory)
    const configDir =
      args.inputFiles.length > 0
        ? dirname(resolve(args.inputFiles[0]))
        : process.cwd();
    const fileConfig = ConfigLoader.load(configDir);
 
    // Merge CLI args with file config
    const config = this.mergeConfig(args, fileConfig);
 
    // Handle --config: show effective configuration
    if (args.showConfig) {
      ConfigPrinter.showConfig(config, fileConfig);
      return { shouldRun: false, exitCode: 0 };
    }
 
    // Validate single entry point
    if (args.inputFiles.length > 1) {
      console.error("Error: Only one entry point file is supported");
      console.error(
        "Other files are discovered automatically via #include directives",
      );
      return { shouldRun: false, exitCode: 1 };
    }
 
    if (!config.input) {
      console.error("Error: No input file specified");
      console.error("Usage: cnext <file.cnx>");
      return { shouldRun: false, exitCode: 1 };
    }
 
    const resolvedInput = resolve(config.input);
    if (!existsSync(resolvedInput)) {
      console.error(`Error: Input not found: ${config.input}`);
      return { shouldRun: false, exitCode: 1 };
    }
 
    if (statSync(resolvedInput).isDirectory()) {
      console.error(
        "Error: Directory input not supported. Specify an entry point file.",
      );
      console.error(`Example: cnext ${config.input}/main.cnx`);
      return { shouldRun: false, exitCode: 1 };
    }
 
    // Handle --clean: delete generated files
    if (args.cleanMode) {
      CleanCommand.execute(
        config.input,
        config.outputPath,
        config.headerOutDir,
      );
      return { shouldRun: false, exitCode: 0 };
    }
 
    return { shouldRun: true, exitCode: 0, config };
  }
 
  /**
   * Merge CLI arguments with file configuration
   * CLI flags take precedence over config file values
   */
  private static mergeConfig(
    args: IParsedArgs,
    fileConfig: IFileConfig,
  ): ICliConfig {
    const rawConfig: ICliConfig = {
      input: args.inputFiles[0] ?? "",
      outputPath: args.outputPath || fileConfig.output || "",
      // Merge include dirs: config includes come first, CLI includes override/append
      includeDirs: [...(fileConfig.include ?? []), ...args.includeDirs],
      defines: args.defines,
      preprocess: args.preprocess,
      verbose: args.verbose,
      cppRequired: args.cppRequired || fileConfig.cppRequired || false,
      noCache: args.noCache || fileConfig.noCache === true,
      parseOnly: args.parseOnly,
      headerOutDir: args.headerOutDir ?? fileConfig.headerOut,
      basePath: args.basePath ?? fileConfig.basePath,
      target: args.target ?? fileConfig.target,
      debugMode: args.debugMode || fileConfig.debugMode,
    };
 
    return PathNormalizer.normalizeConfig(rawConfig);
  }
}
 
export default Cli;