@theunwalked/cardigantime 0.0.8 → 0.0.10
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.
- package/README.md +613 -4
- package/dist/cardigantime.cjs +530 -24
- package/dist/cardigantime.cjs.map +1 -1
- package/dist/cardigantime.d.ts +6 -0
- package/dist/cardigantime.js +79 -4
- package/dist/cardigantime.js.map +1 -1
- package/dist/configure.js +4 -0
- package/dist/configure.js.map +1 -1
- package/dist/read.d.ts +23 -0
- package/dist/read.js +260 -2
- package/dist/read.js.map +1 -1
- package/dist/types.d.ts +40 -0
- package/dist/types.js.map +1 -1
- package/dist/util/hierarchical.d.ts +18 -10
- package/dist/util/hierarchical.js +91 -22
- package/dist/util/hierarchical.js.map +1 -1
- package/dist/util/schema-defaults.d.ts +57 -0
- package/dist/util/schema-defaults.js +108 -0
- package/dist/util/schema-defaults.js.map +1 -0
- package/package.json +1 -1
package/dist/cardigantime.cjs
CHANGED
|
@@ -176,6 +176,10 @@ class ArgumentError extends Error {
|
|
|
176
176
|
throw error;
|
|
177
177
|
}
|
|
178
178
|
}, validatedDefaultDir);
|
|
179
|
+
// Add the init config option
|
|
180
|
+
retCommand = retCommand.option('--init-config', 'Generate initial configuration file and exit');
|
|
181
|
+
// Add the check config option
|
|
182
|
+
retCommand = retCommand.option('--check-config', 'Display resolved configuration with source tracking and exit');
|
|
179
183
|
return retCommand;
|
|
180
184
|
};
|
|
181
185
|
|
|
@@ -590,24 +594,26 @@ const create$1 = (params)=>{
|
|
|
590
594
|
}
|
|
591
595
|
}
|
|
592
596
|
/**
|
|
593
|
-
* Deep merges multiple configuration objects with proper precedence.
|
|
597
|
+
* Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
|
|
594
598
|
*
|
|
595
599
|
* Objects are merged from lowest precedence to highest precedence,
|
|
596
600
|
* meaning that properties in later objects override properties in earlier objects.
|
|
597
|
-
* Arrays
|
|
601
|
+
* Arrays can be merged using different strategies based on the fieldOverlaps configuration.
|
|
598
602
|
*
|
|
599
603
|
* @param configs Array of configuration objects, ordered from lowest to highest precedence
|
|
604
|
+
* @param fieldOverlaps Configuration for how array fields should be merged (optional)
|
|
600
605
|
* @returns Merged configuration object
|
|
601
606
|
*
|
|
602
607
|
* @example
|
|
603
608
|
* ```typescript
|
|
604
609
|
* const merged = deepMergeConfigs([
|
|
605
|
-
* { api: { timeout: 5000 },
|
|
606
|
-
* { api: { retries: 3 }, features: ['
|
|
607
|
-
* ]
|
|
608
|
-
* //
|
|
610
|
+
* { api: { timeout: 5000 }, features: ['auth'] }, // Lower precedence
|
|
611
|
+
* { api: { retries: 3 }, features: ['analytics'] }, // Higher precedence
|
|
612
|
+
* ], {
|
|
613
|
+
* 'features': 'append' // Results in features: ['auth', 'analytics']
|
|
614
|
+
* });
|
|
609
615
|
* ```
|
|
610
|
-
*/ function deepMergeConfigs(configs) {
|
|
616
|
+
*/ function deepMergeConfigs(configs, fieldOverlaps) {
|
|
611
617
|
if (configs.length === 0) {
|
|
612
618
|
return {};
|
|
613
619
|
}
|
|
@@ -617,16 +623,18 @@ const create$1 = (params)=>{
|
|
|
617
623
|
};
|
|
618
624
|
}
|
|
619
625
|
return configs.reduce((merged, current)=>{
|
|
620
|
-
return deepMergeTwo(merged, current);
|
|
626
|
+
return deepMergeTwo(merged, current, fieldOverlaps);
|
|
621
627
|
}, {});
|
|
622
628
|
}
|
|
623
629
|
/**
|
|
624
|
-
* Deep merges two objects with proper precedence.
|
|
630
|
+
* Deep merges two objects with proper precedence and configurable array overlap behavior.
|
|
625
631
|
*
|
|
626
632
|
* @param target Target object (lower precedence)
|
|
627
633
|
* @param source Source object (higher precedence)
|
|
634
|
+
* @param fieldOverlaps Configuration for how array fields should be merged (optional)
|
|
635
|
+
* @param currentPath Current field path for nested merging (used internally)
|
|
628
636
|
* @returns Merged object
|
|
629
|
-
*/ function deepMergeTwo(target, source) {
|
|
637
|
+
*/ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
|
|
630
638
|
// Handle null/undefined
|
|
631
639
|
if (source == null) return target;
|
|
632
640
|
if (target == null) return source;
|
|
@@ -634,11 +642,17 @@ const create$1 = (params)=>{
|
|
|
634
642
|
if (typeof source !== 'object' || typeof target !== 'object') {
|
|
635
643
|
return source; // Source takes precedence
|
|
636
644
|
}
|
|
637
|
-
// Handle arrays
|
|
645
|
+
// Handle arrays with configurable overlap behavior
|
|
638
646
|
if (Array.isArray(source)) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
647
|
+
if (Array.isArray(target) && fieldOverlaps) {
|
|
648
|
+
const overlapMode = getOverlapModeForPath(currentPath, fieldOverlaps);
|
|
649
|
+
return mergeArrays(target, source, overlapMode);
|
|
650
|
+
} else {
|
|
651
|
+
// Default behavior: replace entirely
|
|
652
|
+
return [
|
|
653
|
+
...source
|
|
654
|
+
];
|
|
655
|
+
}
|
|
642
656
|
}
|
|
643
657
|
if (Array.isArray(target)) {
|
|
644
658
|
return source; // Source object replaces target array
|
|
@@ -649,17 +663,72 @@ const create$1 = (params)=>{
|
|
|
649
663
|
};
|
|
650
664
|
for(const key in source){
|
|
651
665
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
666
|
+
const fieldPath = currentPath ? `${currentPath}.${key}` : key;
|
|
652
667
|
if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
|
|
653
668
|
// Recursively merge nested objects
|
|
654
|
-
result[key] = deepMergeTwo(result[key], source[key]);
|
|
669
|
+
result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
|
|
655
670
|
} else {
|
|
656
|
-
//
|
|
657
|
-
|
|
671
|
+
// Handle arrays and primitives with overlap consideration
|
|
672
|
+
if (Array.isArray(source[key]) && Array.isArray(result[key]) && fieldOverlaps) {
|
|
673
|
+
const overlapMode = getOverlapModeForPath(fieldPath, fieldOverlaps);
|
|
674
|
+
result[key] = mergeArrays(result[key], source[key], overlapMode);
|
|
675
|
+
} else {
|
|
676
|
+
// Replace with source value (higher precedence)
|
|
677
|
+
result[key] = source[key];
|
|
678
|
+
}
|
|
658
679
|
}
|
|
659
680
|
}
|
|
660
681
|
}
|
|
661
682
|
return result;
|
|
662
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Determines the overlap mode for a given field path.
|
|
686
|
+
*
|
|
687
|
+
* @param fieldPath The current field path (dot notation)
|
|
688
|
+
* @param fieldOverlaps Configuration mapping field paths to overlap modes
|
|
689
|
+
* @returns The overlap mode to use for this field path
|
|
690
|
+
*/ function getOverlapModeForPath(fieldPath, fieldOverlaps) {
|
|
691
|
+
// Check for exact match first
|
|
692
|
+
if (fieldPath in fieldOverlaps) {
|
|
693
|
+
return fieldOverlaps[fieldPath];
|
|
694
|
+
}
|
|
695
|
+
// Check for any parent path matches (for nested configurations)
|
|
696
|
+
const pathParts = fieldPath.split('.');
|
|
697
|
+
for(let i = pathParts.length - 1; i > 0; i--){
|
|
698
|
+
const parentPath = pathParts.slice(0, i).join('.');
|
|
699
|
+
if (parentPath in fieldOverlaps) {
|
|
700
|
+
return fieldOverlaps[parentPath];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Default to override if no specific configuration found
|
|
704
|
+
return 'override';
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Merges two arrays based on the specified overlap mode.
|
|
708
|
+
*
|
|
709
|
+
* @param targetArray The lower precedence array
|
|
710
|
+
* @param sourceArray The higher precedence array
|
|
711
|
+
* @param mode The overlap mode to use
|
|
712
|
+
* @returns The merged array
|
|
713
|
+
*/ function mergeArrays(targetArray, sourceArray, mode) {
|
|
714
|
+
switch(mode){
|
|
715
|
+
case 'append':
|
|
716
|
+
return [
|
|
717
|
+
...targetArray,
|
|
718
|
+
...sourceArray
|
|
719
|
+
];
|
|
720
|
+
case 'prepend':
|
|
721
|
+
return [
|
|
722
|
+
...sourceArray,
|
|
723
|
+
...targetArray
|
|
724
|
+
];
|
|
725
|
+
case 'override':
|
|
726
|
+
default:
|
|
727
|
+
return [
|
|
728
|
+
...sourceArray
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
663
732
|
/**
|
|
664
733
|
* Loads configurations from multiple directories and merges them with proper precedence.
|
|
665
734
|
*
|
|
@@ -678,15 +747,19 @@ const create$1 = (params)=>{
|
|
|
678
747
|
* configDirName: '.kodrdriv',
|
|
679
748
|
* configFileName: 'config.yaml',
|
|
680
749
|
* startingDir: '/project/subdir',
|
|
681
|
-
* maxLevels: 5
|
|
750
|
+
* maxLevels: 5,
|
|
751
|
+
* fieldOverlaps: {
|
|
752
|
+
* 'features': 'append',
|
|
753
|
+
* 'excludePatterns': 'prepend'
|
|
754
|
+
* }
|
|
682
755
|
* });
|
|
683
756
|
*
|
|
684
|
-
* // result.config contains merged configuration
|
|
757
|
+
* // result.config contains merged configuration with custom array merging
|
|
685
758
|
* // result.discoveredDirs shows where configs were found
|
|
686
759
|
* // result.errors contains any non-fatal errors
|
|
687
760
|
* ```
|
|
688
761
|
*/ async function loadHierarchicalConfig(options) {
|
|
689
|
-
const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray } = options;
|
|
762
|
+
const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
|
|
690
763
|
logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
|
|
691
764
|
// Discover all configuration directories
|
|
692
765
|
const discoveredDirs = await discoverConfigDirectories(options);
|
|
@@ -720,8 +793,8 @@ const create$1 = (params)=>{
|
|
|
720
793
|
logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
|
|
721
794
|
}
|
|
722
795
|
}
|
|
723
|
-
// Merge all configurations with proper precedence
|
|
724
|
-
const mergedConfig = deepMergeConfigs(configs);
|
|
796
|
+
// Merge all configurations with proper precedence and configurable array overlap
|
|
797
|
+
const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
|
|
725
798
|
logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
|
|
726
799
|
return {
|
|
727
800
|
config: mergedConfig,
|
|
@@ -909,7 +982,8 @@ const create$1 = (params)=>{
|
|
|
909
982
|
encoding: options.defaults.encoding,
|
|
910
983
|
logger,
|
|
911
984
|
pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
|
|
912
|
-
resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray
|
|
985
|
+
resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
|
|
986
|
+
fieldOverlaps: options.defaults.fieldOverlaps
|
|
913
987
|
});
|
|
914
988
|
rawFileConfig = hierarchicalResult.config;
|
|
915
989
|
if (hierarchicalResult.discoveredDirs.length > 0) {
|
|
@@ -981,6 +1055,263 @@ const create$1 = (params)=>{
|
|
|
981
1055
|
}
|
|
982
1056
|
return rawFileConfig;
|
|
983
1057
|
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Recursively tracks the source of configuration values from hierarchical loading.
|
|
1060
|
+
*
|
|
1061
|
+
* @param config - The configuration object to track
|
|
1062
|
+
* @param sourcePath - Path to the configuration file
|
|
1063
|
+
* @param level - Hierarchical level
|
|
1064
|
+
* @param prefix - Current object path prefix for nested values
|
|
1065
|
+
* @param tracker - The tracker object to populate
|
|
1066
|
+
*/ function trackConfigSources(config, sourcePath, level, prefix = '', tracker = {}) {
|
|
1067
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
1068
|
+
// For primitives and arrays, track the entire value
|
|
1069
|
+
tracker[prefix] = {
|
|
1070
|
+
value: config,
|
|
1071
|
+
sourcePath,
|
|
1072
|
+
level,
|
|
1073
|
+
sourceLabel: `Level ${level}: ${path__namespace.basename(path__namespace.dirname(sourcePath))}`
|
|
1074
|
+
};
|
|
1075
|
+
return tracker;
|
|
1076
|
+
}
|
|
1077
|
+
// For objects, recursively track each property
|
|
1078
|
+
for (const [key, value] of Object.entries(config)){
|
|
1079
|
+
const fieldPath = prefix ? `${prefix}.${key}` : key;
|
|
1080
|
+
trackConfigSources(value, sourcePath, level, fieldPath, tracker);
|
|
1081
|
+
}
|
|
1082
|
+
return tracker;
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Merges multiple configuration source trackers with proper precedence.
|
|
1086
|
+
* Lower level numbers have higher precedence.
|
|
1087
|
+
*
|
|
1088
|
+
* @param trackers - Array of trackers from different config sources
|
|
1089
|
+
* @returns Merged tracker with proper precedence
|
|
1090
|
+
*/ function mergeConfigTrackers(trackers) {
|
|
1091
|
+
const merged = {};
|
|
1092
|
+
for (const tracker of trackers){
|
|
1093
|
+
for (const [key, info] of Object.entries(tracker)){
|
|
1094
|
+
// Only update if we don't have this key yet, or if this source has higher precedence (lower level)
|
|
1095
|
+
if (!merged[key] || info.level < merged[key].level) {
|
|
1096
|
+
merged[key] = info;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return merged;
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Formats a configuration value for display, handling different types appropriately.
|
|
1104
|
+
*
|
|
1105
|
+
* @param value - The configuration value to format
|
|
1106
|
+
* @returns Formatted string representation
|
|
1107
|
+
*/ function formatConfigValue(value) {
|
|
1108
|
+
if (value === null) return 'null';
|
|
1109
|
+
if (value === undefined) return 'undefined';
|
|
1110
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
1111
|
+
if (typeof value === 'boolean') return value.toString();
|
|
1112
|
+
if (typeof value === 'number') return value.toString();
|
|
1113
|
+
if (Array.isArray(value)) {
|
|
1114
|
+
if (value.length === 0) return '[]';
|
|
1115
|
+
if (value.length <= 3) {
|
|
1116
|
+
return `[${value.map(formatConfigValue).join(', ')}]`;
|
|
1117
|
+
}
|
|
1118
|
+
return `[${value.slice(0, 2).map(formatConfigValue).join(', ')}, ... (${value.length} items)]`;
|
|
1119
|
+
}
|
|
1120
|
+
if (typeof value === 'object') {
|
|
1121
|
+
const keys = Object.keys(value);
|
|
1122
|
+
if (keys.length === 0) return '{}';
|
|
1123
|
+
if (keys.length <= 2) {
|
|
1124
|
+
return `{${keys.slice(0, 2).join(', ')}}`;
|
|
1125
|
+
}
|
|
1126
|
+
return `{${keys.slice(0, 2).join(', ')}, ... (${keys.length} keys)}`;
|
|
1127
|
+
}
|
|
1128
|
+
return String(value);
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Displays configuration with source tracking in a git blame-like format.
|
|
1132
|
+
*
|
|
1133
|
+
* @param config - The resolved configuration object
|
|
1134
|
+
* @param tracker - Configuration source tracker
|
|
1135
|
+
* @param discoveredDirs - Array of discovered configuration directories
|
|
1136
|
+
* @param logger - Logger instance for output
|
|
1137
|
+
*/ function displayConfigWithSources(config, tracker, discoveredDirs, logger) {
|
|
1138
|
+
logger.info('\n' + '='.repeat(80));
|
|
1139
|
+
logger.info('CONFIGURATION SOURCE ANALYSIS');
|
|
1140
|
+
logger.info('='.repeat(80));
|
|
1141
|
+
// Display discovered configuration hierarchy
|
|
1142
|
+
logger.info('\nDISCOVERED CONFIGURATION HIERARCHY:');
|
|
1143
|
+
if (discoveredDirs.length === 0) {
|
|
1144
|
+
logger.info(' No configuration directories found in hierarchy');
|
|
1145
|
+
} else {
|
|
1146
|
+
discoveredDirs.sort((a, b)=>a.level - b.level) // Sort by precedence (lower level = higher precedence)
|
|
1147
|
+
.forEach((dir)=>{
|
|
1148
|
+
const precedence = dir.level === 0 ? '(highest precedence)' : dir.level === Math.max(...discoveredDirs.map((d)=>d.level)) ? '(lowest precedence)' : '';
|
|
1149
|
+
logger.info(` Level ${dir.level}: ${dir.path} ${precedence}`);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
// Display resolved configuration with sources
|
|
1153
|
+
logger.info('\nRESOLVED CONFIGURATION WITH SOURCES:');
|
|
1154
|
+
logger.info('Format: [Source] key: value\n');
|
|
1155
|
+
const sortedKeys = Object.keys(tracker).sort();
|
|
1156
|
+
const maxKeyLength = Math.max(...sortedKeys.map((k)=>k.length), 20);
|
|
1157
|
+
const maxSourceLength = Math.max(...Object.values(tracker).map((info)=>info.sourceLabel.length), 25);
|
|
1158
|
+
for (const key of sortedKeys){
|
|
1159
|
+
const info = tracker[key];
|
|
1160
|
+
const paddedKey = key.padEnd(maxKeyLength);
|
|
1161
|
+
const paddedSource = info.sourceLabel.padEnd(maxSourceLength);
|
|
1162
|
+
const formattedValue = formatConfigValue(info.value);
|
|
1163
|
+
logger.info(`[${paddedSource}] ${paddedKey}: ${formattedValue}`);
|
|
1164
|
+
}
|
|
1165
|
+
// Display summary
|
|
1166
|
+
logger.info('\n' + '-'.repeat(80));
|
|
1167
|
+
logger.info('SUMMARY:');
|
|
1168
|
+
logger.info(` Total configuration keys: ${Object.keys(tracker).length}`);
|
|
1169
|
+
logger.info(` Configuration sources: ${discoveredDirs.length}`);
|
|
1170
|
+
// Count values by source
|
|
1171
|
+
const sourceCount = {};
|
|
1172
|
+
for (const info of Object.values(tracker)){
|
|
1173
|
+
sourceCount[info.sourceLabel] = (sourceCount[info.sourceLabel] || 0) + 1;
|
|
1174
|
+
}
|
|
1175
|
+
logger.info(' Values by source:');
|
|
1176
|
+
for (const [source, count] of Object.entries(sourceCount)){
|
|
1177
|
+
logger.info(` ${source}: ${count} value(s)`);
|
|
1178
|
+
}
|
|
1179
|
+
logger.info('='.repeat(80));
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Checks and displays the resolved configuration with detailed source tracking.
|
|
1183
|
+
*
|
|
1184
|
+
* This function provides a git blame-like view of configuration resolution,
|
|
1185
|
+
* showing which file and hierarchical level contributed each configuration value.
|
|
1186
|
+
*
|
|
1187
|
+
* @template T - The Zod schema shape type for configuration validation
|
|
1188
|
+
* @param args - Parsed command-line arguments
|
|
1189
|
+
* @param options - Cardigantime options with defaults, schema, and logger
|
|
1190
|
+
* @returns Promise that resolves when the configuration check is complete
|
|
1191
|
+
*
|
|
1192
|
+
* @example
|
|
1193
|
+
* ```typescript
|
|
1194
|
+
* await checkConfig(cliArgs, {
|
|
1195
|
+
* defaults: { configDirectory: './config', configFile: 'app.yaml' },
|
|
1196
|
+
* configShape: MySchema.shape,
|
|
1197
|
+
* logger: console,
|
|
1198
|
+
* features: ['config', 'hierarchical']
|
|
1199
|
+
* });
|
|
1200
|
+
* // Outputs detailed configuration source analysis
|
|
1201
|
+
* ```
|
|
1202
|
+
*/ const checkConfig = async (args, options)=>{
|
|
1203
|
+
var _options_defaults, _options_defaults_pathResolution;
|
|
1204
|
+
const logger = options.logger;
|
|
1205
|
+
logger.info('Starting configuration check...');
|
|
1206
|
+
const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
|
|
1207
|
+
if (!rawConfigDir) {
|
|
1208
|
+
throw new Error('Configuration directory must be specified');
|
|
1209
|
+
}
|
|
1210
|
+
const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
|
|
1211
|
+
logger.verbose(`Resolved config directory: ${resolvedConfigDir}`);
|
|
1212
|
+
let rawFileConfig = {};
|
|
1213
|
+
let discoveredDirs = [];
|
|
1214
|
+
let tracker = {};
|
|
1215
|
+
// Check if hierarchical configuration discovery is enabled
|
|
1216
|
+
if (options.features.includes('hierarchical')) {
|
|
1217
|
+
logger.verbose('Using hierarchical configuration discovery for source tracking');
|
|
1218
|
+
try {
|
|
1219
|
+
var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
|
|
1220
|
+
// Extract the config directory name from the path for hierarchical discovery
|
|
1221
|
+
const configDirName = path__namespace.basename(resolvedConfigDir);
|
|
1222
|
+
const startingDir = path__namespace.dirname(resolvedConfigDir);
|
|
1223
|
+
logger.debug(`Using hierarchical discovery: configDirName=${configDirName}, startingDir=${startingDir}`);
|
|
1224
|
+
const hierarchicalResult = await loadHierarchicalConfig({
|
|
1225
|
+
configDirName,
|
|
1226
|
+
configFileName: options.defaults.configFile,
|
|
1227
|
+
startingDir,
|
|
1228
|
+
encoding: options.defaults.encoding,
|
|
1229
|
+
logger,
|
|
1230
|
+
pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
|
|
1231
|
+
resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
|
|
1232
|
+
fieldOverlaps: options.defaults.fieldOverlaps
|
|
1233
|
+
});
|
|
1234
|
+
rawFileConfig = hierarchicalResult.config;
|
|
1235
|
+
discoveredDirs = hierarchicalResult.discoveredDirs;
|
|
1236
|
+
// Build detailed source tracking by re-loading each config individually
|
|
1237
|
+
const trackers = [];
|
|
1238
|
+
// Sort by level (highest level first = lowest precedence first) to match merge order
|
|
1239
|
+
const sortedDirs = [
|
|
1240
|
+
...discoveredDirs
|
|
1241
|
+
].sort((a, b)=>b.level - a.level);
|
|
1242
|
+
for (const dir of sortedDirs){
|
|
1243
|
+
const storage = create$1({
|
|
1244
|
+
log: logger.debug
|
|
1245
|
+
});
|
|
1246
|
+
const configFilePath = path__namespace.join(dir.path, options.defaults.configFile);
|
|
1247
|
+
try {
|
|
1248
|
+
const exists = await storage.exists(configFilePath);
|
|
1249
|
+
if (!exists) continue;
|
|
1250
|
+
const isReadable = await storage.isFileReadable(configFilePath);
|
|
1251
|
+
if (!isReadable) continue;
|
|
1252
|
+
const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
|
|
1253
|
+
const parsedYaml = yaml__namespace.load(yamlContent);
|
|
1254
|
+
if (parsedYaml !== null && typeof parsedYaml === 'object') {
|
|
1255
|
+
const levelTracker = trackConfigSources(parsedYaml, configFilePath, dir.level);
|
|
1256
|
+
trackers.push(levelTracker);
|
|
1257
|
+
}
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
logger.debug(`Error loading config for source tracking from ${configFilePath}: ${error.message}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
// Merge trackers with proper precedence
|
|
1263
|
+
tracker = mergeConfigTrackers(trackers);
|
|
1264
|
+
if (hierarchicalResult.errors.length > 0) {
|
|
1265
|
+
logger.warn('Configuration loading warnings:');
|
|
1266
|
+
hierarchicalResult.errors.forEach((error)=>logger.warn(` ${error}`));
|
|
1267
|
+
}
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
|
|
1270
|
+
logger.verbose('Falling back to single directory configuration loading');
|
|
1271
|
+
// Fall back to single directory mode for source tracking
|
|
1272
|
+
rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
|
|
1273
|
+
const configFilePath = path__namespace.join(resolvedConfigDir, options.defaults.configFile);
|
|
1274
|
+
tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
|
|
1275
|
+
discoveredDirs = [
|
|
1276
|
+
{
|
|
1277
|
+
path: resolvedConfigDir,
|
|
1278
|
+
level: 0
|
|
1279
|
+
}
|
|
1280
|
+
];
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
// Use traditional single directory configuration loading
|
|
1284
|
+
logger.verbose('Using single directory configuration loading for source tracking');
|
|
1285
|
+
rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
|
|
1286
|
+
const configFilePath = path__namespace.join(resolvedConfigDir, options.defaults.configFile);
|
|
1287
|
+
tracker = trackConfigSources(rawFileConfig, configFilePath, 0);
|
|
1288
|
+
discoveredDirs = [
|
|
1289
|
+
{
|
|
1290
|
+
path: resolvedConfigDir,
|
|
1291
|
+
level: 0
|
|
1292
|
+
}
|
|
1293
|
+
];
|
|
1294
|
+
}
|
|
1295
|
+
// Apply path resolution if configured (this doesn't change source tracking)
|
|
1296
|
+
let processedConfig = rawFileConfig;
|
|
1297
|
+
if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
|
|
1298
|
+
processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
|
|
1299
|
+
}
|
|
1300
|
+
// Build final configuration including built-in values
|
|
1301
|
+
const finalConfig = clean({
|
|
1302
|
+
...processedConfig,
|
|
1303
|
+
configDirectory: resolvedConfigDir
|
|
1304
|
+
});
|
|
1305
|
+
// Add built-in configuration to tracker
|
|
1306
|
+
tracker['configDirectory'] = {
|
|
1307
|
+
value: resolvedConfigDir,
|
|
1308
|
+
sourcePath: 'built-in',
|
|
1309
|
+
level: -1,
|
|
1310
|
+
sourceLabel: 'Built-in (runtime)'
|
|
1311
|
+
};
|
|
1312
|
+
// Display the configuration with source information
|
|
1313
|
+
displayConfigWithSources(finalConfig, tracker, discoveredDirs, logger);
|
|
1314
|
+
};
|
|
984
1315
|
|
|
985
1316
|
/**
|
|
986
1317
|
* Error thrown when configuration validation fails
|
|
@@ -1293,6 +1624,110 @@ class ConfigurationError extends Error {
|
|
|
1293
1624
|
return;
|
|
1294
1625
|
};
|
|
1295
1626
|
|
|
1627
|
+
/**
|
|
1628
|
+
* Extracts default values from a Zod schema recursively.
|
|
1629
|
+
*
|
|
1630
|
+
* This function traverses a Zod schema and builds an object containing
|
|
1631
|
+
* all the default values defined in the schema. It handles:
|
|
1632
|
+
* - ZodDefault types with explicit default values
|
|
1633
|
+
* - ZodOptional/ZodNullable types by unwrapping them
|
|
1634
|
+
* - ZodObject types by recursively processing their shape
|
|
1635
|
+
* - ZodArray types by providing an empty array as default
|
|
1636
|
+
*
|
|
1637
|
+
* @param schema - The Zod schema to extract defaults from
|
|
1638
|
+
* @returns An object containing all default values from the schema
|
|
1639
|
+
*
|
|
1640
|
+
* @example
|
|
1641
|
+
* ```typescript
|
|
1642
|
+
* const schema = z.object({
|
|
1643
|
+
* name: z.string().default('app'),
|
|
1644
|
+
* port: z.number().default(3000),
|
|
1645
|
+
* debug: z.boolean().default(false),
|
|
1646
|
+
* database: z.object({
|
|
1647
|
+
* host: z.string().default('localhost'),
|
|
1648
|
+
* port: z.number().default(5432)
|
|
1649
|
+
* })
|
|
1650
|
+
* });
|
|
1651
|
+
*
|
|
1652
|
+
* const defaults = extractSchemaDefaults(schema);
|
|
1653
|
+
* // Returns: { name: 'app', port: 3000, debug: false, database: { host: 'localhost', port: 5432 } }
|
|
1654
|
+
* ```
|
|
1655
|
+
*/ const extractSchemaDefaults = (schema)=>{
|
|
1656
|
+
// Handle ZodDefault - extract the default value
|
|
1657
|
+
if (schema._def && schema._def.typeName === 'ZodDefault') {
|
|
1658
|
+
const defaultSchema = schema;
|
|
1659
|
+
return defaultSchema._def.defaultValue();
|
|
1660
|
+
}
|
|
1661
|
+
// Handle ZodOptional and ZodNullable - unwrap and recurse
|
|
1662
|
+
if (schema._def && (schema._def.typeName === 'ZodOptional' || schema._def.typeName === 'ZodNullable')) {
|
|
1663
|
+
const unwrappable = schema;
|
|
1664
|
+
return extractSchemaDefaults(unwrappable.unwrap());
|
|
1665
|
+
}
|
|
1666
|
+
// Handle ZodObject - recursively process shape
|
|
1667
|
+
if (schema._def && schema._def.typeName === 'ZodObject') {
|
|
1668
|
+
const objectSchema = schema;
|
|
1669
|
+
const result = {};
|
|
1670
|
+
for (const [key, subschema] of Object.entries(objectSchema.shape)){
|
|
1671
|
+
const defaultValue = extractSchemaDefaults(subschema);
|
|
1672
|
+
if (defaultValue !== undefined) {
|
|
1673
|
+
result[key] = defaultValue;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
1677
|
+
}
|
|
1678
|
+
// Handle ZodArray - provide empty array as default
|
|
1679
|
+
if (schema._def && schema._def.typeName === 'ZodArray') {
|
|
1680
|
+
const arraySchema = schema;
|
|
1681
|
+
const elementDefaults = extractSchemaDefaults(arraySchema.element);
|
|
1682
|
+
// Return an empty array, or an array with one example element if it has defaults
|
|
1683
|
+
return elementDefaults !== undefined ? [
|
|
1684
|
+
elementDefaults
|
|
1685
|
+
] : [];
|
|
1686
|
+
}
|
|
1687
|
+
// Handle ZodRecord - provide empty object as default
|
|
1688
|
+
if (schema._def && schema._def.typeName === 'ZodRecord') {
|
|
1689
|
+
return {};
|
|
1690
|
+
}
|
|
1691
|
+
// For other types, return undefined (no default available)
|
|
1692
|
+
return undefined;
|
|
1693
|
+
};
|
|
1694
|
+
/**
|
|
1695
|
+
* Generates a complete configuration object with all default values populated.
|
|
1696
|
+
*
|
|
1697
|
+
* This function combines the base ConfigSchema with a user-provided schema shape
|
|
1698
|
+
* and extracts all available default values to create a complete configuration
|
|
1699
|
+
* example that can be serialized to YAML.
|
|
1700
|
+
*
|
|
1701
|
+
* @template T - The Zod schema shape type
|
|
1702
|
+
* @param configShape - The user's configuration schema shape
|
|
1703
|
+
* @param configDirectory - The configuration directory to include in the defaults
|
|
1704
|
+
* @returns An object containing all default values suitable for YAML serialization
|
|
1705
|
+
*
|
|
1706
|
+
* @example
|
|
1707
|
+
* ```typescript
|
|
1708
|
+
* const shape = z.object({
|
|
1709
|
+
* apiKey: z.string().describe('Your API key'),
|
|
1710
|
+
* timeout: z.number().default(5000).describe('Request timeout in milliseconds'),
|
|
1711
|
+
* features: z.array(z.string()).default(['auth', 'logging'])
|
|
1712
|
+
* }).shape;
|
|
1713
|
+
*
|
|
1714
|
+
* const config = generateDefaultConfig(shape, './config');
|
|
1715
|
+
* // Returns: { timeout: 5000, features: ['auth', 'logging'] }
|
|
1716
|
+
* // Note: apiKey is not included since it has no default
|
|
1717
|
+
* ```
|
|
1718
|
+
*/ const generateDefaultConfig = (configShape, configDirectory)=>{
|
|
1719
|
+
// Create the full schema by combining base and user schema
|
|
1720
|
+
const fullSchema = zod.z.object({
|
|
1721
|
+
...configShape
|
|
1722
|
+
});
|
|
1723
|
+
// Extract defaults from the full schema
|
|
1724
|
+
const defaults = extractSchemaDefaults(fullSchema);
|
|
1725
|
+
// Don't include configDirectory in the generated file since it's runtime-specific
|
|
1726
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1727
|
+
const { configDirectory: _, ...configDefaults } = defaults || {};
|
|
1728
|
+
return configDefaults || {};
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1296
1731
|
/**
|
|
1297
1732
|
* Creates a new Cardigantime instance for configuration management.
|
|
1298
1733
|
*
|
|
@@ -1335,9 +1770,15 @@ class ConfigurationError extends Error {
|
|
|
1335
1770
|
* pathResolution: {
|
|
1336
1771
|
* pathFields: ['contextDirectories'],
|
|
1337
1772
|
* resolvePathArray: ['contextDirectories']
|
|
1773
|
+
* },
|
|
1774
|
+
* // Configure how array fields are merged in hierarchical mode
|
|
1775
|
+
* fieldOverlaps: {
|
|
1776
|
+
* 'features': 'append', // Accumulate features from all levels
|
|
1777
|
+
* 'excludePatterns': 'prepend' // Higher precedence patterns come first
|
|
1338
1778
|
* }
|
|
1339
1779
|
* },
|
|
1340
1780
|
* configShape: MyConfigSchema.shape,
|
|
1781
|
+
* features: ['config', 'hierarchical'], // Enable hierarchical discovery
|
|
1341
1782
|
* });
|
|
1342
1783
|
*
|
|
1343
1784
|
* // If config file is at ../config/myapp.yaml and contains:
|
|
@@ -1362,11 +1803,76 @@ class ConfigurationError extends Error {
|
|
|
1362
1803
|
logger = pLogger;
|
|
1363
1804
|
options.logger = pLogger;
|
|
1364
1805
|
};
|
|
1806
|
+
const generateConfig = async (configDirectory)=>{
|
|
1807
|
+
const targetDir = configDirectory || options.defaults.configDirectory;
|
|
1808
|
+
const configFile = options.defaults.configFile;
|
|
1809
|
+
const encoding = options.defaults.encoding;
|
|
1810
|
+
logger.verbose(`Generating configuration file in: ${targetDir}`);
|
|
1811
|
+
// Create storage utility
|
|
1812
|
+
const storage = create$1({
|
|
1813
|
+
log: logger.debug
|
|
1814
|
+
});
|
|
1815
|
+
// Ensure the target directory exists
|
|
1816
|
+
const dirExists = await storage.exists(targetDir);
|
|
1817
|
+
if (!dirExists) {
|
|
1818
|
+
logger.info(`Creating configuration directory: ${targetDir}`);
|
|
1819
|
+
try {
|
|
1820
|
+
await storage.createDirectory(targetDir);
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
throw FileSystemError.directoryCreationFailed(targetDir, error);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
// Check if directory is writable
|
|
1826
|
+
const isWritable = await storage.isDirectoryWritable(targetDir);
|
|
1827
|
+
if (!isWritable) {
|
|
1828
|
+
throw new FileSystemError('not_writable', 'Configuration directory is not writable', targetDir, 'directory_write');
|
|
1829
|
+
}
|
|
1830
|
+
// Build the full config file path
|
|
1831
|
+
const configFilePath = path__namespace.join(targetDir, configFile);
|
|
1832
|
+
// Generate default configuration
|
|
1833
|
+
const defaultConfig = generateDefaultConfig(options.configShape);
|
|
1834
|
+
// Convert to YAML with nice formatting
|
|
1835
|
+
const yamlContent = yaml__namespace.dump(defaultConfig, {
|
|
1836
|
+
indent: 2,
|
|
1837
|
+
lineWidth: 120,
|
|
1838
|
+
noRefs: true,
|
|
1839
|
+
sortKeys: true
|
|
1840
|
+
});
|
|
1841
|
+
// Add header comment to the YAML file
|
|
1842
|
+
const header = `# Configuration file generated by Cardigantime
|
|
1843
|
+
# This file contains default values for your application configuration.
|
|
1844
|
+
# Modify the values below to customize your application's behavior.
|
|
1845
|
+
#
|
|
1846
|
+
# For more information about Cardigantime configuration:
|
|
1847
|
+
# https://github.com/SemicolonAmbulance/cardigantime
|
|
1848
|
+
|
|
1849
|
+
`;
|
|
1850
|
+
const finalContent = header + yamlContent;
|
|
1851
|
+
// Check if config file already exists
|
|
1852
|
+
const configExists = await storage.exists(configFilePath);
|
|
1853
|
+
if (configExists) {
|
|
1854
|
+
logger.warn(`Configuration file already exists: ${configFilePath}`);
|
|
1855
|
+
logger.warn('This file was not overwritten, but here is what the default configuration looks like if you want to copy it:');
|
|
1856
|
+
logger.info('\n' + '='.repeat(60));
|
|
1857
|
+
logger.info(finalContent.trim());
|
|
1858
|
+
logger.info('='.repeat(60));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
// Write the configuration file
|
|
1862
|
+
try {
|
|
1863
|
+
await storage.writeFile(configFilePath, finalContent, encoding);
|
|
1864
|
+
logger.info(`Configuration file generated successfully: ${configFilePath}`);
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
throw FileSystemError.operationFailed('write configuration file', configFilePath, error);
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1365
1869
|
return {
|
|
1366
1870
|
setLogger,
|
|
1367
1871
|
configure: (command)=>configure(command, options),
|
|
1368
1872
|
validate: (config)=>validate(config, options),
|
|
1369
|
-
read: (args)=>read(args, options)
|
|
1873
|
+
read: (args)=>read(args, options),
|
|
1874
|
+
generateConfig,
|
|
1875
|
+
checkConfig: (args)=>checkConfig(args, options)
|
|
1370
1876
|
};
|
|
1371
1877
|
};
|
|
1372
1878
|
|