@utilarium/cardigantime 0.0.24-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +65 -0
  2. package/README.md +398 -0
  3. package/dist/cardigantime.cjs +2169 -0
  4. package/dist/cardigantime.cjs.map +1 -0
  5. package/dist/cardigantime.d.ts +92 -0
  6. package/dist/cardigantime.js +198 -0
  7. package/dist/cardigantime.js.map +1 -0
  8. package/dist/config/executable-security.d.ts +32 -0
  9. package/dist/config/format-detector.d.ts +59 -0
  10. package/dist/configure.d.ts +55 -0
  11. package/dist/configure.js +125 -0
  12. package/dist/configure.js.map +1 -0
  13. package/dist/constants.d.ts +25 -0
  14. package/dist/constants.js +38 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/discovery/discoverer.d.ts +62 -0
  17. package/dist/discovery/hierarchical-modes.d.ts +64 -0
  18. package/dist/discovery/index.d.ts +15 -0
  19. package/dist/discovery/patterns.d.ts +77 -0
  20. package/dist/discovery/root-detection.d.ts +100 -0
  21. package/dist/discovery/traversal-security.d.ts +106 -0
  22. package/dist/env/errors.d.ts +18 -0
  23. package/dist/env/index.d.ts +7 -0
  24. package/dist/env/naming.d.ts +38 -0
  25. package/dist/env/parser.d.ts +61 -0
  26. package/dist/env/reader.d.ts +45 -0
  27. package/dist/env/resolver.d.ts +25 -0
  28. package/dist/env/schema-utils.d.ts +33 -0
  29. package/dist/env/types.d.ts +43 -0
  30. package/dist/error/ArgumentError.d.ts +31 -0
  31. package/dist/error/ArgumentError.js +48 -0
  32. package/dist/error/ArgumentError.js.map +1 -0
  33. package/dist/error/ConfigParseError.d.ts +26 -0
  34. package/dist/error/ConfigurationError.d.ts +21 -0
  35. package/dist/error/ConfigurationError.js +46 -0
  36. package/dist/error/ConfigurationError.js.map +1 -0
  37. package/dist/error/FileSystemError.d.ts +30 -0
  38. package/dist/error/FileSystemError.js +58 -0
  39. package/dist/error/FileSystemError.js.map +1 -0
  40. package/dist/error/index.d.ts +4 -0
  41. package/dist/mcp/discovery.d.ts +105 -0
  42. package/dist/mcp/errors.d.ts +75 -0
  43. package/dist/mcp/index.d.ts +22 -0
  44. package/dist/mcp/integration.d.ts +184 -0
  45. package/dist/mcp/parser.d.ts +141 -0
  46. package/dist/mcp/resolver.d.ts +165 -0
  47. package/dist/mcp/tools/check-config-types.d.ts +208 -0
  48. package/dist/mcp/tools/check-config.d.ts +85 -0
  49. package/dist/mcp/tools/index.d.ts +12 -0
  50. package/dist/mcp/types.d.ts +210 -0
  51. package/dist/parsers/index.d.ts +25 -0
  52. package/dist/parsers/javascript-parser.d.ts +12 -0
  53. package/dist/parsers/json-parser.d.ts +6 -0
  54. package/dist/parsers/typescript-parser.d.ts +15 -0
  55. package/dist/parsers/yaml-parser.d.ts +6 -0
  56. package/dist/read.d.ts +56 -0
  57. package/dist/read.js +653 -0
  58. package/dist/read.js.map +1 -0
  59. package/dist/security/audit-logger.d.ts +135 -0
  60. package/dist/security/cli-validator.d.ts +73 -0
  61. package/dist/security/config-validator.d.ts +95 -0
  62. package/dist/security/defaults.d.ts +17 -0
  63. package/dist/security/index.d.ts +14 -0
  64. package/dist/security/numeric-guard.d.ts +111 -0
  65. package/dist/security/path-guard.d.ts +53 -0
  66. package/dist/security/profiles.d.ts +127 -0
  67. package/dist/security/security-validator.d.ts +109 -0
  68. package/dist/security/string-guard.d.ts +92 -0
  69. package/dist/security/types.d.ts +126 -0
  70. package/dist/security/zod-secure-enum.d.ts +20 -0
  71. package/dist/security/zod-secure-number.d.ts +39 -0
  72. package/dist/security/zod-secure-path.d.ts +24 -0
  73. package/dist/security/zod-secure-string.d.ts +38 -0
  74. package/dist/types.d.ts +584 -0
  75. package/dist/types.js +56 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/util/hierarchical.d.ts +136 -0
  78. package/dist/util/hierarchical.js +436 -0
  79. package/dist/util/hierarchical.js.map +1 -0
  80. package/dist/util/schema-defaults.d.ts +80 -0
  81. package/dist/util/schema-defaults.js +118 -0
  82. package/dist/util/schema-defaults.js.map +1 -0
  83. package/dist/util/storage.d.ts +31 -0
  84. package/dist/util/storage.js +154 -0
  85. package/dist/util/storage.js.map +1 -0
  86. package/dist/validate.d.ts +113 -0
  87. package/dist/validate.js +260 -0
  88. package/dist/validate.js.map +1 -0
  89. package/package.json +84 -0
package/dist/read.js ADDED
@@ -0,0 +1,653 @@
1
+ import * as yaml from 'js-yaml';
2
+ import * as path from 'node:path';
3
+ import { create } from './util/storage.js';
4
+ import { loadHierarchicalConfig } from './util/hierarchical.js';
5
+
6
+ /**
7
+ * Removes undefined values from an object to create a clean configuration.
8
+ * This is used to merge configuration sources while avoiding undefined pollution.
9
+ *
10
+ * @param obj - The object to clean
11
+ * @returns A new object with undefined values filtered out
12
+ */ function clean(obj) {
13
+ return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
14
+ }
15
+ /**
16
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
17
+ *
18
+ * @param config - The configuration object to process
19
+ * @param configDir - The directory containing the configuration file
20
+ * @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
21
+ * @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
22
+ * @returns The configuration object with resolved paths
23
+ */ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
24
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
25
+ return config;
26
+ }
27
+ const resolvedConfig = {
28
+ ...config
29
+ };
30
+ for (const fieldPath of pathFields){
31
+ const value = getNestedValue(resolvedConfig, fieldPath);
32
+ if (value !== undefined) {
33
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
34
+ const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
35
+ setNestedValue(resolvedConfig, fieldPath, resolvedValue);
36
+ }
37
+ }
38
+ return resolvedConfig;
39
+ }
40
+ /**
41
+ * Gets a nested value from an object using dot notation.
42
+ */ function getNestedValue(obj, path) {
43
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
44
+ }
45
+ /**
46
+ * Checks if a key is unsafe for prototype pollution prevention.
47
+ */ function isUnsafeKey(key) {
48
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
49
+ }
50
+ /**
51
+ * Sets a nested value in an object using dot notation.
52
+ * Prevents prototype pollution by rejecting dangerous property names.
53
+ */ function setNestedValue(obj, path, value) {
54
+ const keys = path.split('.');
55
+ const lastKey = keys.pop();
56
+ // Prevent prototype pollution via special property names
57
+ if (isUnsafeKey(lastKey) || keys.some(isUnsafeKey)) {
58
+ return;
59
+ }
60
+ const target = keys.reduce((current, key)=>{
61
+ // Skip if this is an unsafe key (already checked above, but defensive)
62
+ if (isUnsafeKey(key)) {
63
+ return current;
64
+ }
65
+ if (!(key in current)) {
66
+ current[key] = {};
67
+ }
68
+ return current[key];
69
+ }, obj);
70
+ target[lastKey] = value;
71
+ }
72
+ /**
73
+ * Resolves a path value (string or array of strings) relative to the config directory.
74
+ */ function resolvePathValue(value, configDir, resolveArrayElements) {
75
+ if (typeof value === 'string') {
76
+ return resolveSinglePath(value, configDir);
77
+ }
78
+ if (Array.isArray(value) && resolveArrayElements) {
79
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
80
+ }
81
+ return value;
82
+ }
83
+ /**
84
+ * Resolves a single path string relative to the config directory if it's a relative path.
85
+ */ function resolveSinglePath(pathStr, configDir) {
86
+ if (!pathStr || path.isAbsolute(pathStr)) {
87
+ return pathStr;
88
+ }
89
+ return path.resolve(configDir, pathStr);
90
+ }
91
+ /**
92
+ * Validates and secures a user-provided path to prevent path traversal attacks.
93
+ *
94
+ * Security checks include:
95
+ * - Path traversal prevention (blocks '..')
96
+ * - Absolute path detection
97
+ * - Path separator validation
98
+ *
99
+ * @param userPath - The user-provided path component
100
+ * @param basePath - The base directory to join the path with
101
+ * @returns The safely joined and normalized path
102
+ * @throws {Error} When path traversal or absolute paths are detected
103
+ */ function validatePath(userPath, basePath) {
104
+ if (!userPath || !basePath) {
105
+ throw new Error('Invalid path parameters');
106
+ }
107
+ const normalized = path.normalize(userPath);
108
+ // Prevent path traversal attacks
109
+ if (normalized.includes('..') || path.isAbsolute(normalized)) {
110
+ throw new Error('Invalid path: path traversal detected');
111
+ }
112
+ // Ensure the path doesn't start with a path separator
113
+ if (normalized.startsWith('/') || normalized.startsWith('\\')) {
114
+ throw new Error('Invalid path: absolute path detected');
115
+ }
116
+ return path.join(basePath, normalized);
117
+ }
118
+ /**
119
+ * Validates a configuration directory path for security and basic formatting.
120
+ *
121
+ * Performs validation to prevent:
122
+ * - Null byte injection attacks
123
+ * - Extremely long paths that could cause DoS
124
+ * - Empty or invalid directory specifications
125
+ *
126
+ * @param configDir - The configuration directory path to validate
127
+ * @returns The normalized configuration directory path
128
+ * @throws {Error} When the directory path is invalid or potentially dangerous
129
+ */ function validateConfigDirectory(configDir) {
130
+ if (!configDir) {
131
+ throw new Error('Configuration directory is required');
132
+ }
133
+ // Check for null bytes which could be used for path injection
134
+ if (configDir.includes('\0')) {
135
+ throw new Error('Invalid path: null byte detected');
136
+ }
137
+ const normalized = path.normalize(configDir);
138
+ // Basic validation - could be expanded based on requirements
139
+ if (normalized.length > 1000) {
140
+ throw new Error('Configuration directory path too long');
141
+ }
142
+ return normalized;
143
+ }
144
+ /**
145
+ * Reads configuration from files and merges it with CLI arguments.
146
+ *
147
+ * This function implements the core configuration loading logic:
148
+ * 1. Validates and resolves the configuration directory path
149
+ * 2. Attempts to read the YAML configuration file
150
+ * 3. Safely parses the YAML content with security protections
151
+ * 4. Merges file configuration with runtime arguments
152
+ * 5. Returns a typed configuration object
153
+ *
154
+ * The function handles missing files gracefully and provides detailed
155
+ * logging for troubleshooting configuration issues.
156
+ *
157
+ * @template T - The Zod schema shape type for configuration validation
158
+ * @param args - Parsed command-line arguments containing potential config overrides
159
+ * @param options - Cardigantime options with defaults, schema, and logger
160
+ * @returns Promise resolving to the merged and typed configuration object
161
+ * @throws {Error} When configuration directory is invalid or required files cannot be read
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const config = await read(cliArgs, {
166
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
167
+ * configShape: MySchema.shape,
168
+ * logger: console,
169
+ * features: ['config']
170
+ * });
171
+ * // config is fully typed based on your schema
172
+ * ```
173
+ */ const read = async (args, options)=>{
174
+ var _options_defaults, _options_defaults_pathResolution;
175
+ const logger = options.logger;
176
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
177
+ if (!rawConfigDir) {
178
+ throw new Error('Configuration directory must be specified');
179
+ }
180
+ const resolvedConfigDir = validateConfigDirectory(rawConfigDir);
181
+ logger.verbose('Resolved config directory');
182
+ let rawFileConfig = {};
183
+ let discoveredConfigDirs = [];
184
+ let resolvedConfigDirs = [];
185
+ // Check if hierarchical configuration discovery is enabled
186
+ // Use optional chaining for safety although options.features is defaulted
187
+ if (options.features && options.features.includes('hierarchical')) {
188
+ logger.verbose('Hierarchical configuration discovery enabled');
189
+ try {
190
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
191
+ // Extract the config directory name from the path for hierarchical discovery
192
+ const configDirName = path.basename(resolvedConfigDir);
193
+ const startingDir = path.dirname(resolvedConfigDir);
194
+ logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
195
+ const hierarchicalResult = await loadHierarchicalConfig({
196
+ configDirName,
197
+ configFileName: options.defaults.configFile,
198
+ startingDir,
199
+ encoding: options.defaults.encoding,
200
+ logger,
201
+ pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
202
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
203
+ fieldOverlaps: options.defaults.fieldOverlaps
204
+ });
205
+ rawFileConfig = hierarchicalResult.config;
206
+ discoveredConfigDirs = hierarchicalResult.discoveredDirs.map((dir)=>dir.path);
207
+ resolvedConfigDirs = hierarchicalResult.resolvedConfigDirs.map((dir)=>dir.path);
208
+ if (hierarchicalResult.discoveredDirs.length > 0) {
209
+ logger.verbose(`Hierarchical discovery found ${hierarchicalResult.discoveredDirs.length} configuration directories`);
210
+ hierarchicalResult.discoveredDirs.forEach((dir)=>{
211
+ logger.debug(` Level ${dir.level}: ${dir.path}`);
212
+ });
213
+ } else {
214
+ logger.verbose('No configuration directories found in hierarchy');
215
+ }
216
+ if (hierarchicalResult.resolvedConfigDirs.length > 0) {
217
+ logger.verbose(`Found ${hierarchicalResult.resolvedConfigDirs.length} directories with actual configuration files`);
218
+ hierarchicalResult.resolvedConfigDirs.forEach((dir)=>{
219
+ logger.debug(` Config dir level ${dir.level}: ${dir.path}`);
220
+ });
221
+ }
222
+ if (hierarchicalResult.errors.length > 0) {
223
+ hierarchicalResult.errors.forEach((error)=>logger.warn(`Hierarchical config warning: ${error}`));
224
+ }
225
+ } catch (error) {
226
+ logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
227
+ // Fall back to single directory mode
228
+ logger.verbose('Falling back to single directory configuration loading');
229
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
230
+ // Include the directory in both arrays (discovered but check if it had config)
231
+ discoveredConfigDirs = [
232
+ resolvedConfigDir
233
+ ];
234
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
235
+ resolvedConfigDirs = [
236
+ resolvedConfigDir
237
+ ];
238
+ } else {
239
+ resolvedConfigDirs = [];
240
+ }
241
+ }
242
+ } else {
243
+ // Use traditional single directory configuration loading
244
+ logger.verbose('Using single directory configuration loading');
245
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
246
+ // Include the directory in discovered, and in resolved only if it had config
247
+ discoveredConfigDirs = [
248
+ resolvedConfigDir
249
+ ];
250
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
251
+ resolvedConfigDirs = [
252
+ resolvedConfigDir
253
+ ];
254
+ } else {
255
+ resolvedConfigDirs = [];
256
+ }
257
+ }
258
+ // Apply path resolution if configured
259
+ let processedConfig = rawFileConfig;
260
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
261
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
262
+ }
263
+ const config = clean({
264
+ ...processedConfig,
265
+ ...{
266
+ configDirectory: resolvedConfigDir,
267
+ discoveredConfigDirs,
268
+ resolvedConfigDirs
269
+ }
270
+ });
271
+ return config;
272
+ };
273
+ /**
274
+ * Tries to find a config file with alternative extensions (.yaml or .yml).
275
+ *
276
+ * @param storage Storage instance to use for file operations
277
+ * @param configDir The directory containing the config file
278
+ * @param configFileName The base config file name (may have .yaml or .yml extension)
279
+ * @param logger Logger for debugging
280
+ * @returns Promise resolving to the found config file path or null if not found
281
+ */ async function findConfigFileWithExtension(storage, configDir, configFileName, logger) {
282
+ // Validate the config file name to prevent path traversal
283
+ const configFilePath = validatePath(configFileName, configDir);
284
+ // First try the exact filename as specified
285
+ const exists = await storage.exists(configFilePath);
286
+ if (exists) {
287
+ const isReadable = await storage.isFileReadable(configFilePath);
288
+ if (isReadable) {
289
+ return configFilePath;
290
+ }
291
+ }
292
+ // If the exact filename doesn't exist or isn't readable, try alternative extensions
293
+ // Only do this if the filename has a .yaml or .yml extension
294
+ const ext = path.extname(configFileName);
295
+ if (ext === '.yaml' || ext === '.yml') {
296
+ const baseName = path.basename(configFileName, ext);
297
+ const alternativeExt = ext === '.yaml' ? '.yml' : '.yaml';
298
+ const alternativeFileName = baseName + alternativeExt;
299
+ const alternativePath = validatePath(alternativeFileName, configDir);
300
+ logger.debug(`Config file not found at ${configFilePath}, trying alternative: ${alternativePath}`);
301
+ const altExists = await storage.exists(alternativePath);
302
+ if (altExists) {
303
+ const altIsReadable = await storage.isFileReadable(alternativePath);
304
+ if (altIsReadable) {
305
+ logger.debug(`Found config file with alternative extension: ${alternativePath}`);
306
+ return alternativePath;
307
+ }
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+ /**
313
+ * Loads configuration from a single directory (traditional mode).
314
+ *
315
+ * @param resolvedConfigDir - The resolved configuration directory path
316
+ * @param options - Cardigantime options
317
+ * @param logger - Logger instance
318
+ * @returns Promise resolving to the configuration object
319
+ */ async function loadSingleDirectoryConfig(resolvedConfigDir, options, logger) {
320
+ const storage = create({
321
+ log: logger.debug
322
+ });
323
+ logger.verbose('Attempting to load config file for cardigantime');
324
+ let rawFileConfig = {};
325
+ try {
326
+ // Try to find the config file with alternative extensions
327
+ const configFilePath = await findConfigFileWithExtension(storage, resolvedConfigDir, options.defaults.configFile, logger);
328
+ if (!configFilePath) {
329
+ logger.verbose('Configuration file not found. Using empty configuration.');
330
+ return rawFileConfig;
331
+ }
332
+ const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
333
+ // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities
334
+ const parsedYaml = yaml.load(yamlContent);
335
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
336
+ rawFileConfig = parsedYaml;
337
+ logger.verbose('Loaded configuration file successfully');
338
+ } else if (parsedYaml !== null) {
339
+ logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
340
+ }
341
+ } catch (error) {
342
+ // Re-throw security-related errors (path validation failures)
343
+ if (error.message && /Invalid path|path traversal|absolute path/i.test(error.message)) {
344
+ throw error;
345
+ }
346
+ if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
347
+ logger.verbose('Configuration file not found. Using empty configuration.');
348
+ } else {
349
+ // SECURITY FIX: Don't expose internal paths or detailed error information
350
+ logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
351
+ }
352
+ }
353
+ return rawFileConfig;
354
+ }
355
+ /**
356
+ * Recursively tracks the source of configuration values from hierarchical loading.
357
+ *
358
+ * @param config - The configuration object to track
359
+ * @param sourcePath - Path to the configuration file
360
+ * @param level - Hierarchical level
361
+ * @param prefix - Current object path prefix for nested values
362
+ * @param tracker - The tracker object to populate
363
+ */ function trackConfigSources(config, sourcePath, level, prefix = '', tracker = {}) {
364
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
365
+ // For primitives and arrays, track the entire value
366
+ tracker[prefix] = {
367
+ value: config,
368
+ sourcePath,
369
+ level,
370
+ sourceLabel: `Level ${level}: ${path.basename(path.dirname(sourcePath))}`
371
+ };
372
+ return tracker;
373
+ }
374
+ // For objects, recursively track each property
375
+ for (const [key, value] of Object.entries(config)){
376
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
377
+ trackConfigSources(value, sourcePath, level, fieldPath, tracker);
378
+ }
379
+ return tracker;
380
+ }
381
+ /**
382
+ * Merges multiple configuration source trackers with proper precedence.
383
+ * Lower level numbers have higher precedence.
384
+ *
385
+ * @param trackers - Array of trackers from different config sources
386
+ * @returns Merged tracker with proper precedence
387
+ */ function mergeConfigTrackers(trackers) {
388
+ const merged = {};
389
+ for (const tracker of trackers){
390
+ for (const [key, info] of Object.entries(tracker)){
391
+ // Only update if we don't have this key yet, or if this source has higher precedence (lower level)
392
+ if (!merged[key] || info.level < merged[key].level) {
393
+ merged[key] = info;
394
+ }
395
+ }
396
+ }
397
+ return merged;
398
+ }
399
+ /**
400
+ * Formats a configuration value for display, handling different types appropriately.
401
+ *
402
+ * @param value - The configuration value to format
403
+ * @returns Formatted string representation
404
+ */ function formatConfigValue(value) {
405
+ if (value === null) return 'null';
406
+ if (value === undefined) return 'undefined';
407
+ if (typeof value === 'string') return `"${value}"`;
408
+ if (typeof value === 'boolean') return value.toString();
409
+ if (typeof value === 'number') return value.toString();
410
+ if (Array.isArray(value)) {
411
+ if (value.length === 0) return '[]';
412
+ if (value.length <= 3) {
413
+ return `[${value.map(formatConfigValue).join(', ')}]`;
414
+ }
415
+ return `[${value.slice(0, 2).map(formatConfigValue).join(', ')}, ... (${value.length} items)]`;
416
+ }
417
+ if (typeof value === 'object') {
418
+ const keys = Object.keys(value);
419
+ if (keys.length === 0) return '{}';
420
+ if (keys.length <= 2) {
421
+ return `{${keys.slice(0, 2).join(', ')}}`;
422
+ }
423
+ return `{${keys.slice(0, 2).join(', ')}, ... (${keys.length} keys)}`;
424
+ }
425
+ return String(value);
426
+ }
427
+ /**
428
+ * Displays configuration with source tracking in a git blame-like format.
429
+ *
430
+ * @param config - The resolved configuration object
431
+ * @param tracker - Configuration source tracker
432
+ * @param discoveredDirs - Array of discovered configuration directories
433
+ * @param logger - Logger instance for output
434
+ */ function displayConfigWithSources(config, tracker, discoveredDirs, logger) {
435
+ logger.info('\n' + '='.repeat(80));
436
+ logger.info('CONFIGURATION SOURCE ANALYSIS');
437
+ logger.info('='.repeat(80));
438
+ // Display discovered configuration hierarchy
439
+ logger.info('\nDISCOVERED CONFIGURATION HIERARCHY:');
440
+ if (discoveredDirs.length === 0) {
441
+ logger.info(' No configuration directories found in hierarchy');
442
+ } else {
443
+ discoveredDirs.sort((a, b)=>a.level - b.level) // Sort by precedence (lower level = higher precedence)
444
+ .forEach((dir)=>{
445
+ const precedence = dir.level === 0 ? '(highest precedence)' : dir.level === Math.max(...discoveredDirs.map((d)=>d.level)) ? '(lowest precedence)' : '';
446
+ logger.info(` Level ${dir.level}: ${dir.path} ${precedence}`);
447
+ });
448
+ }
449
+ // Display resolved configuration with sources
450
+ logger.info('\nRESOLVED CONFIGURATION WITH SOURCES:');
451
+ logger.info('Format: [Source] key: value\n');
452
+ const sortedKeys = Object.keys(tracker).sort();
453
+ const maxKeyLength = Math.max(...sortedKeys.map((k)=>k.length), 20);
454
+ const maxSourceLength = Math.max(...Object.values(tracker).map((info)=>info.sourceLabel.length), 25);
455
+ for (const key of sortedKeys){
456
+ const info = tracker[key];
457
+ const paddedKey = key.padEnd(maxKeyLength);
458
+ const paddedSource = info.sourceLabel.padEnd(maxSourceLength);
459
+ const formattedValue = formatConfigValue(info.value);
460
+ logger.info(`[${paddedSource}] ${paddedKey}: ${formattedValue}`);
461
+ }
462
+ // Display summary
463
+ logger.info('\n' + '-'.repeat(80));
464
+ logger.info('SUMMARY:');
465
+ logger.info(` Total configuration keys: ${Object.keys(tracker).length}`);
466
+ logger.info(` Configuration sources: ${discoveredDirs.length}`);
467
+ // Count values by source
468
+ const sourceCount = {};
469
+ for (const info of Object.values(tracker)){
470
+ sourceCount[info.sourceLabel] = (sourceCount[info.sourceLabel] || 0) + 1;
471
+ }
472
+ logger.info(' Values by source:');
473
+ for (const [source, count] of Object.entries(sourceCount)){
474
+ logger.info(` ${source}: ${count} value(s)`);
475
+ }
476
+ logger.info('='.repeat(80));
477
+ }
478
+ /**
479
+ * Checks and displays the resolved configuration with detailed source tracking.
480
+ *
481
+ * This function provides a git blame-like view of configuration resolution,
482
+ * showing which file and hierarchical level contributed each configuration value.
483
+ *
484
+ * @template T - The Zod schema shape type for configuration validation
485
+ * @param args - Parsed command-line arguments
486
+ * @param options - Cardigantime options with defaults, schema, and logger
487
+ * @returns Promise that resolves when the configuration check is complete
488
+ *
489
+ * @example
490
+ * ```typescript
491
+ * await checkConfig(cliArgs, {
492
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
493
+ * configShape: MySchema.shape,
494
+ * logger: console,
495
+ * features: ['config', 'hierarchical']
496
+ * });
497
+ * // Outputs detailed configuration source analysis
498
+ * ```
499
+ */ const checkConfig = async (args, options)=>{
500
+ var _options_defaults, _options_defaults_pathResolution;
501
+ const logger = options.logger;
502
+ logger.info('Starting configuration check...');
503
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
504
+ if (!rawConfigDir) {
505
+ throw new Error('Configuration directory must be specified');
506
+ }
507
+ const resolvedConfigDir = validateConfigDirectory(rawConfigDir);
508
+ logger.verbose(`Resolved config directory: ${resolvedConfigDir}`);
509
+ let rawFileConfig = {};
510
+ let discoveredDirs = [];
511
+ let resolvedConfigDirs = [];
512
+ let tracker = {};
513
+ // Check if hierarchical configuration discovery is enabled
514
+ // Use optional chaining for safety although options.features is defaulted
515
+ if (options.features && options.features.includes('hierarchical')) {
516
+ logger.verbose('Using hierarchical configuration discovery for source tracking');
517
+ try {
518
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
519
+ // Extract the config directory name from the path for hierarchical discovery
520
+ const configDirName = path.basename(resolvedConfigDir);
521
+ const startingDir = path.dirname(resolvedConfigDir);
522
+ logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
523
+ const hierarchicalResult = await loadHierarchicalConfig({
524
+ configDirName,
525
+ configFileName: options.defaults.configFile,
526
+ startingDir,
527
+ encoding: options.defaults.encoding,
528
+ logger,
529
+ pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
530
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
531
+ fieldOverlaps: options.defaults.fieldOverlaps
532
+ });
533
+ rawFileConfig = hierarchicalResult.config;
534
+ discoveredDirs = hierarchicalResult.discoveredDirs;
535
+ resolvedConfigDirs = hierarchicalResult.resolvedConfigDirs;
536
+ // Build detailed source tracking by re-loading each config individually
537
+ const trackers = [];
538
+ // Sort by level (highest level first = lowest precedence first) to match merge order
539
+ const sortedDirs = [
540
+ ...resolvedConfigDirs
541
+ ].sort((a, b)=>b.level - a.level);
542
+ for (const dir of sortedDirs){
543
+ const storage = create({
544
+ log: logger.debug
545
+ });
546
+ const configFilePath = path.join(dir.path, options.defaults.configFile);
547
+ try {
548
+ const exists = await storage.exists(configFilePath);
549
+ if (!exists) continue;
550
+ const isReadable = await storage.isFileReadable(configFilePath);
551
+ if (!isReadable) continue;
552
+ const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
553
+ const parsedYaml = yaml.load(yamlContent);
554
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
555
+ const levelTracker = trackConfigSources(parsedYaml, configFilePath, dir.level);
556
+ trackers.push(levelTracker);
557
+ }
558
+ } catch (error) {
559
+ logger.debug(`Error loading config for source tracking from ${configFilePath}: ${error.message}`);
560
+ }
561
+ }
562
+ // Merge trackers with proper precedence
563
+ tracker = mergeConfigTrackers(trackers);
564
+ if (hierarchicalResult.errors.length > 0) {
565
+ logger.warn('Configuration loading warnings:');
566
+ hierarchicalResult.errors.forEach((error)=>logger.warn(` ${error}`));
567
+ }
568
+ } catch (error) {
569
+ logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
570
+ logger.verbose('Falling back to single directory configuration loading');
571
+ // Fall back to single directory mode for source tracking
572
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
573
+ const configFilePath = path.join(resolvedConfigDir, options.defaults.configFile);
574
+ tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
575
+ // Include the directory in discovered, and in resolved only if it had config
576
+ discoveredDirs = [
577
+ {
578
+ path: resolvedConfigDir,
579
+ level: 0
580
+ }
581
+ ];
582
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
583
+ resolvedConfigDirs = [
584
+ {
585
+ path: resolvedConfigDir,
586
+ level: 0
587
+ }
588
+ ];
589
+ } else {
590
+ resolvedConfigDirs = [];
591
+ }
592
+ }
593
+ } else {
594
+ // Use traditional single directory configuration loading
595
+ logger.verbose('Using single directory configuration loading for source tracking');
596
+ rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
597
+ const configFilePath = path.join(resolvedConfigDir, options.defaults.configFile);
598
+ tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
599
+ // Include the directory in discovered, and in resolved only if it had config
600
+ discoveredDirs = [
601
+ {
602
+ path: resolvedConfigDir,
603
+ level: 0
604
+ }
605
+ ];
606
+ if (rawFileConfig && Object.keys(rawFileConfig).length > 0) {
607
+ resolvedConfigDirs = [
608
+ {
609
+ path: resolvedConfigDir,
610
+ level: 0
611
+ }
612
+ ];
613
+ } else {
614
+ resolvedConfigDirs = [];
615
+ }
616
+ }
617
+ // Apply path resolution if configured (this doesn't change source tracking)
618
+ let processedConfig = rawFileConfig;
619
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
620
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
621
+ }
622
+ // Build final configuration including built-in values
623
+ const finalConfig = clean({
624
+ ...processedConfig,
625
+ configDirectory: resolvedConfigDir,
626
+ discoveredConfigDirs: discoveredDirs.map((dir)=>dir.path),
627
+ resolvedConfigDirs: resolvedConfigDirs.map((dir)=>dir.path)
628
+ });
629
+ // Add built-in configuration to tracker
630
+ tracker['configDirectory'] = {
631
+ value: resolvedConfigDir,
632
+ sourcePath: 'built-in',
633
+ level: -1,
634
+ sourceLabel: 'Built-in (runtime)'
635
+ };
636
+ tracker['discoveredConfigDirs'] = {
637
+ value: discoveredDirs.map((dir)=>dir.path),
638
+ sourcePath: 'built-in',
639
+ level: -1,
640
+ sourceLabel: 'Built-in (runtime)'
641
+ };
642
+ tracker['resolvedConfigDirs'] = {
643
+ value: resolvedConfigDirs.map((dir)=>dir.path),
644
+ sourcePath: 'built-in',
645
+ level: -1,
646
+ sourceLabel: 'Built-in (runtime)'
647
+ };
648
+ // Display the configuration with source information
649
+ displayConfigWithSources(finalConfig, tracker, discoveredDirs, logger);
650
+ };
651
+
652
+ export { checkConfig, read };
653
+ //# sourceMappingURL=read.js.map