All files / cli Cli.ts

100% Statements 37/37
100% Branches 37/37
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 142                                                          28x     28x 1x 1x     27x 1x 1x       26x 1x                   25x     28x     28x     28x 2x 2x       23x 1x 1x     1x     22x 1x 1x 1x     21x 21x 1x 1x     20x 1x     1x 1x       19x 1x         1x     18x                     25x                                   25x          
/**
 * Cli
 * Main CLI orchestrator that handles argument parsing, config loading,
 * and dispatching to appropriate commands
 */
 
import { dirname, resolve } from "node:path";
import NodeFileSystem from "../cnx/PARSE/1-Discover/NodeFileSystem";
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: cnx <file.cnx>");
      return { shouldRun: false, exitCode: 1 };
    }
 
    const resolvedInput = resolve(config.input);
    if (!NodeFileSystem.instance.exists(resolvedInput)) {
      console.error(`Error: Input not found: ${config.input}`);
      return { shouldRun: false, exitCode: 1 };
    }
 
    if (NodeFileSystem.instance.isDirectory(resolvedInput)) {
      console.error(
        "Error: Directory input not supported. Specify an entry point file.",
      );
      console.error(`Example: cnx ${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,
      verbose: args.verbose,
      // #1844: `--cpp`/`--no-cpp` wins; otherwise the config's, unset when
      // neither says, so 1.1 detects the mode rather than reading a false.
      cppRequired: args.cppRequired ?? fileConfig.cppRequired,
      noCache: args.noCache || fileConfig.noCache === true,
      parseOnly: args.parseOnly,
      headerOutDir: args.headerOutDir ?? fileConfig.headerOut,
      target: args.target ?? fileConfig.target,
      pioEnv: args.pioEnv,
      debugMode: args.debugMode || fileConfig.debugMode,
    };
 
    return PathNormalizer.normalizeConfig(rawConfig);
  }
}
 
export default Cli;