feature: cli framework

This commit is contained in:
2025-10-27 11:55:32 +08:00
parent 2ab091d939
commit f7167576cd
8 changed files with 904 additions and 1 deletions

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"]
}