UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

559 lines (558 loc) 21.9 kB
"use strict"; 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Config = void 0; const yaml = __importStar(require("js-yaml")); const _ = __importStar(require("lodash")); const path_1 = require("path"); /** * Supported config file names in priority order (highest priority first) */ const CONFIG_FILES = ['lt.config.json', 'lt.config.yaml', 'lt.config']; /** * Config helper functions for loading and merging lt.config files * Supports JSON and YAML formats with hierarchical merging */ class Config { constructor(filesystem, options) { var _a; this.filesystem = filesystem; this.suppressWarnings = (_a = options === null || options === void 0 ? void 0 : options.suppressWarnings) !== null && _a !== void 0 ? _a : false; } /** * Load configuration from lt.config files (JSON or YAML) * Searches from current directory up to root, merging configurations * * Supported file formats (in priority order): * 1. lt.config.json - explicit JSON format * 2. lt.config.yaml - explicit YAML format * 3. lt.config - auto-detected format (tries JSON first, then YAML) * * Priority (lowest to highest): * 1. Default values * 2. Config from parent directories (higher up = lower priority) * 3. Config from current directory * 4. CLI parameters * 5. Interactive user input * * @param startPath - Starting directory (defaults to current working directory) * @returns Merged configuration object */ loadConfig(startPath) { const start = startPath || this.filesystem.cwd(); const configs = []; // Search from current directory up to root let currentPath = start; const root = this.filesystem.separator === '/' ? '/' : /^[A-Z]:\\$/i; while (true) { const config = this.loadConfigFromDirectory(currentPath); if (config) { // Add to beginning (parent configs have lower priority) configs.unshift(config); } // Check if we've reached the root const parent = this.filesystem.path(currentPath, '..'); if (parent === currentPath || (typeof root !== 'string' && root.test(currentPath))) { break; } currentPath = parent; } // Merge all configs (later configs override earlier ones) return this.mergeConfigs(...configs); } /** * Load config from a single directory * Checks for config files in priority order * Warns if multiple config file variants exist * * @param dirPath - Directory to search in * @returns Config object or null if no valid config found */ loadConfigFromDirectory(dirPath) { // Find all existing config files in this directory const existingFiles = []; for (const configFile of CONFIG_FILES) { const configPath = (0, path_1.join)(dirPath, configFile); if (this.filesystem.exists(configPath)) { existingFiles.push(configFile); } } // No config files found if (existingFiles.length === 0) { return null; } // Warn if multiple config files exist if (existingFiles.length > 1 && !this.suppressWarnings) { const used = existingFiles[0]; const ignored = existingFiles.slice(1); console.warn(`Warning: Multiple config files found in ${dirPath}:\n` + ` Using: ${used}\n` + ` Ignored: ${ignored.join(', ')}\n` + ` Priority: lt.config.json > lt.config.yaml > lt.config`); } // Return the highest priority config (first in the list) for (const configFile of existingFiles) { const configPath = (0, path_1.join)(dirPath, configFile); const config = this.parseConfigFile(configPath, configFile); if (config) { return config; } } return null; } /** * Parse a config file based on its name/format * * @param configPath - Full path to config file * @param fileName - Name of the config file * @returns Parsed config object or null on error */ parseConfigFile(configPath, fileName) { try { const content = this.filesystem.read(configPath); if (!content || content.trim() === '') { if (!this.suppressWarnings) { console.warn(`Warning: Config file is empty: ${configPath}`); } return null; } // Explicit JSON file if (fileName === 'lt.config.json') { try { return JSON.parse(content); } catch (e) { if (!this.suppressWarnings) { this.logParseError(configPath, 'JSON', e); } return null; } } // Explicit YAML file if (fileName === 'lt.config.yaml') { try { return yaml.load(content); } catch (e) { if (!this.suppressWarnings) { this.logParseError(configPath, 'YAML', e); } return null; } } // Auto-detect format for lt.config return this.parseAutoDetect(content, configPath); } catch (error) { if (!this.suppressWarnings) { console.warn(`Warning: Could not read config file ${configPath}`); if (error instanceof Error) { console.warn(` Error: ${error.message}`); } } return null; } } /** * Log a detailed parse error message */ logParseError(configPath, format, error) { console.warn(`Warning: Could not parse ${format} config file: ${configPath}`); if (error instanceof SyntaxError) { // JSON parse error - extract position info const match = error.message.match(/position\s+(\d+)/i); if (match) { const position = parseInt(match[1], 10); console.warn(` Syntax error at position ${position}: ${error.message}`); } else { console.warn(` Syntax error: ${error.message}`); } } else if (error instanceof yaml.YAMLException) { // YAML parse error - has line/column info console.warn(` ${error.message}`); if (error.mark) { console.warn(` at line ${error.mark.line + 1}, column ${error.mark.column + 1}`); } } else if (error instanceof Error) { console.warn(` Error: ${error.message}`); } } /** * Auto-detect and parse config content (JSON or YAML) * Tries JSON first, then YAML as fallback * * @param content - Raw config file content * @param configPath - Path to the config file (for error reporting) * @returns Parsed config object or null on error */ parseAutoDetect(content, configPath) { let jsonError = null; let yamlError = null; // Try JSON first try { return JSON.parse(content); } catch (e) { jsonError = e instanceof Error ? e : new Error(String(e)); } // Try YAML try { return yaml.load(content); } catch (e) { yamlError = e instanceof Error ? e : new Error(String(e)); } // Neither JSON nor YAML - report both errors if (!this.suppressWarnings && configPath) { console.warn(`Warning: Could not parse config file: ${configPath}`); console.warn(` File format could not be auto-detected.`); console.warn(` JSON error: ${jsonError === null || jsonError === void 0 ? void 0 : jsonError.message}`); console.warn(` YAML error: ${yamlError === null || yamlError === void 0 ? void 0 : yamlError.message}`); } return null; } /** * Merge multiple config objects * Later configs override earlier ones * * Merge behavior: * - Objects are deeply merged * - Arrays are completely replaced (not merged) * - null values delete the corresponding key from parent configs * * @param configs - Config objects to merge * @returns Merged configuration */ mergeConfigs(...configs) { const merged = {}; // Filter out null/undefined configs const validConfigs = configs.filter((c) => c !== null && c !== undefined); if (validConfigs.length === 0) { return merged; } // Use lodash mergeWith with custom handling for arrays and null values const result = _.mergeWith(merged, ...validConfigs, (_objValue, srcValue) => { // If source value is null, delete the key from target if (srcValue === null) { return undefined; // This tells lodash to use undefined, which we'll clean up later } // If source value is an array, replace rather than merge if (Array.isArray(srcValue)) { return srcValue; } // Otherwise, use default merge behavior return undefined; }); // Clean up null values (remove keys that were set to null) return this.removeNullValues(result); } /** * Recursively remove keys with null values from an object * * @param obj - Object to clean * @returns Cleaned object without null values */ removeNullValues(obj) { if (obj === null || obj === undefined) { return undefined; } if (Array.isArray(obj)) { return obj.map((item) => this.removeNullValues(item)).filter((item) => item !== undefined); } if (typeof obj === 'object') { const result = {}; for (const key of Object.keys(obj)) { const value = this.removeNullValues(obj[key]); if (value !== undefined) { result[key] = value; } } return Object.keys(result).length > 0 ? result : undefined; } return obj; } /** * Get a configuration value with priority handling * * Priority (lowest to highest): * 1. defaultValue (code default) * 2. globalValue (from defaults section) * 3. configValue (from commands section) * 4. cliValue (CLI parameter) * 5. interactiveValue (user input) * * @param options - Configuration options * @returns The value according to priority */ getValue(options) { const { cliValue, configValue, defaultValue, globalValue, interactiveValue } = options; // Priority: interactive > cli > config > global > default if (interactiveValue !== undefined && interactiveValue !== null) { return interactiveValue; } if (cliValue !== undefined && cliValue !== null) { return cliValue; } if (configValue !== undefined && configValue !== null) { return configValue; } if (globalValue !== undefined && globalValue !== null) { return globalValue; } return defaultValue; } /** * Get a global default value from the defaults section * * @param config - Loaded config object * @param key - Key in the defaults section * @returns The global default value or undefined */ getGlobalDefault(config, key) { var _a; return (_a = config === null || config === void 0 ? void 0 : config.defaults) === null || _a === void 0 ? void 0 : _a[key]; } /** * Check if a value should be considered as "set" (not undefined/null) * Useful for determining if a config value should skip interactive prompts * * @param value - Value to check * @returns true if the value is set */ isSet(value) { return value !== undefined && value !== null; } /** * Get noConfirm setting with standard priority handling * Simplifies the common pattern used across many commands * * @param options - Configuration options * @returns The resolved noConfirm value */ getNoConfirm(options) { var _a, _b; const { cliValue, commandConfig, config, parentConfig } = options; const configNoConfirm = (_a = commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.noConfirm) !== null && _a !== void 0 ? _a : parentConfig === null || parentConfig === void 0 ? void 0 : parentConfig.noConfirm; const globalNoConfirm = this.getGlobalDefault(config, 'noConfirm'); return ((_b = this.getValue({ cliValue, configValue: configNoConfirm, defaultValue: false, globalValue: globalNoConfirm, })) !== null && _b !== void 0 ? _b : false); } /** * Get skipLint setting with standard priority handling * * @param options - Configuration options * @returns The resolved skipLint value */ getSkipLint(options) { var _a; const { cliValue, commandConfig, config } = options; const globalSkipLint = this.getGlobalDefault(config, 'skipLint'); return ((_a = this.getValue({ cliValue, configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.skipLint, defaultValue: false, globalValue: globalSkipLint, })) !== null && _a !== void 0 ? _a : false); } /** * Save configuration to a file in the specified directory * * @param config - Configuration to save * @param targetPath - Directory to save config in (defaults to current directory) * @param options - Save options * @param options.format - File format: 'json' (default) or 'yaml' */ saveConfig(config, targetPath, options) { const path = targetPath || this.filesystem.cwd(); const format = (options === null || options === void 0 ? void 0 : options.format) || 'json'; if (format === 'yaml') { const configPath = (0, path_1.join)(path, 'lt.config.yaml'); const yamlContent = yaml.dump(config, { indent: 2, lineWidth: -1, noRefs: true, }); this.filesystem.write(configPath, yamlContent); } else { const configPath = (0, path_1.join)(path, 'lt.config.json'); this.filesystem.write(configPath, config, { jsonIndent: 2 }); } } /** * Update an existing configuration file or create a new one * Merges with existing config if it exists * * @param config - Configuration updates to apply * @param targetPath - Directory containing config file (defaults to current directory) */ updateConfig(config, targetPath) { const path = targetPath || this.filesystem.cwd(); // Try to load existing config const existing = this.loadConfigFromDirectory(path) || {}; const merged = this.mergeConfigs(existing, config); this.saveConfig(merged, path); } /** * Get the effective configuration for a specific command * Combines loaded config with CLI parameters * * @param commandPath - Path to the command config (e.g., ['server', 'module']) * @param cliOptions - CLI parameters object * @returns Combined configuration for the command */ getCommandConfig(commandPath, cliOptions = {}) { const loadedConfig = this.loadConfig(); let configValue = loadedConfig.commands; // Navigate to the command config for (const key of commandPath) { configValue = configValue === null || configValue === void 0 ? void 0 : configValue[key]; } // Merge CLI options with config (CLI takes precedence) return Object.assign(Object.assign({}, configValue), cliOptions); } /** * Load configuration with origin tracking * Returns both the merged config and information about where each value came from * * @param startPath - Starting directory (defaults to current working directory) * @returns Object containing merged config and origins map */ loadConfigWithOrigins(startPath) { const start = startPath || this.filesystem.cwd(); const configsWithPaths = []; // Search from current directory up to root let currentPath = start; const root = this.filesystem.separator === '/' ? '/' : /^[A-Z]:\\$/i; while (true) { const result = this.loadConfigFromDirectoryWithPath(currentPath); if (result) { // Add to beginning (parent configs have lower priority) configsWithPaths.unshift(result); } // Check if we've reached the root const parent = this.filesystem.path(currentPath, '..'); if (parent === currentPath || (typeof root !== 'string' && root.test(currentPath))) { break; } currentPath = parent; } // Track origins for each key path const origins = new Map(); // Process configs in order (lower priority first) for (const { config, path } of configsWithPaths) { this.trackOrigins(config, path, '', origins); } // Merge all configs const configs = configsWithPaths.map((c) => c.config); const mergedConfig = this.mergeConfigs(...configs); return { config: mergedConfig, files: configsWithPaths, origins, }; } /** * Load config from a directory and return with its path */ loadConfigFromDirectoryWithPath(dirPath) { // Find all existing config files in this directory const existingFiles = []; for (const configFile of CONFIG_FILES) { const configPath = (0, path_1.join)(dirPath, configFile); if (this.filesystem.exists(configPath)) { existingFiles.push(configFile); } } if (existingFiles.length === 0) { return null; } // Warn if multiple config files exist if (existingFiles.length > 1 && !this.suppressWarnings) { const used = existingFiles[0]; const ignored = existingFiles.slice(1); console.warn(`Warning: Multiple config files found in ${dirPath}:\n` + ` Using: ${used}\n` + ` Ignored: ${ignored.join(', ')}\n` + ` Priority: lt.config.json > lt.config.yaml > lt.config`); } // Return the highest priority config with its path for (const configFile of existingFiles) { const configPath = (0, path_1.join)(dirPath, configFile); const config = this.parseConfigFile(configPath, configFile); if (config) { return { config, path: configPath }; } } return null; } /** * Recursively track the origin of each config value */ trackOrigins(obj, filePath, keyPath, origins) { if (obj === null || obj === undefined) { return; } if (typeof obj === 'object' && !Array.isArray(obj)) { for (const key of Object.keys(obj)) { const newPath = keyPath ? `${keyPath}.${key}` : key; const value = obj[key]; if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Recurse into nested objects this.trackOrigins(value, filePath, newPath, origins); } else { // Leaf value - record origin origins.set(newPath, filePath); } } } } } exports.Config = Config; /** * Extension function to add config helper to toolbox */ exports.default = (toolbox) => { const config = new Config(toolbox.filesystem); toolbox.config = config; };