mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	feature: cli framework
This commit is contained in:
		@@ -13,9 +13,14 @@ build-accesscontrol:
 | 
			
		||||
build-test:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.test.json
 | 
			
		||||
 | 
			
		||||
build-example:
 | 
			
		||||
build-example: build-tuiExample build-cliExample
 | 
			
		||||
 | 
			
		||||
build-tuiExample:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.tuiExample.json
 | 
			
		||||
 | 
			
		||||
build-cliExample:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.cliExample.json
 | 
			
		||||
 | 
			
		||||
sync:
 | 
			
		||||
    rsync --delete -r "./build/" "{{ sync-path }}"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										201
									
								
								src/cliExample/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/cliExample/main.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,201 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Example CLI application demonstrating the ccCLI framework
 | 
			
		||||
 * This example shows how to create a calculator CLI with global context injection
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Command, createCli, CliError } from "../lib/ccCLI/index";
 | 
			
		||||
import { Ok, Result } from "../lib/thirdparty/ts-result-es";
 | 
			
		||||
 | 
			
		||||
// 1. Define global context type
 | 
			
		||||
interface AppContext {
 | 
			
		||||
  appName: string;
 | 
			
		||||
  log: (message: string) => void;
 | 
			
		||||
  debugMode: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 2. Define individual commands
 | 
			
		||||
const addCommand: Command<AppContext> = {
 | 
			
		||||
  name: "add",
 | 
			
		||||
  description: "将两个数字相加",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "a", description: "第一个数字", required: true },
 | 
			
		||||
    { name: "b", description: "第二个数字", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }): Result<void, CliError> => {
 | 
			
		||||
    context.log(`在 '${context.appName}' 中执行 'add' 命令`);
 | 
			
		||||
 | 
			
		||||
    const a = tonumber(args.a as string);
 | 
			
		||||
    const b = tonumber(args.b as string);
 | 
			
		||||
 | 
			
		||||
    if (a === undefined || b === undefined) {
 | 
			
		||||
      print("错误: 参数必须是数字。");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const result = a + b;
 | 
			
		||||
    print(`${a} + ${b} = ${result}`);
 | 
			
		||||
 | 
			
		||||
    if (context.debugMode) {
 | 
			
		||||
      context.log(`计算结果: ${result}`);
 | 
			
		||||
    }
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const subtractCommand: Command<AppContext> = {
 | 
			
		||||
  name: "subtract",
 | 
			
		||||
  description: "将第二个数字从第一个数字中减去",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "a", description: "被减数", required: true },
 | 
			
		||||
    { name: "b", description: "减数", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }): Result<void, CliError> => {
 | 
			
		||||
    context.log(`在 '${context.appName}' 中执行 'subtract' 命令`);
 | 
			
		||||
 | 
			
		||||
    const a = tonumber(args.a as string);
 | 
			
		||||
    const b = tonumber(args.b as string);
 | 
			
		||||
 | 
			
		||||
    if (a === undefined || b === undefined) {
 | 
			
		||||
      print("错误: 参数必须是数字。");
 | 
			
		||||
      return Ok.EMPTY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const result = a - b;
 | 
			
		||||
    print(`${a} - ${b} = ${result}`);
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const greetCommand: Command<AppContext> = {
 | 
			
		||||
  name: "greet",
 | 
			
		||||
  description: "打印问候语",
 | 
			
		||||
  options: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "name",
 | 
			
		||||
      shortName: "n",
 | 
			
		||||
      description: "要问候的名字",
 | 
			
		||||
      defaultValue: "World",
 | 
			
		||||
    },
 | 
			
		||||
    { name: "times", shortName: "t", description: "重复次数", defaultValue: 1 },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ options, context }): Result<void, CliError> => {
 | 
			
		||||
    context.log(`在 '${context.appName}' 中执行 'greet' 命令`);
 | 
			
		||||
 | 
			
		||||
    const name = options.name as string;
 | 
			
		||||
    const times = tonumber(options.times as string) ?? 1;
 | 
			
		||||
 | 
			
		||||
    for (let i = 1; i <= times; i++) {
 | 
			
		||||
      print(`Hello, ${name}!`);
 | 
			
		||||
 | 
			
		||||
      if (context.debugMode && times > 1) {
 | 
			
		||||
        context.log(`问候 ${i}/${times}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Math subcommands group
 | 
			
		||||
const mathCommand: Command<AppContext> = {
 | 
			
		||||
  name: "math",
 | 
			
		||||
  description: "数学运算命令",
 | 
			
		||||
  subcommands: [addCommand, subtractCommand],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Config command with nested subcommands
 | 
			
		||||
const configShowCommand: Command<AppContext> = {
 | 
			
		||||
  name: "show",
 | 
			
		||||
  description: "显示当前配置",
 | 
			
		||||
  action: ({ context }): Result<void, CliError> => {
 | 
			
		||||
    print(`应用名称: ${context.appName}`);
 | 
			
		||||
    print(`调试模式: ${context.debugMode ? "开启" : "关闭"}`);
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const configSetCommand: Command<AppContext> = {
 | 
			
		||||
  name: "set",
 | 
			
		||||
  description: "设置配置项",
 | 
			
		||||
  args: [
 | 
			
		||||
    { name: "key", description: "配置键", required: true },
 | 
			
		||||
    { name: "value", description: "配置值", required: true },
 | 
			
		||||
  ],
 | 
			
		||||
  action: ({ args, context }): Result<void, CliError> => {
 | 
			
		||||
    const key = args.key as string;
 | 
			
		||||
    const value = args.value as string;
 | 
			
		||||
 | 
			
		||||
    context.log(`设置配置: ${key} = ${value}`);
 | 
			
		||||
    print(`配置 '${key}' 已设置为 '${value}'`);
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const configCommand: Command<AppContext> = {
 | 
			
		||||
  name: "config",
 | 
			
		||||
  description: "配置管理命令",
 | 
			
		||||
  subcommands: [configShowCommand, configSetCommand],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 3. Define root command
 | 
			
		||||
const rootCommand: Command<AppContext> = {
 | 
			
		||||
  name: "calculator",
 | 
			
		||||
  description: "一个功能丰富的计算器程序",
 | 
			
		||||
  options: [
 | 
			
		||||
    {
 | 
			
		||||
      name: "debug",
 | 
			
		||||
      shortName: "d",
 | 
			
		||||
      description: "启用调试模式",
 | 
			
		||||
      defaultValue: false,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  subcommands: [mathCommand, greetCommand, configCommand],
 | 
			
		||||
  action: ({ options, context }): Result<void, CliError> => {
 | 
			
		||||
    // Update debug mode from command line option
 | 
			
		||||
    const debugFromOption = options.debug as boolean;
 | 
			
		||||
    if (debugFromOption) {
 | 
			
		||||
      context.debugMode = true;
 | 
			
		||||
      context.log("调试模式已启用");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print(`欢迎使用 ${context.appName}!`);
 | 
			
		||||
    print("使用 --help 查看可用命令");
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 4. Create global context instance
 | 
			
		||||
const appContext: AppContext = {
 | 
			
		||||
  appName: "MyAwesomeCalculator",
 | 
			
		||||
  debugMode: false,
 | 
			
		||||
  log: (message) => {
 | 
			
		||||
    print(`[LOG] ${message}`);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 5. Create and export CLI handler
 | 
			
		||||
const cli = createCli(rootCommand, { globalContext: appContext });
 | 
			
		||||
const args = [...$vararg];
 | 
			
		||||
cli(args);
 | 
			
		||||
 | 
			
		||||
// Example usage (uncomment to test):
 | 
			
		||||
/*
 | 
			
		||||
// Simple math operations
 | 
			
		||||
cli(['math', 'add', '5', '7']);          // Output: 12
 | 
			
		||||
cli(['math', 'subtract', '10', '3']);    // Output: 7
 | 
			
		||||
 | 
			
		||||
// Greet with options
 | 
			
		||||
cli(['greet', '--name', 'TypeScript']);  // Output: Hello, TypeScript!
 | 
			
		||||
cli(['greet', '-n', 'World', '-t', '3']); // Output: Hello, World! (3 times)
 | 
			
		||||
 | 
			
		||||
// Config management
 | 
			
		||||
cli(['config', 'show']);                 // Shows current config
 | 
			
		||||
cli(['config', 'set', 'theme', 'dark']); // Sets config
 | 
			
		||||
 | 
			
		||||
// Help examples
 | 
			
		||||
cli(['--help']);                         // Shows root help
 | 
			
		||||
cli(['math', '--help']);                 // Shows math command help
 | 
			
		||||
cli(['config', 'set', '--help']);       // Shows config set help
 | 
			
		||||
 | 
			
		||||
// Debug mode
 | 
			
		||||
cli(['--debug', 'math', 'add', '1', '2']); // Enables debug logging
 | 
			
		||||
*/
 | 
			
		||||
							
								
								
									
										254
									
								
								src/lib/ccCLI/cli.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								src/lib/ccCLI/cli.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,254 @@
 | 
			
		||||
import { Ok, Err, Result } from "../thirdparty/ts-result-es";
 | 
			
		||||
import {
 | 
			
		||||
  Command,
 | 
			
		||||
  ActionContext,
 | 
			
		||||
  Argument,
 | 
			
		||||
  Option,
 | 
			
		||||
  CliError,
 | 
			
		||||
  ParsedInput,
 | 
			
		||||
  CommandResolution,
 | 
			
		||||
} from "./types";
 | 
			
		||||
import {
 | 
			
		||||
  parseArguments,
 | 
			
		||||
  validateRequiredArgs,
 | 
			
		||||
  validateRequiredOptions,
 | 
			
		||||
  normalizeOptions,
 | 
			
		||||
} from "./parser";
 | 
			
		||||
import { generateHelp, shouldShowHelp, generateCommandList } from "./help";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface CreateCliOptions
 | 
			
		||||
 * @description Optional configuration for the CLI handler.
 | 
			
		||||
 */
 | 
			
		||||
export interface CreateCliOptions<TContext extends object> {
 | 
			
		||||
  /** An optional global context object to be made available in all command actions. */
 | 
			
		||||
  globalContext?: TContext;
 | 
			
		||||
  /** An optional function to handle output. Defaults to the global `print` function. */
 | 
			
		||||
  writer?: (message: string) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a CLI handler function from a root command definition.
 | 
			
		||||
 * @param rootCommand The root command for the entire CLI application.
 | 
			
		||||
 * @param globalContext An optional global context object to be made available in all command actions.
 | 
			
		||||
 * @returns A function that takes command-line arguments and executes the appropriate command.
 | 
			
		||||
 */
 | 
			
		||||
export function createCli<TContext extends object>(
 | 
			
		||||
  rootCommand: Command<TContext>,
 | 
			
		||||
  options: CreateCliOptions<TContext> = {},
 | 
			
		||||
): (argv: string[]) => void {
 | 
			
		||||
  const { globalContext, writer = print } = options;
 | 
			
		||||
 | 
			
		||||
  return (argv: string[]): void => {
 | 
			
		||||
    // Check for top-level help flags before any parsing.
 | 
			
		||||
    if (shouldShowHelp(argv)) {
 | 
			
		||||
      writer(generateHelp(rootCommand));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parsedInput = parseArguments(argv);
 | 
			
		||||
    const executionResult = findCommand(
 | 
			
		||||
      rootCommand,
 | 
			
		||||
      parsedInput.commandPath,
 | 
			
		||||
    ).andThen((resolution) =>
 | 
			
		||||
      processAndExecute(resolution, parsedInput, globalContext, (msg: string) =>
 | 
			
		||||
        writer(msg),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (executionResult.isErr()) {
 | 
			
		||||
      const error = executionResult.error;
 | 
			
		||||
      writer(formatError(error, rootCommand));
 | 
			
		||||
 | 
			
		||||
      // If it was an unknown command, suggest alternatives.
 | 
			
		||||
      if (error.kind === "UnknownCommand") {
 | 
			
		||||
        const parent = findCommand(
 | 
			
		||||
          rootCommand,
 | 
			
		||||
          parsedInput.commandPath.slice(0, -1),
 | 
			
		||||
        );
 | 
			
		||||
        if (parent.isOk() && parent.value.command.subcommands) {
 | 
			
		||||
          writer(generateCommandList(parent.value.command.subcommands));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Processes the parsed input and executes the resolved command.
 | 
			
		||||
 * @param resolution The resolved command and its context.
 | 
			
		||||
 * @param parsedInput The raw parsed command-line input.
 | 
			
		||||
 * @param globalContext The global context for the CLI.
 | 
			
		||||
 * @returns A `Result` indicating the success or failure of the execution.
 | 
			
		||||
 */
 | 
			
		||||
function processAndExecute<TContext extends object>(
 | 
			
		||||
  resolution: CommandResolution<TContext>,
 | 
			
		||||
  parsedInput: ParsedInput,
 | 
			
		||||
  globalContext: TContext | undefined,
 | 
			
		||||
  writer: (message: string) => void,
 | 
			
		||||
): Result<void, CliError> {
 | 
			
		||||
  const { command, commandPath, remainingArgs } = resolution;
 | 
			
		||||
 | 
			
		||||
  // Handle requests for help on a specific command.
 | 
			
		||||
  if (shouldShowHelp([...remainingArgs, ...Object.keys(parsedInput.options)])) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If a command has subcommands but no action, show its help page.
 | 
			
		||||
  if (
 | 
			
		||||
    command.subcommands &&
 | 
			
		||||
    command.subcommands.length > 0 &&
 | 
			
		||||
    command.action === undefined
 | 
			
		||||
  ) {
 | 
			
		||||
    writer(generateHelp(command, commandPath));
 | 
			
		||||
    return Ok.EMPTY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // A command that is meant to be executed must have an action.
 | 
			
		||||
  if (command.action === undefined) {
 | 
			
		||||
    return new Err({
 | 
			
		||||
      kind: "NoAction",
 | 
			
		||||
      commandPath: [...commandPath, command.name],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return processArguments(
 | 
			
		||||
    command.args ?? [],
 | 
			
		||||
    remainingArgs,
 | 
			
		||||
    parsedInput.remaining,
 | 
			
		||||
  )
 | 
			
		||||
    .andThen((args) => {
 | 
			
		||||
      return processOptions(command.options ?? [], parsedInput.options).map(
 | 
			
		||||
        (options) => ({ args, options }),
 | 
			
		||||
      );
 | 
			
		||||
    })
 | 
			
		||||
    .andThen(({ args, options }) => {
 | 
			
		||||
      const context: ActionContext<TContext> = {
 | 
			
		||||
        args,
 | 
			
		||||
        options,
 | 
			
		||||
        context: globalContext!,
 | 
			
		||||
      };
 | 
			
		||||
      // Finally, execute the command's action.
 | 
			
		||||
      return command.action!(context);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Finds the target command based on a given path.
 | 
			
		||||
 * @param rootCommand The command to start searching from.
 | 
			
		||||
 * @param commandPath An array of strings representing the path to the command.
 | 
			
		||||
 * @returns A `Result` containing the `CommandResolution` or an `UnknownCommandError`.
 | 
			
		||||
 */
 | 
			
		||||
function findCommand<TContext extends object>(
 | 
			
		||||
  rootCommand: Command<TContext>,
 | 
			
		||||
  commandPath: string[],
 | 
			
		||||
): Result<CommandResolution<TContext>, CliError> {
 | 
			
		||||
  let currentCommand = rootCommand;
 | 
			
		||||
  const resolvedPath: string[] = [];
 | 
			
		||||
  let i = 0;
 | 
			
		||||
 | 
			
		||||
  for (const name of commandPath) {
 | 
			
		||||
    const subcommand = currentCommand.subcommands?.find(
 | 
			
		||||
      (cmd) => cmd.name === name,
 | 
			
		||||
    );
 | 
			
		||||
    if (!subcommand) {
 | 
			
		||||
      // Part of the path was not a valid command, so the rest are arguments.
 | 
			
		||||
      return new Err({ kind: "UnknownCommand", commandName: name });
 | 
			
		||||
    }
 | 
			
		||||
    currentCommand = subcommand;
 | 
			
		||||
    resolvedPath.push(name);
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const remainingArgs = commandPath.slice(i);
 | 
			
		||||
  return new Ok({
 | 
			
		||||
    command: currentCommand,
 | 
			
		||||
    commandPath: resolvedPath,
 | 
			
		||||
    remainingArgs,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Processes and validates command arguments from the raw input.
 | 
			
		||||
 * @param argDefs The argument definitions for the command.
 | 
			
		||||
 * @param remainingArgs The positional arguments captured during command resolution.
 | 
			
		||||
 * @param additionalArgs Any extra arguments parsed after options.
 | 
			
		||||
 * @returns A `Result` with the processed arguments record or a `MissingArgumentError`.
 | 
			
		||||
 */
 | 
			
		||||
function processArguments(
 | 
			
		||||
  argDefs: Argument[],
 | 
			
		||||
  remainingArgs: string[],
 | 
			
		||||
  additionalArgs: string[],
 | 
			
		||||
): Result<Record<string, unknown>, CliError> {
 | 
			
		||||
  const args: Record<string, unknown> = {};
 | 
			
		||||
  const allArgs = [...remainingArgs, ...additionalArgs];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < argDefs.length; i++) {
 | 
			
		||||
    const argDef = argDefs[i];
 | 
			
		||||
    if (i < allArgs.length) {
 | 
			
		||||
      args[argDef.name] = allArgs[i];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const requiredArgs = argDefs
 | 
			
		||||
    .filter((arg) => arg.required ?? false)
 | 
			
		||||
    .map((arg) => arg.name);
 | 
			
		||||
  return validateRequiredArgs(args, requiredArgs).map(() => args);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Processes and validates command options from the raw input.
 | 
			
		||||
 * @param optionDefs The option definitions for the command.
 | 
			
		||||
 * @param rawOptions The raw options parsed from the command line.
 | 
			
		||||
 * @returns A `Result` with the processed options record or a `MissingOptionError`.
 | 
			
		||||
 */
 | 
			
		||||
function processOptions(
 | 
			
		||||
  optionDefs: Option[],
 | 
			
		||||
  rawOptions: Record<string, unknown>,
 | 
			
		||||
): Result<Record<string, unknown>, CliError> {
 | 
			
		||||
  const shortToLongMap: Record<string, string> = {};
 | 
			
		||||
  const defaultValues: Record<string, unknown> = {};
 | 
			
		||||
 | 
			
		||||
  for (const optionDef of optionDefs) {
 | 
			
		||||
    if (optionDef.shortName !== undefined) {
 | 
			
		||||
      shortToLongMap[optionDef.shortName] = optionDef.name;
 | 
			
		||||
    }
 | 
			
		||||
    if (optionDef.defaultValue !== undefined) {
 | 
			
		||||
      defaultValues[optionDef.name] = optionDef.defaultValue;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const normalizedOptions = normalizeOptions(rawOptions, shortToLongMap);
 | 
			
		||||
  const options = { ...defaultValues, ...normalizedOptions };
 | 
			
		||||
 | 
			
		||||
  const requiredOptions = optionDefs
 | 
			
		||||
    .filter((opt) => opt.required ?? false)
 | 
			
		||||
    .map((opt) => opt.name);
 | 
			
		||||
  return validateRequiredOptions(options, requiredOptions).map(() => options);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Formats a `CliError` into a user-friendly string.
 | 
			
		||||
 * @param error The `CliError` object.
 | 
			
		||||
 * @param rootCommand The root command, used for context in some errors.
 | 
			
		||||
 * @returns A formatted error message string.
 | 
			
		||||
 */
 | 
			
		||||
function formatError<TContext extends object>(
 | 
			
		||||
  error: CliError,
 | 
			
		||||
  _rootCommand: Command<TContext>,
 | 
			
		||||
): string {
 | 
			
		||||
  switch (error.kind) {
 | 
			
		||||
    case "UnknownCommand":
 | 
			
		||||
      return `Error: Unknown command "${error.commandName}".`;
 | 
			
		||||
    case "MissingArgument":
 | 
			
		||||
      return `Error: Missing required argument "${error.argName}".`;
 | 
			
		||||
    case "MissingOption":
 | 
			
		||||
      return `Error: Missing required option "--${error.optionName}".`;
 | 
			
		||||
    case "NoAction":
 | 
			
		||||
      return `Error: Command "${error.commandPath.join(" ")}" is not runnable.`;
 | 
			
		||||
    default:
 | 
			
		||||
      // This should be unreachable if all error kinds are handled.
 | 
			
		||||
      return "An unexpected error occurred.";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								src/lib/ccCLI/help.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/lib/ccCLI/help.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import { Command } from "./types";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates a well-formatted help string for a given command.
 | 
			
		||||
 * @param command The command to generate help for.
 | 
			
		||||
 * @param commandPath The path to the command, used for showing the full command name.
 | 
			
		||||
 * @returns A formatted string containing the complete help message.
 | 
			
		||||
 */
 | 
			
		||||
export function generateHelp<TContext extends object>(
 | 
			
		||||
  command: Command<TContext>,
 | 
			
		||||
  commandPath: string[] = [],
 | 
			
		||||
): string {
 | 
			
		||||
  const lines: string[] = [];
 | 
			
		||||
  const fullCommandName = [...commandPath, command.name].join(" ");
 | 
			
		||||
 | 
			
		||||
  // Description
 | 
			
		||||
  if (command.description !== undefined) {
 | 
			
		||||
    lines.push(command.description);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Usage
 | 
			
		||||
  const usageParts: string[] = ["Usage:", fullCommandName];
 | 
			
		||||
  if (command.options && command.options.length > 0) {
 | 
			
		||||
    usageParts.push("[OPTIONS]");
 | 
			
		||||
  }
 | 
			
		||||
  if (command.subcommands && command.subcommands.length > 0) {
 | 
			
		||||
    usageParts.push("<COMMAND>");
 | 
			
		||||
  }
 | 
			
		||||
  if (command.args && command.args.length > 0) {
 | 
			
		||||
    for (const arg of command.args) {
 | 
			
		||||
      usageParts.push(
 | 
			
		||||
        arg.required === true ? `<${arg.name}>` : `[${arg.name}]`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  lines.push("\n" + usageParts.join(" "));
 | 
			
		||||
 | 
			
		||||
  // Arguments
 | 
			
		||||
  if (command.args && command.args.length > 0) {
 | 
			
		||||
    lines.push("\nArguments:");
 | 
			
		||||
    for (const arg of command.args) {
 | 
			
		||||
      const requiredText = arg.required === true ? " (required)" : "";
 | 
			
		||||
      lines.push(`  ${arg.name.padEnd(20)} ${arg.description}${requiredText}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Options
 | 
			
		||||
  if (command.options && command.options.length > 0) {
 | 
			
		||||
    lines.push("\nOptions:");
 | 
			
		||||
    for (const option of command.options) {
 | 
			
		||||
      const short =
 | 
			
		||||
        option.shortName !== undefined ? `-${option.shortName}, ` : "    ";
 | 
			
		||||
      const long = `--${option.name}`;
 | 
			
		||||
      const display = `${short}${long}`.padEnd(20);
 | 
			
		||||
      const requiredText = option.required === true ? " (required)" : "";
 | 
			
		||||
      const defaultText =
 | 
			
		||||
        option.defaultValue !== undefined
 | 
			
		||||
          ? ` (default: ${textutils.serialise(option.defaultValue!)})`
 | 
			
		||||
          : "";
 | 
			
		||||
      lines.push(
 | 
			
		||||
        `  ${display} ${option.description}${requiredText}${defaultText}`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Subcommands
 | 
			
		||||
  if (command.subcommands && command.subcommands.length > 0) {
 | 
			
		||||
    lines.push("\nCommands:");
 | 
			
		||||
    for (const subcommand of command.subcommands) {
 | 
			
		||||
      lines.push(`  ${subcommand.name.padEnd(20)} ${subcommand.description}`);
 | 
			
		||||
    }
 | 
			
		||||
    lines.push(
 | 
			
		||||
      `\nRun '${fullCommandName} <COMMAND> --help' for more information on a command.`,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return lines.join("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generates a simple list of available commands, typically for error messages.
 | 
			
		||||
 * @param commands An array of command objects.
 | 
			
		||||
 * @returns A formatted string listing the available commands.
 | 
			
		||||
 */
 | 
			
		||||
export function generateCommandList<TContext extends object>(
 | 
			
		||||
  commands: Command<TContext>[],
 | 
			
		||||
): string {
 | 
			
		||||
  if (commands.length === 0) {
 | 
			
		||||
    return "No commands available.";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const lines: string[] = ["Available commands:"];
 | 
			
		||||
  for (const command of commands) {
 | 
			
		||||
    lines.push(`  ${command.name.padEnd(20)} ${command.description}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return lines.join("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if the `--help` or `-h` flag is present in the arguments.
 | 
			
		||||
 * @param argv An array of command-line arguments.
 | 
			
		||||
 * @returns `true` if a help flag is found, otherwise `false`.
 | 
			
		||||
 */
 | 
			
		||||
export function shouldShowHelp(argv: string[]): boolean {
 | 
			
		||||
  return argv.includes("--help") || argv.includes("-h");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/lib/ccCLI/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/lib/ccCLI/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
/**
 | 
			
		||||
 * CC:Tweaked CLI Framework
 | 
			
		||||
 *
 | 
			
		||||
 * A functional-style CLI framework for CC:Tweaked and TSTL.
 | 
			
		||||
 * This framework provides a declarative way to define command-line interfaces with support
 | 
			
		||||
 * for nested commands, arguments, options, and Result-based error handling.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// --- Core public API ---
 | 
			
		||||
export { createCli } from "./cli";
 | 
			
		||||
 | 
			
		||||
// --- Type definitions for creating commands ---
 | 
			
		||||
export type {
 | 
			
		||||
  Command,
 | 
			
		||||
  Argument,
 | 
			
		||||
  Option,
 | 
			
		||||
  ActionContext,
 | 
			
		||||
  CliError,
 | 
			
		||||
  UnknownCommandError,
 | 
			
		||||
  MissingArgumentError,
 | 
			
		||||
  MissingOptionError,
 | 
			
		||||
  NoActionError,
 | 
			
		||||
} from "./types";
 | 
			
		||||
 | 
			
		||||
// --- Utility functions for help generation and advanced parsing ---
 | 
			
		||||
export { generateHelp, generateCommandList, shouldShowHelp } from "./help";
 | 
			
		||||
export {
 | 
			
		||||
  parseArguments,
 | 
			
		||||
  validateRequiredArgs,
 | 
			
		||||
  validateRequiredOptions,
 | 
			
		||||
  normalizeOptions,
 | 
			
		||||
} from "./parser";
 | 
			
		||||
							
								
								
									
										151
									
								
								src/lib/ccCLI/parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/lib/ccCLI/parser.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
import { Ok, Err, Result } from "../thirdparty/ts-result-es";
 | 
			
		||||
import { ParsedInput, MissingArgumentError, MissingOptionError } from "./types";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Parses command line arguments into a structured format.
 | 
			
		||||
 * This function does not validate arguments or options, it only parses the raw input.
 | 
			
		||||
 * @param argv Array of command line arguments (e.g., from `os.pullEvent`).
 | 
			
		||||
 * @returns A `ParsedInput` object containing the command path, options, and remaining args.
 | 
			
		||||
 */
 | 
			
		||||
export function parseArguments(argv: string[]): ParsedInput {
 | 
			
		||||
  const result: ParsedInput = {
 | 
			
		||||
    commandPath: [],
 | 
			
		||||
    options: {},
 | 
			
		||||
    remaining: [],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let i = 0;
 | 
			
		||||
  let inOptions = false;
 | 
			
		||||
 | 
			
		||||
  while (i < argv.length) {
 | 
			
		||||
    const arg = argv[i];
 | 
			
		||||
 | 
			
		||||
    if (arg === undefined) {
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle double dash (--) - everything after is treated as a remaining argument.
 | 
			
		||||
    if (arg === "--") {
 | 
			
		||||
      result.remaining.push(...argv.slice(i + 1));
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle long options (--option or --option=value)
 | 
			
		||||
    if (arg.startsWith("--")) {
 | 
			
		||||
      inOptions = true;
 | 
			
		||||
      const equalsIndex = arg.indexOf("=");
 | 
			
		||||
 | 
			
		||||
      if (equalsIndex !== -1) {
 | 
			
		||||
        // --option=value format
 | 
			
		||||
        const optionName = arg.slice(2, equalsIndex);
 | 
			
		||||
        const optionValue = arg.slice(equalsIndex + 1);
 | 
			
		||||
        result.options[optionName] = optionValue;
 | 
			
		||||
      } else {
 | 
			
		||||
        // --option [value] format
 | 
			
		||||
        const optionName = arg.slice(2);
 | 
			
		||||
        if (
 | 
			
		||||
          i + 1 < argv.length &&
 | 
			
		||||
          argv[i + 1] !== undefined &&
 | 
			
		||||
          !argv[i + 1].startsWith("-")
 | 
			
		||||
        ) {
 | 
			
		||||
          result.options[optionName] = argv[i + 1];
 | 
			
		||||
          i++; // Skip the value argument
 | 
			
		||||
        } else {
 | 
			
		||||
          // Boolean flag
 | 
			
		||||
          result.options[optionName] = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle short options (-o or -o value)
 | 
			
		||||
    else if (arg.startsWith("-") && arg.length > 1) {
 | 
			
		||||
      inOptions = true;
 | 
			
		||||
      const optionName = arg.slice(1);
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        i + 1 < argv.length &&
 | 
			
		||||
        argv[i + 1] !== undefined &&
 | 
			
		||||
        !argv[i + 1].startsWith("-")
 | 
			
		||||
      ) {
 | 
			
		||||
        result.options[optionName] = argv[i + 1];
 | 
			
		||||
        i++; // Skip the value argument
 | 
			
		||||
      } else {
 | 
			
		||||
        // Boolean flag
 | 
			
		||||
        result.options[optionName] = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle positional arguments and commands
 | 
			
		||||
    else {
 | 
			
		||||
      if (!inOptions) {
 | 
			
		||||
        // Before any options, treat as part of the command path
 | 
			
		||||
        result.commandPath.push(arg);
 | 
			
		||||
      } else {
 | 
			
		||||
        // After options have started, treat as a remaining argument
 | 
			
		||||
        result.remaining.push(arg);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates that all required arguments are present in the parsed arguments.
 | 
			
		||||
 * @param parsedArgs A record of the arguments that were parsed.
 | 
			
		||||
 * @param requiredArgs An array of names of required arguments.
 | 
			
		||||
 * @returns An `Ok` result if validation passes, otherwise an `Err` with a `MissingArgumentError`.
 | 
			
		||||
 */
 | 
			
		||||
export function validateRequiredArgs(
 | 
			
		||||
  parsedArgs: Record<string, unknown>,
 | 
			
		||||
  requiredArgs: string[],
 | 
			
		||||
): Result<void, MissingArgumentError> {
 | 
			
		||||
  for (const argName of requiredArgs) {
 | 
			
		||||
    if (!(argName in parsedArgs) || parsedArgs[argName] === undefined) {
 | 
			
		||||
      return new Err({ kind: "MissingArgument", argName });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return Ok.EMPTY;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validates that all required options are present in the parsed options.
 | 
			
		||||
 * @param parsedOptions A record of the options that were parsed.
 | 
			
		||||
 * @param requiredOptions An array of names of required options.
 | 
			
		||||
 * @returns An `Ok` result if validation passes, otherwise an `Err` with a `MissingOptionError`.
 | 
			
		||||
 */
 | 
			
		||||
export function validateRequiredOptions(
 | 
			
		||||
  parsedOptions: Record<string, unknown>,
 | 
			
		||||
  requiredOptions: string[],
 | 
			
		||||
): Result<void, MissingOptionError> {
 | 
			
		||||
  for (const optionName of requiredOptions) {
 | 
			
		||||
    if (
 | 
			
		||||
      !(optionName in parsedOptions) ||
 | 
			
		||||
      parsedOptions[optionName] === undefined
 | 
			
		||||
    ) {
 | 
			
		||||
      return new Err({ kind: "MissingOption", optionName });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return Ok.EMPTY;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Normalizes option names by mapping short names to their corresponding long names.
 | 
			
		||||
 * @param options The raw parsed options record (may contain short names).
 | 
			
		||||
 * @param optionMapping A map from short option names to long option names.
 | 
			
		||||
 * @returns A new options record with all short names replaced by long names.
 | 
			
		||||
 */
 | 
			
		||||
export function normalizeOptions(
 | 
			
		||||
  options: Record<string, unknown>,
 | 
			
		||||
  optionMapping: Record<string, string>,
 | 
			
		||||
): Record<string, unknown> {
 | 
			
		||||
  const normalized: Record<string, unknown> = {};
 | 
			
		||||
 | 
			
		||||
  for (const [key, value] of Object.entries(options)) {
 | 
			
		||||
    const normalizedKey = optionMapping[key] ?? key;
 | 
			
		||||
    normalized[normalizedKey] = value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return normalized;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										144
									
								
								src/lib/ccCLI/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/lib/ccCLI/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
import { Result } from "../thirdparty/ts-result-es";
 | 
			
		||||
 | 
			
		||||
// --- Error Types ---
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an error when an unknown command is used.
 | 
			
		||||
 * @property commandName - The name of the command that was not found.
 | 
			
		||||
 */
 | 
			
		||||
export interface UnknownCommandError {
 | 
			
		||||
  kind: "UnknownCommand";
 | 
			
		||||
  commandName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an error when a required argument is missing.
 | 
			
		||||
 * @property argName - The name of the missing argument.
 | 
			
		||||
 */
 | 
			
		||||
export interface MissingArgumentError {
 | 
			
		||||
  kind: "MissingArgument";
 | 
			
		||||
  argName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an error when a required option is missing.
 | 
			
		||||
 * @property optionName - The name of the missing option.
 | 
			
		||||
 */
 | 
			
		||||
export interface MissingOptionError {
 | 
			
		||||
  kind: "MissingOption";
 | 
			
		||||
  optionName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an error when a command that requires an action has none.
 | 
			
		||||
 * @property commandPath - The path to the command without an action.
 | 
			
		||||
 */
 | 
			
		||||
export interface NoActionError {
 | 
			
		||||
  kind: "NoAction";
 | 
			
		||||
  commandPath: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A union of all possible CLI-related errors.
 | 
			
		||||
 * This allows for exhaustive error handling using pattern matching on the `kind` property.
 | 
			
		||||
 */
 | 
			
		||||
export type CliError =
 | 
			
		||||
  | UnknownCommandError
 | 
			
		||||
  | MissingArgumentError
 | 
			
		||||
  | MissingOptionError
 | 
			
		||||
  | NoActionError;
 | 
			
		||||
 | 
			
		||||
// --- Core CLI Structures ---
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface Argument
 | 
			
		||||
 * @description Defines a command-line argument for a command.
 | 
			
		||||
 */
 | 
			
		||||
export interface Argument {
 | 
			
		||||
  /** The name of the argument, used to access its value. */
 | 
			
		||||
  name: string;
 | 
			
		||||
  /** A brief description of what the argument does, shown in help messages. */
 | 
			
		||||
  description: string;
 | 
			
		||||
  /** Whether the argument is required. Defaults to false. */
 | 
			
		||||
  required?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface Option
 | 
			
		||||
 * @description Defines a command-line option (also known as a flag).
 | 
			
		||||
 */
 | 
			
		||||
export interface Option {
 | 
			
		||||
  /** The long name of the option (e.g., "verbose" for `--verbose`). */
 | 
			
		||||
  name: string;
 | 
			
		||||
  /** An optional short name for the option (e.g., "v" for `-v`). */
 | 
			
		||||
  shortName?: string;
 | 
			
		||||
  /** A brief description of what the option does, shown in help messages. */
 | 
			
		||||
  description: string;
 | 
			
		||||
  /** Whether the option is required. Defaults to false. */
 | 
			
		||||
  required?: boolean;
 | 
			
		||||
  /** The default value for the option if it's not provided. */
 | 
			
		||||
  defaultValue?: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface ActionContext
 | 
			
		||||
 * @description The context object passed to a command's action handler.
 | 
			
		||||
 * @template TContext - The type of the global context object.
 | 
			
		||||
 */
 | 
			
		||||
export interface ActionContext<TContext extends object> {
 | 
			
		||||
  /** A record of parsed argument values, keyed by argument name. */
 | 
			
		||||
  args: Record<string, unknown>;
 | 
			
		||||
  /** A record of parsed option values, keyed by option name. */
 | 
			
		||||
  options: Record<string, unknown>;
 | 
			
		||||
  /** The global context object, shared across all commands. */
 | 
			
		||||
  context: TContext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface Command
 | 
			
		||||
 * @description Defines a CLI command, which can have its own arguments, options, and subcommands.
 | 
			
		||||
 * @template TContext - The type of the global context object.
 | 
			
		||||
 */
 | 
			
		||||
export interface Command<TContext extends object> {
 | 
			
		||||
  /** The name of the command. */
 | 
			
		||||
  name: string;
 | 
			
		||||
  /** A brief description of the command, shown in help messages. */
 | 
			
		||||
  description: string;
 | 
			
		||||
  /** An array of argument definitions for the command. */
 | 
			
		||||
  args?: Argument[];
 | 
			
		||||
  /** An array of option definitions for the command. */
 | 
			
		||||
  options?: Option[];
 | 
			
		||||
  /**
 | 
			
		||||
   * The function to execute when the command is run.
 | 
			
		||||
   * It receives an `ActionContext` object.
 | 
			
		||||
   * Should return a `Result` to indicate success or failure.
 | 
			
		||||
   */
 | 
			
		||||
  action?: (context: ActionContext<TContext>) => Result<void, CliError>;
 | 
			
		||||
  /** An array of subcommands, allowing for nested command structures. */
 | 
			
		||||
  subcommands?: Command<TContext>[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Parsing and Execution Internals ---
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @interface ParsedInput
 | 
			
		||||
 * @description The raw output from the initial argument parsing stage.
 | 
			
		||||
 */
 | 
			
		||||
export interface ParsedInput {
 | 
			
		||||
  /** The identified command path from the arguments. */
 | 
			
		||||
  commandPath: string[];
 | 
			
		||||
  /** A record of raw option values. */
 | 
			
		||||
  options: Record<string, unknown>;
 | 
			
		||||
  /** Any remaining arguments that were not parsed as part of the command path or options. */
 | 
			
		||||
  remaining: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type CommandResolution
 | 
			
		||||
 * @description The result of resolving a command path to a specific command.
 | 
			
		||||
 */
 | 
			
		||||
export interface CommandResolution<TContext extends object> {
 | 
			
		||||
  command: Command<TContext>;
 | 
			
		||||
  commandPath: string[];
 | 
			
		||||
  remainingArgs: string[];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								tsconfig.cliExample.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tsconfig.cliExample.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://raw.githubusercontent.com/MCJack123/TypeScriptToLua/master/tsconfig-schema.json",
 | 
			
		||||
  "extends": "./tsconfig.json",
 | 
			
		||||
  "tstl": {
 | 
			
		||||
    "luaBundle": "build/cliExample.lua",
 | 
			
		||||
    "luaBundleEntry": "src/cliExample/main.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/cliExample/*.ts", "src/lib/ccCLI/*.ts"]
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user