UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

776 lines (775 loc) 33.7 kB
"use strict"; /** * CommandContextFactory - Creates fully-hydrated ICommandContext from CLI options * * ARCHITECTURE DOCUMENTATION * ========================== * * This factory is the central point for: * 1. Parsing Commander.js options into typed structures * 2. Detecting and loading projects (moved from loadProjects() in index.ts) * 3. Setting up storage abstractions * 4. Creating worker pools * 5. Configuring logging * * ENTRY POINT: * CommandContextFactory.create() is called from index.ts after Commander.js * parses the command line. It returns a fully-hydrated ICommandContext that * commands use to access projects, storage, logging, and worker pools. * * KEY METHODS: * - create(): Main entry point, orchestrates all setup * - loadProjects(): Detects and loads projects from input path * - parseThreads(): Converts thread option to number * - parseOutputType(): Converts output type string to enum * - resolveSuite(): Converts suite string to ProjectInfoSuite enum * * PROJECT DETECTION LOGIC: * The project detection supports several modes: * * 1. Single File Mode (-f, --file): * - Input is a single file (e.g., .mcaddon, .mcpack, .zip) * - Creates one project from that file * * 2. Multi-Level Multi-Project: * - Root folder contains subfolders * - Each subfolder contains .zip/.mcaddon files + optional .data.json * - Each zip becomes a separate project * * 3. Children-of-Folder Multi-Project: * - Root folder contains zip files directly OR subfolders that are projects * - Subfolders with manifest.json, behavior_packs/, etc. are detected * * 4. Single Project (fallback): * - Treat the entire input folder as one project * * STORAGE SETUP: * - inputStorage: NodeStorage pointing to input folder (read-only for non-edit commands) * - outputStorage: NodeStorage pointing to output folder (or same as input) * - Additional storage for Minecraft paths, deployment, etc. * * WORKER POOL: * - Created via createWorkerPool() from WorkerPool.ts * - Supports parallel execution with configurable thread count * - Falls back to single-threaded mode when threads=1 * * LOGGING: * - Created via createLogger() from Logger.ts * - Supports verbose, quiet, and debug modes * - ConsoleLogger for normal output, SilentLogger for testing * * KEY FILES: * - ICommandContext.ts: Interface definitions * - WorkerPool.ts: Parallel execution * - Logger.ts: Logging implementation * - ClUtils.ts: Legacy utilities (being migrated) */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandContextFactory = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const Project_1 = __importStar(require("../../app/Project")); const NodeStorage_1 = __importDefault(require("../../local/NodeStorage")); const StorageUtilities_1 = __importDefault(require("../../storage/StorageUtilities")); const MinecraftUtilities_1 = __importDefault(require("../../minecraft/MinecraftUtilities")); const ClUtils_1 = __importStar(require("../ClUtils")); const ICommandContext_1 = require("./ICommandContext"); const WorkerPool_1 = require("./WorkerPool"); const Logger_1 = require("./Logger"); const Log_1 = __importDefault(require("../../core/Log")); /** * Factory for creating ICommandContext instances. */ class CommandContextFactory { /** Folder names that indicate a Minecraft project root. */ static PROJECT_INDICATOR_FOLDERS = [ "behavior_packs", "behavior_pack", "development_behavior_packs", "resource_packs", "resource_pack", "development_resource_packs", ]; /** Maximum number of parent levels to walk when auto-discovering a project root. */ static MAX_DISCOVERY_LEVELS = 8; /** * Walk up from `startDir` to find the nearest Minecraft project root. * * A folder qualifies as a project root if it contains: * - A `package.json` file, OR * - A child folder matching one of the Minecraft pack folder conventions * (behavior_packs, resource_packs, etc.) * * The search stops when: * - A qualifying folder is found (returned immediately) * - MAX_DISCOVERY_LEVELS parent directories have been checked * - The folder is at a "second-order root" — its parent is a filesystem * root (e.g. `C:\projects` on Windows, `/home` on Unix), to avoid * scanning broad top-level directories * - The filesystem root is reached * * If no qualifying folder is found, `startDir` is returned unchanged * (preserving the current cwd-fallback behavior). */ static resolveProjectRoot(startDir, log) { let current = path_1.default.resolve(startDir); for (let level = 0; level <= CommandContextFactory.MAX_DISCOVERY_LEVELS; level++) { // Second-order root boundary: stop if the parent of `current` is a // filesystem root. This prevents considering folders like C:\projects\ // or /home/ which are too broad to be a project root. const parent = path_1.default.dirname(current); if (level > 0 && parent === path_1.default.dirname(parent)) { // `parent` is a filesystem root (e.g. C:\ or /), so `current` is a // top-level directory — too high to be a project. break; } if (CommandContextFactory.isProjectRoot(current)) { if (level > 0) { log?.verbose(`Auto-discovered project root: ${current}`); } return current; } // Move to parent directory if (parent === current) { // Reached filesystem root break; } current = parent; } return startDir; } /** * Check whether a directory looks like a Minecraft project root. * * Returns true if the directory contains a `package.json` file or any * of the standard Minecraft pack folder names. */ static isProjectRoot(dir) { try { if (fs_1.default.existsSync(path_1.default.join(dir, "package.json"))) { return true; } for (const folderName of CommandContextFactory.PROJECT_INDICATOR_FOLDERS) { const candidate = path_1.default.join(dir, folderName); try { if (fs_1.default.statSync(candidate).isDirectory()) { return true; } } catch { // Folder doesn't exist — continue } } } catch (err) { // Permission error or similar — can't read this directory Log_1.default.debug(`isProjectRoot: could not inspect '${dir}': ${err}`); } return false; } /** * Create a fully-hydrated command context. * * @param creatorTools Initialized CreatorTools instance * @param localEnv Initialized LocalEnvironment * @param taskType The command being executed * @param options Raw options from Commander.js * @param args Command-specific arguments */ static async create(creatorTools, localEnv, taskType, options, args = {}) { // Parse numeric and boolean options const threads = CommandContextFactory.parseThreads(options.threads); const force = options.force ?? false; const isolated = options.isolated ?? false; const debug = options.debug ?? false; const verbose = options.verbose ?? false; const quiet = options.quiet ?? false; const json = options.json ?? false; // Implicit --yes: when --json is set, the caller is non-interactive (CI / MCP). // Treat it as if --yes was also supplied so commands skip prompts and use defaults. const yes = options.yes ?? json; const dryRun = options.dryRun ?? false; // Parse output type - if --json flag is set, use json output type const outputType = json ? ClUtils_1.OutputType.json : CommandContextFactory.parseOutputType(options.outputType); // Create logger (quiet mode suppresses non-essential output, json mode routes non-data to stderr) const log = (0, Logger_1.createLogger)(verbose, quiet, debug, false, json); // Resolve input/output folders to absolute paths // When -i is not specified, auto-discover the nearest project root by // walking up from cwd (checks for package.json or *_packs folders). let inputFolderAutoDiscovered = false; let rawInputFolder; if (options.inputFolder) { rawInputFolder = options.inputFolder; } else { const cwd = process.cwd(); const discovered = CommandContextFactory.resolveProjectRoot(cwd, log); rawInputFolder = discovered; inputFolderAutoDiscovered = discovered !== cwd; } const inputFolder = path_1.default.isAbsolute(rawInputFolder) ? rawInputFolder : path_1.default.resolve(process.cwd(), rawInputFolder); const rawOutputFolder = options.outputFolder || rawInputFolder; const outputFolder = path_1.default.isAbsolute(rawOutputFolder) ? rawOutputFolder : path_1.default.resolve(process.cwd(), rawOutputFolder); // Create storage instances const inputStorage = new NodeStorage_1.default(inputFolder, ""); inputStorage.readOnly = !ClUtils_1.default.getIsEditInPlaceCommand(taskType); const outputStorage = new NodeStorage_1.default(outputFolder, ""); // Get work folders const inputWorkFolder = await CommandContextFactory.getWorkFolder(inputStorage.rootFolder, taskType, options.inputFolder, options.outputFolder, false // isOutputFolder ); // For commands with a separate output folder, ensure the output folder exists // This applies to validate, write commands, and any command with explicit -o flag const shouldCreateOutputFolder = outputFolder !== inputFolder && options.outputFolder !== undefined; const outputWorkFolder = outputFolder === inputFolder ? inputWorkFolder : await CommandContextFactory.getWorkFolder(outputStorage.rootFolder, taskType, options.inputFolder, options.outputFolder, shouldCreateOutputFolder // isOutputFolder - will create if needed ); // Detect and load projects // For isEditInPlace commands (like 'add') where only -o is specified (no -i), // we should detect/create projects in the output folder, not the current directory const isEditInPlace = ClUtils_1.default.getIsEditInPlaceCommand(taskType); const onlyOutputSpecified = !options.inputFolder && options.outputFolder !== undefined; const projectDetectionFolder = isEditInPlace && onlyOutputSpecified ? outputWorkFolder : inputWorkFolder; const projectStarts = await CommandContextFactory.detectProjects(creatorTools, taskType, options, projectDetectionFolder, log); if (projectStarts.length === 0) { Log_1.default.debug("No Minecraft projects detected in " + projectDetectionFolder.fullPath); } // Hydrate projects const projects = CommandContextFactory.hydrateProjects(creatorTools, projectStarts); // Create worker pool // Note: For now we create a placeholder pool. Commands will use the pool // which internally creates workers with the TaskWorker.js script. const workerPool = (0, WorkerPool_1.createWorkerPool)(threads); // Parse server options const server = CommandContextFactory.parseServerOptions(options); // Parse world options const world = CommandContextFactory.parseWorldOptions(options); // Parse validation options const validation = CommandContextFactory.parseValidationOptions(options); // Build the context let exitCode = 0; const context = { // Core creatorTools, localEnv, workerPool, log, // Projects projects, projectCount: projects.length, isSingleProject: projects.length === 1, // Input/Output inputFolder, inputFolderSpecified: options.inputFolder !== undefined, inputFolderAutoDiscovered, outputFolder, outputFile: options.outputFile, inputStorage, outputStorage, inputWorkFolder, outputWorkFolder, // Global options threads, force, isolated, debug, verbose, quiet, json, yes, dryRun, outputType, taskType, // Command args subCommand: args.subCommand, propertyValue: args.propertyValue, searchTerm: args.searchTerm, mode: args.mode, type: args.type, newName: args.newName, description: args.description, projectStartsWith: options.projectStartsWith, referenceFolder: options.referenceFolder, commandOptions: options.commandOptions || {}, // Grouped options server, world, validation, // Exit state exitCode, setExitCode(code) { if (code > exitCode) { exitCode = code; context.exitCode = code; } }, // Utility method async forEachProject(fn, label) { for (let i = 0; i < projects.length; i++) { const project = projects[i]; try { if (projects.length > 1) { log.info(`${label || "Processing"} project ${i + 1}/${projects.length}: ${project.name}`); } await fn(project, i); } catch (error) { const message = error instanceof Error ? error.message : String(error); log.error(`Error with project ${project.name}: ${message}`); context.setExitCode(ICommandContext_1.ErrorCodes.INIT_ERROR); } } }, }; return context; } /** * Parse threads option. */ static parseThreads(threadsStr) { if (!threadsStr) { return 1; } const parsed = parseInt(threadsStr, 10); if (isNaN(parsed) || parsed < 1) { return 1; } return Math.min(parsed, 8); // Cap at 8 threads } /** * Parse output type option. */ static parseOutputType(outputTypeStr) { if (outputTypeStr === "noreports") { return ClUtils_1.OutputType.noReports; } return ClUtils_1.OutputType.normal; } /** * Get work folder, creating if needed for write commands. */ static async getWorkFolder(folder, taskType, inputFolder, outputFolder, shouldCreateIfMissing = false) { // Create folder if: // 1. shouldCreateIfMissing flag is set (for output folders of write commands) // 2. OR legacy behavior: no inputFolder but outputFolder exists and it's a write command if (shouldCreateIfMissing || (!inputFolder && outputFolder && ClUtils_1.default.getIsWriteCommand(taskType))) { await folder.ensureExists(); } const exists = await folder.exists(); if (!exists) { throw new Error(`Folder does not exist: ${folder.fullPath}`); } await folder.load(); return folder; } /** * Detect projects in the input folder/file. * * This is the core project detection logic migrated from loadProjects() in index.ts. */ static async detectProjects(creatorTools, taskType, options, workFolder, log) { const projectStarts = []; const psw = options.projectStartsWith?.toLowerCase(); const additionalFiles = CommandContextFactory.parseAdditionalFiles(options.additionalFiles); // ------------------------------------------------------------------------- // Single file mode // ------------------------------------------------------------------------- if (options.inputFile) { if (options.inputFolder) { throw new Error("Cannot specify both an input file and an input folder."); } const inputFolderPath = StorageUtilities_1.default.getFolderPath(options.inputFile); const inputFileName = StorageUtilities_1.default.getLeafName(options.inputFile); if (!inputFileName || inputFileName.length < 2 || !inputFolderPath || inputFolderPath.length < 2) { throw new Error(`Could not process file with path: '${options.inputFile}'`); } if (!creatorTools.ensureLocalFolder) { throw new Error("CreatorTools.ensureLocalFolder is not configured"); } const containingFolder = creatorTools.ensureLocalFolder(inputFolderPath); containingFolder.storage.readOnly = true; const file = containingFolder.ensureFile(inputFileName); const fileExists = await file.exists(); if (!fileExists) { throw new Error(`Could not find file with path: '${options.inputFile}'`); } projectStarts.push({ ctorProjectName: inputFileName, localFilePath: options.inputFile, accessoryFiles: additionalFiles, }); return projectStarts; } // ------------------------------------------------------------------------- // Folder-based detection // ------------------------------------------------------------------------- const name = StorageUtilities_1.default.getLeafName(workFolder.fullPath); // Check for --single flag if (options.single) { projectStarts.push({ ctorProjectName: name, localFolderPath: workFolder.fullPath, accessoryFiles: additionalFiles, }); return projectStarts; } // Try multi-level multi-project detection const multiLevelProjects = await CommandContextFactory.detectMultiLevelProjects(workFolder, additionalFiles, psw, log); if (multiLevelProjects.length > 0) { return multiLevelProjects; } // Try children-of-folder multi-project detection const childrenProjects = await CommandContextFactory.detectChildrenProjects(workFolder, additionalFiles, psw, log); if (childrenProjects.length > 0) { return childrenProjects; } // Fallback: treat as single project projectStarts.push({ ctorProjectName: name, localFolderPath: workFolder.fullPath, accessoryFiles: additionalFiles, }); return projectStarts; } /** * Detect multi-level multi-project layout. * * Structure: * root/ * subfolder1/ * project1.zip * project1.data.json * subfolder2/ * project2.mcaddon */ static async detectMultiLevelProjects(workFolder, additionalFiles, psw, log) { const projectStarts = []; // Root folder must have no non-storage files if (workFolder.fileCount > 0) { for (const subFileName in workFolder.files) { const file = workFolder.files[subFileName]; if (file && !StorageUtilities_1.default.isFileStorageItem(file) && !file.fullPath.endsWith(".mci.json.zip")) { return []; // Not multi-level } } } let storageItemCount = 0; // Check subfolders for storage items for (const subFolderName in workFolder.folders) { const subFolder = workFolder.folders[subFolderName]; if (!subFolder) continue; await subFolder.load(); for (const subFileName in subFolder.files) { const subFile = subFolder.files[subFileName]; if (!subFile) continue; if (StorageUtilities_1.default.isFileStorageItem(subFile) && !subFile.fullPath.endsWith(".mci.json.zip")) { storageItemCount++; } const typeFromName = StorageUtilities_1.default.getTypeFromName(subFileName); if (!StorageUtilities_1.default.isFileStorageItem(subFile) && !subFile.fullPath.endsWith(".mci.json.zip") && typeFromName !== "json" && typeFromName !== "csv" && typeFromName !== "" && typeFromName !== "html") { return []; // Not multi-level } } } if (storageItemCount < 2) { return []; } log?.verbose(`Working across subfolders with projects at '${workFolder.fullPath}'`); // Collect projects from subfolders for (const subFolderName in workFolder.folders) { const subFolder = workFolder.folders[subFolderName]; if (!subFolder || subFolder.errorStatus) continue; await subFolder.load(); for (const fileName in subFolder.files) { const file = subFolder.files[fileName]; if (!file || !StorageUtilities_1.default.isFileStorageItem(file) || file.fullPath.endsWith(".mci.json.zip")) { continue; } const ps = { ctorProjectName: file.name, accessoryFiles: [...additionalFiles], }; // Look for associated .data.json files let baseName = StorageUtilities_1.default.getBaseFromName(file.name); if (subFolder.files[baseName + ".data.json"]) { ps.accessoryFiles?.push(baseName + ".data.json"); } const lastDash = baseName.lastIndexOf("-"); if (lastDash > 0) { baseName = baseName.substring(0, lastDash); if (subFolder.files[baseName + ".data.json"]) { ps.accessoryFiles?.push(baseName + ".data.json"); } } ps.localFilePath = file.fullPath; if (!psw || baseName.toLowerCase().startsWith(psw)) { projectStarts.push(ps); } } } return projectStarts; } /** * Detect children-of-folder multi-project layout. * * Structure: * root/ * project1.mcaddon * project2.zip * project3/ * manifest.json * ... */ static async detectChildrenProjects(workFolder, additionalFiles, psw, log) { const projectStarts = []; let isChildrenOfFolderMultiProject = true; let foundASubProject = false; // Check files in root for (const fileName in workFolder.files) { const file = workFolder.files[fileName]; if (!file) continue; if (!StorageUtilities_1.default.isFileStorageItem(file)) { isChildrenOfFolderMultiProject = false; continue; } else { foundASubProject = true; } } // Check folders for pack-like names for (const folderName in workFolder.folders) { if (MinecraftUtilities_1.default.pathLooksLikePackName(folderName) || MinecraftUtilities_1.default.pathLooksLikePackContainerName(folderName)) { isChildrenOfFolderMultiProject = false; continue; } } if (!isChildrenOfFolderMultiProject) { return []; } // Check subfolders for project markers for (const folderName in workFolder.folders) { const folder = workFolder.folders[folderName]; if (!folder || folder.errorStatus) continue; await folder.load(true); if (folder.files["manifest.json"] || folder.files["pack_manifest.json"] || folder.folders["content"] || folder.folders["Content"] || folder.folders["world_template"] || folder.folders["behavior_packs"]) { foundASubProject = true; } if (StorageUtilities_1.default.isMinecraftInternalFolder(folder)) { isChildrenOfFolderMultiProject = false; continue; } } // Must have found a sub-project AND not have internal folder markers if (!foundASubProject || !isChildrenOfFolderMultiProject) { return []; } log?.verbose(`Working across subfolders/packages at '${workFolder.fullPath}'`); // Collect file-based projects for (const fileName in workFolder.files) { const file = workFolder.files[fileName]; if (!file || !StorageUtilities_1.default.isFileStorageItem(file) || file.fullPath.endsWith(".mci.json.zip")) { continue; } if (!psw || file.name.toLowerCase().startsWith(psw)) { projectStarts.push({ ctorProjectName: file.name, localFilePath: file.fullPath, accessoryFiles: [...additionalFiles], }); } } // Collect folder-based projects for (const folderName in workFolder.folders) { const folder = workFolder.folders[folderName]; if (!folder || folder.errorStatus || folder.name === "out") continue; await folder.load(); if (folder.folderCount > 0) { if (!psw || folder.name.toLowerCase().startsWith(psw)) { projectStarts.push({ ctorProjectName: folder.name, localFolderPath: folder.fullPath, accessoryFiles: [...additionalFiles], }); } } } return projectStarts; } /** * Hydrate project start infos into full Project instances. */ static hydrateProjects(creatorTools, projectStarts) { return projectStarts.map((ps) => { const project = new Project_1.default(creatorTools, ps.ctorProjectName, null); if (ps.localFilePath) { project.localFilePath = ps.localFilePath; } if (ps.localFolderPath) { project.localFolderPath = ps.localFolderPath; } if (ps.accessoryFiles) { project.accessoryFilePaths = ps.accessoryFiles; } project.autoDeploymentMode = Project_1.ProjectAutoDeploymentMode.noAutoDeployment; return project; }); } /** * Parse additional files option. */ static parseAdditionalFiles(additionalFilesStr) { if (!additionalFilesStr) { return []; } return additionalFilesStr.split(",").map((p) => p.trim()); } /** * Parse a port string into a valid port number, falling back to a default. */ static parsePort(portStr, defaultPort) { const portNum = parseInt(portStr || String(defaultPort), 10); if (isNaN(portNum) || portNum < 1 || portNum > 65535) { Log_1.default.debug("Invalid port number: " + portStr + ", using default " + defaultPort); return defaultPort; } return portNum; } /** * Parse server-related options. */ static parseServerOptions(options) { return { port: CommandContextFactory.parsePort(options.port, 6126), httpsPort: options.httpsPort ? CommandContextFactory.parsePort(options.httpsPort, 443) : undefined, slot: options.slot ? Math.max(0, parseInt(options.slot, 10) || 0) : undefined, displayReadOnlyPasscode: options.displaypc, fullReadOnlyPasscode: options.fullpc, updateStatePasscode: options.updatepc, adminPasscode: options.adminpc, title: options.title, domainName: options.domain ? options.domain.trim().substring(0, 253) : undefined, messageOfTheDay: options.motd ? options.motd.substring(0, 256) : undefined, runOnce: options.runOnce ?? false, timeout: options.timeout ? parseInt(options.timeout, 10) : undefined, forceInk: options.forceInk ?? false, mcpRequireAuth: options.mcpRequireAuth ?? false, logFile: options.logFile, features: options.features, ssl: options.experimentalSslCert || options.experimentalSslPfx ? { certPath: options.experimentalSslCert, keyPath: options.experimentalSslKey, pfxPath: options.experimentalSslPfx, pfxPassphrase: options.experimentalSslPfxPassphrase, caPath: options.experimentalSslCa, port: CommandContextFactory.parsePort(options.experimentalSslPort, 443), httpsOnly: options.experimentalSslOnly ?? false, } : undefined, }; } /** * Parse world-related options. */ static parseWorldOptions(options) { return { betaApis: options.betaApis ?? false, editor: options.editor ?? false, difficulty: options.difficulty, gameMode: options.gameMode, name: options.worldName, seed: options.seed, ensureWorld: options.ensureWorld ?? false, testWorld: options.testWorld ?? false, launch: options.launch ?? false, }; } /** * Parse validation-specific options. */ static parseValidationOptions(options) { // Pass suite as string - TaskWorker will convert to ProjectInfoSuite const validSuites = ["all", "default", "addon", "currentplatform", "main"]; const rawSuite = options.suite || "main"; if (rawSuite && !validSuites.includes(rawSuite)) { Log_1.default.message("Unknown validation suite '" + rawSuite + "'. Valid suites: " + validSuites.join(", ") + ". Using 'main'."); } // Handle shell comma-splitting: PowerShell treats "A,B" as two separate args, // so "PATHLENGTH,PACKSIZE" becomes exclusions="PATHLENGTH", aggregateReports="PACKSIZE". // Detect non-aggregate values in aggregateReports and merge them back into exclusions. const validAggregateValues = ["aggregatenoindex", "aggregate", "true", "false", "1", "0"]; let exclusions = options.exclusions; let rawAggregate = options.aggregateReports; if (rawAggregate && !validAggregateValues.includes(rawAggregate)) { exclusions = exclusions ? exclusions + "," + rawAggregate : rawAggregate; rawAggregate = undefined; } const isAggregate = rawAggregate === "aggregate" || rawAggregate === "aggregatenoindex" || rawAggregate === "true" || rawAggregate === "1"; return { suite: validSuites.includes(rawSuite) ? rawSuite : "main", exclusionList: exclusions, outputMci: false, aggregateReports: isAggregate, warnOnly: options.warnOnly ?? false, }; } } exports.CommandContextFactory = CommandContextFactory;