@theunwalked/cardigantime 0.0.7 → 0.0.9

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 CHANGED
@@ -233,6 +233,60 @@ debug: true
233
233
  ./myapp --debug
234
234
  ```
235
235
 
236
+ ### Advanced Usage Examples
237
+
238
+ #### Basic Configuration with Path Resolution
239
+
240
+ ```typescript
241
+ import { create } from '@theunwalked/cardigantime';
242
+ import { z } from 'zod';
243
+
244
+ const MyConfigSchema = z.object({
245
+ apiKey: z.string().min(1),
246
+ timeout: z.number().default(5000),
247
+ debug: z.boolean().default(false),
248
+ contextDirectories: z.array(z.string()).optional(),
249
+ });
250
+
251
+ const cardigantime = create({
252
+ defaults: {
253
+ configDirectory: './config',
254
+ configFile: 'myapp.yaml',
255
+ // Resolve relative paths in contextDirectories relative to config file location
256
+ pathResolution: {
257
+ pathFields: ['contextDirectories'],
258
+ resolvePathArray: ['contextDirectories']
259
+ }
260
+ },
261
+ configShape: MyConfigSchema.shape,
262
+ });
263
+ ```
264
+
265
+ #### Hierarchical Configuration with Custom Array Overlap
266
+
267
+ ```typescript
268
+ const cardigantime = create({
269
+ defaults: {
270
+ configDirectory: '.myapp',
271
+ configFile: 'config.yaml',
272
+ fieldOverlaps: {
273
+ 'features': 'append', // Accumulate features from all levels
274
+ 'excludePatterns': 'prepend', // Higher precedence patterns come first
275
+ 'api.endpoints': 'append', // Nested field configuration
276
+ 'security.allowedOrigins': 'append' // Security settings accumulate
277
+ }
278
+ },
279
+ configShape: MyConfigSchema.shape,
280
+ features: ['config', 'hierarchical'], // Enable hierarchical discovery
281
+ });
282
+ ```
283
+
284
+ This configuration enables powerful composition scenarios where:
285
+ - **Features** from all configuration levels are combined (e.g., base features + project features + local features)
286
+ - **Exclude patterns** are layered with local patterns taking precedence
287
+ - **API endpoints** can be extended at each level
288
+ - **Security settings** accumulate for maximum flexibility
289
+
236
290
  ## Core Concepts
237
291
 
238
292
  ### 1. Configuration Sources & Precedence
@@ -297,6 +351,9 @@ database:
297
351
  ssl: false
298
352
  logging:
299
353
  level: info
354
+ features:
355
+ - auth
356
+ - basic-logging
300
357
 
301
358
  # /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
302
359
  database:
@@ -304,14 +361,19 @@ database:
304
361
  ssl: true
305
362
  api:
306
363
  timeout: 5000
364
+ features:
365
+ - advanced-logging
366
+ - metrics
307
367
 
308
368
  # /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
309
369
  database:
310
370
  host: dev.example.com
311
371
  logging:
312
372
  level: debug
373
+ features:
374
+ - debug-mode
313
375
 
314
- # Final merged configuration:
376
+ # Final merged configuration (with default array behavior):
315
377
  database:
316
378
  host: dev.example.com # From Level 0 (highest precedence)
317
379
  port: 5433 # From Level 1
@@ -320,6 +382,79 @@ api:
320
382
  timeout: 5000 # From Level 1
321
383
  logging:
322
384
  level: debug # From Level 0 (highest precedence)
385
+ features:
386
+ - debug-mode # From Level 0 (arrays override by default)
387
+ ```
388
+
389
+ #### Configurable Array Overlap Behavior
390
+
391
+ By default, arrays in hierarchical configurations follow the **override** behavior - arrays from higher precedence levels completely replace arrays from lower precedence levels. However, you can configure custom overlap behavior for array fields:
392
+
393
+ ```typescript
394
+ const cardigantime = create({
395
+ defaults: {
396
+ configDirectory: '.kodrdriv',
397
+ configFile: 'config.yaml',
398
+ fieldOverlaps: {
399
+ 'features': 'append', // Combine features by appending
400
+ 'excludePatterns': 'prepend', // Combine exclude patterns by prepending
401
+ 'middlewares': 'override' // Override middlewares (default behavior)
402
+ }
403
+ },
404
+ configShape: MyConfigSchema.shape,
405
+ features: ['config', 'hierarchical'],
406
+ });
407
+ ```
408
+
409
+ **Available Overlap Modes:**
410
+
411
+ - **`override`** (default): Higher precedence arrays completely replace lower precedence arrays
412
+ - **`append`**: Higher precedence array elements are appended to lower precedence arrays
413
+ - **`prepend`**: Higher precedence array elements are prepended to lower precedence arrays
414
+
415
+ **Example with Custom Array Overlap:**
416
+
417
+ ```yaml
418
+ # /home/user/projects/.kodrdriv/config.yaml (Level 2)
419
+ features: ['auth', 'basic-logging']
420
+ excludePatterns: ['*.tmp', '*.cache']
421
+
422
+ # /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
423
+ features: ['advanced-logging', 'metrics']
424
+ excludePatterns: ['*.log']
425
+
426
+ # /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
427
+ features: ['debug-mode']
428
+ excludePatterns: ['*.debug']
429
+ ```
430
+
431
+ With the configuration above (`features: 'append'`, `excludePatterns: 'prepend'`):
432
+
433
+ ```yaml
434
+ # Final merged configuration:
435
+ features:
436
+ - auth # From Level 2
437
+ - basic-logging # From Level 2
438
+ - advanced-logging # From Level 1
439
+ - metrics # From Level 1
440
+ - debug-mode # From Level 0 (appended)
441
+ excludePatterns:
442
+ - "*.debug" # From Level 0 (prepended first)
443
+ - "*.log" # From Level 1 (prepended second)
444
+ - "*.tmp" # From Level 2
445
+ - "*.cache" # From Level 2
446
+ ```
447
+
448
+ **Nested Field Paths:**
449
+
450
+ You can configure overlap behavior for nested array fields using dot notation:
451
+
452
+ ```typescript
453
+ fieldOverlaps: {
454
+ 'api.endpoints': 'append',
455
+ 'database.migrations': 'prepend',
456
+ 'config.features.experimental': 'override'
457
+ }
323
458
  ```
324
459
 
325
460
  #### Enabling Hierarchical Discovery
@@ -187,7 +187,8 @@ class ArgumentError extends Error {
187
187
  */ const DEFAULT_OPTIONS = {
188
188
  configFile: DEFAULT_CONFIG_FILE,
189
189
  isRequired: false,
190
- encoding: DEFAULT_ENCODING
190
+ encoding: DEFAULT_ENCODING,
191
+ pathResolution: undefined
191
192
  };
192
193
  /**
193
194
  * Default features enabled when creating a Cardigantime instance.
@@ -415,6 +416,62 @@ const create$1 = (params)=>{
415
416
  };
416
417
  };
417
418
 
419
+ /**
420
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
421
+ */ function resolveConfigPaths$1(config, configDir, pathFields = [], resolvePathArray = []) {
422
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
423
+ return config;
424
+ }
425
+ const resolvedConfig = {
426
+ ...config
427
+ };
428
+ for (const fieldPath of pathFields){
429
+ const value = getNestedValue$1(resolvedConfig, fieldPath);
430
+ if (value !== undefined) {
431
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
432
+ const resolvedValue = resolvePathValue$1(value, configDir, shouldResolveArrayElements);
433
+ setNestedValue$1(resolvedConfig, fieldPath, resolvedValue);
434
+ }
435
+ }
436
+ return resolvedConfig;
437
+ }
438
+ /**
439
+ * Gets a nested value from an object using dot notation.
440
+ */ function getNestedValue$1(obj, path) {
441
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
442
+ }
443
+ /**
444
+ * Sets a nested value in an object using dot notation.
445
+ */ function setNestedValue$1(obj, path, value) {
446
+ const keys = path.split('.');
447
+ const lastKey = keys.pop();
448
+ const target = keys.reduce((current, key)=>{
449
+ if (!(key in current)) {
450
+ current[key] = {};
451
+ }
452
+ return current[key];
453
+ }, obj);
454
+ target[lastKey] = value;
455
+ }
456
+ /**
457
+ * Resolves a path value (string or array of strings) relative to the config directory.
458
+ */ function resolvePathValue$1(value, configDir, resolveArrayElements) {
459
+ if (typeof value === 'string') {
460
+ return resolveSinglePath$1(value, configDir);
461
+ }
462
+ if (Array.isArray(value) && resolveArrayElements) {
463
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath$1(item, configDir) : item);
464
+ }
465
+ return value;
466
+ }
467
+ /**
468
+ * Resolves a single path string relative to the config directory if it's a relative path.
469
+ */ function resolveSinglePath$1(pathStr, configDir) {
470
+ if (!pathStr || path.isAbsolute(pathStr)) {
471
+ return pathStr;
472
+ }
473
+ return path.resolve(configDir, pathStr);
474
+ }
418
475
  /**
419
476
  * Discovers configuration directories by traversing up the directory tree.
420
477
  *
@@ -493,8 +550,10 @@ const create$1 = (params)=>{
493
550
  * @param configFileName Name of the configuration file
494
551
  * @param encoding File encoding
495
552
  * @param logger Optional logger
553
+ * @param pathFields Optional array of field names that contain paths to be resolved
554
+ * @param resolvePathArray Optional array of field names whose array elements should all be resolved as paths
496
555
  * @returns Promise resolving to parsed configuration object or null if not found
497
- */ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger) {
556
+ */ async function loadConfigFromDirectory(configDir, configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray) {
498
557
  const storage = create$1({
499
558
  log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
500
559
  });
@@ -514,8 +573,13 @@ const create$1 = (params)=>{
514
573
  const yamlContent = await storage.readFile(configFilePath, encoding);
515
574
  const parsedYaml = yaml__namespace.load(yamlContent);
516
575
  if (parsedYaml !== null && typeof parsedYaml === 'object') {
576
+ let config = parsedYaml;
577
+ // Apply path resolution if configured
578
+ if (pathFields && pathFields.length > 0) {
579
+ config = resolveConfigPaths$1(config, configDir, pathFields, resolvePathArray || []);
580
+ }
517
581
  logger === null || logger === void 0 ? void 0 : logger.verbose(`Successfully loaded config from: ${configFilePath}`);
518
- return parsedYaml;
582
+ return config;
519
583
  } else {
520
584
  logger === null || logger === void 0 ? void 0 : logger.debug(`Config file contains invalid format: ${configFilePath}`);
521
585
  return null;
@@ -526,24 +590,26 @@ const create$1 = (params)=>{
526
590
  }
527
591
  }
528
592
  /**
529
- * Deep merges multiple configuration objects with proper precedence.
593
+ * Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
530
594
  *
531
595
  * Objects are merged from lowest precedence to highest precedence,
532
596
  * meaning that properties in later objects override properties in earlier objects.
533
- * Arrays are replaced entirely (not merged).
597
+ * Arrays can be merged using different strategies based on the fieldOverlaps configuration.
534
598
  *
535
599
  * @param configs Array of configuration objects, ordered from lowest to highest precedence
600
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
536
601
  * @returns Merged configuration object
537
602
  *
538
603
  * @example
539
604
  * ```typescript
540
605
  * const merged = deepMergeConfigs([
541
- * { api: { timeout: 5000 }, debug: true }, // Lower precedence
542
- * { api: { retries: 3 }, features: ['auth'] }, // Higher precedence
543
- * ]);
544
- * // Result: { api: { timeout: 5000, retries: 3 }, debug: true, features: ['auth'] }
606
+ * { api: { timeout: 5000 }, features: ['auth'] }, // Lower precedence
607
+ * { api: { retries: 3 }, features: ['analytics'] }, // Higher precedence
608
+ * ], {
609
+ * 'features': 'append' // Results in features: ['auth', 'analytics']
610
+ * });
545
611
  * ```
546
- */ function deepMergeConfigs(configs) {
612
+ */ function deepMergeConfigs(configs, fieldOverlaps) {
547
613
  if (configs.length === 0) {
548
614
  return {};
549
615
  }
@@ -553,16 +619,18 @@ const create$1 = (params)=>{
553
619
  };
554
620
  }
555
621
  return configs.reduce((merged, current)=>{
556
- return deepMergeTwo(merged, current);
622
+ return deepMergeTwo(merged, current, fieldOverlaps);
557
623
  }, {});
558
624
  }
559
625
  /**
560
- * Deep merges two objects with proper precedence.
626
+ * Deep merges two objects with proper precedence and configurable array overlap behavior.
561
627
  *
562
628
  * @param target Target object (lower precedence)
563
629
  * @param source Source object (higher precedence)
630
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
631
+ * @param currentPath Current field path for nested merging (used internally)
564
632
  * @returns Merged object
565
- */ function deepMergeTwo(target, source) {
633
+ */ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
566
634
  // Handle null/undefined
567
635
  if (source == null) return target;
568
636
  if (target == null) return source;
@@ -570,11 +638,17 @@ const create$1 = (params)=>{
570
638
  if (typeof source !== 'object' || typeof target !== 'object') {
571
639
  return source; // Source takes precedence
572
640
  }
573
- // Handle arrays - replace entirely, don't merge
641
+ // Handle arrays with configurable overlap behavior
574
642
  if (Array.isArray(source)) {
575
- return [
576
- ...source
577
- ];
643
+ if (Array.isArray(target) && fieldOverlaps) {
644
+ const overlapMode = getOverlapModeForPath(currentPath, fieldOverlaps);
645
+ return mergeArrays(target, source, overlapMode);
646
+ } else {
647
+ // Default behavior: replace entirely
648
+ return [
649
+ ...source
650
+ ];
651
+ }
578
652
  }
579
653
  if (Array.isArray(target)) {
580
654
  return source; // Source object replaces target array
@@ -585,17 +659,72 @@ const create$1 = (params)=>{
585
659
  };
586
660
  for(const key in source){
587
661
  if (Object.prototype.hasOwnProperty.call(source, key)) {
662
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
588
663
  if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
589
664
  // Recursively merge nested objects
590
- result[key] = deepMergeTwo(result[key], source[key]);
665
+ result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
591
666
  } else {
592
- // Replace with source value (higher precedence)
593
- result[key] = source[key];
667
+ // Handle arrays and primitives with overlap consideration
668
+ if (Array.isArray(source[key]) && Array.isArray(result[key]) && fieldOverlaps) {
669
+ const overlapMode = getOverlapModeForPath(fieldPath, fieldOverlaps);
670
+ result[key] = mergeArrays(result[key], source[key], overlapMode);
671
+ } else {
672
+ // Replace with source value (higher precedence)
673
+ result[key] = source[key];
674
+ }
594
675
  }
595
676
  }
596
677
  }
597
678
  return result;
598
679
  }
680
+ /**
681
+ * Determines the overlap mode for a given field path.
682
+ *
683
+ * @param fieldPath The current field path (dot notation)
684
+ * @param fieldOverlaps Configuration mapping field paths to overlap modes
685
+ * @returns The overlap mode to use for this field path
686
+ */ function getOverlapModeForPath(fieldPath, fieldOverlaps) {
687
+ // Check for exact match first
688
+ if (fieldPath in fieldOverlaps) {
689
+ return fieldOverlaps[fieldPath];
690
+ }
691
+ // Check for any parent path matches (for nested configurations)
692
+ const pathParts = fieldPath.split('.');
693
+ for(let i = pathParts.length - 1; i > 0; i--){
694
+ const parentPath = pathParts.slice(0, i).join('.');
695
+ if (parentPath in fieldOverlaps) {
696
+ return fieldOverlaps[parentPath];
697
+ }
698
+ }
699
+ // Default to override if no specific configuration found
700
+ return 'override';
701
+ }
702
+ /**
703
+ * Merges two arrays based on the specified overlap mode.
704
+ *
705
+ * @param targetArray The lower precedence array
706
+ * @param sourceArray The higher precedence array
707
+ * @param mode The overlap mode to use
708
+ * @returns The merged array
709
+ */ function mergeArrays(targetArray, sourceArray, mode) {
710
+ switch(mode){
711
+ case 'append':
712
+ return [
713
+ ...targetArray,
714
+ ...sourceArray
715
+ ];
716
+ case 'prepend':
717
+ return [
718
+ ...sourceArray,
719
+ ...targetArray
720
+ ];
721
+ case 'override':
722
+ default:
723
+ return [
724
+ ...sourceArray
725
+ ];
726
+ }
727
+ }
599
728
  /**
600
729
  * Loads configurations from multiple directories and merges them with proper precedence.
601
730
  *
@@ -614,15 +743,19 @@ const create$1 = (params)=>{
614
743
  * configDirName: '.kodrdriv',
615
744
  * configFileName: 'config.yaml',
616
745
  * startingDir: '/project/subdir',
617
- * maxLevels: 5
746
+ * maxLevels: 5,
747
+ * fieldOverlaps: {
748
+ * 'features': 'append',
749
+ * 'excludePatterns': 'prepend'
750
+ * }
618
751
  * });
619
752
  *
620
- * // result.config contains merged configuration
753
+ * // result.config contains merged configuration with custom array merging
621
754
  * // result.discoveredDirs shows where configs were found
622
755
  * // result.errors contains any non-fatal errors
623
756
  * ```
624
757
  */ async function loadHierarchicalConfig(options) {
625
- const { configFileName, encoding = 'utf8', logger } = options;
758
+ const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
626
759
  logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
627
760
  // Discover all configuration directories
628
761
  const discoveredDirs = await discoverConfigDirectories(options);
@@ -643,7 +776,7 @@ const create$1 = (params)=>{
643
776
  ].sort((a, b)=>b.level - a.level);
644
777
  for (const dir of sortedDirs){
645
778
  try {
646
- const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger);
779
+ const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger, pathFields, resolvePathArray);
647
780
  if (config !== null) {
648
781
  configs.push(config);
649
782
  logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
@@ -656,8 +789,8 @@ const create$1 = (params)=>{
656
789
  logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
657
790
  }
658
791
  }
659
- // Merge all configurations with proper precedence
660
- const mergedConfig = deepMergeConfigs(configs);
792
+ // Merge all configurations with proper precedence and configurable array overlap
793
+ const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
661
794
  logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
662
795
  return {
663
796
  config: mergedConfig,
@@ -675,6 +808,68 @@ const create$1 = (params)=>{
675
808
  */ function clean(obj) {
676
809
  return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
677
810
  }
811
+ /**
812
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
813
+ *
814
+ * @param config - The configuration object to process
815
+ * @param configDir - The directory containing the configuration file
816
+ * @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
817
+ * @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
818
+ * @returns The configuration object with resolved paths
819
+ */ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
820
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
821
+ return config;
822
+ }
823
+ const resolvedConfig = {
824
+ ...config
825
+ };
826
+ for (const fieldPath of pathFields){
827
+ const value = getNestedValue(resolvedConfig, fieldPath);
828
+ if (value !== undefined) {
829
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
830
+ const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
831
+ setNestedValue(resolvedConfig, fieldPath, resolvedValue);
832
+ }
833
+ }
834
+ return resolvedConfig;
835
+ }
836
+ /**
837
+ * Gets a nested value from an object using dot notation.
838
+ */ function getNestedValue(obj, path) {
839
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
840
+ }
841
+ /**
842
+ * Sets a nested value in an object using dot notation.
843
+ */ function setNestedValue(obj, path, value) {
844
+ const keys = path.split('.');
845
+ const lastKey = keys.pop();
846
+ const target = keys.reduce((current, key)=>{
847
+ if (!(key in current)) {
848
+ current[key] = {};
849
+ }
850
+ return current[key];
851
+ }, obj);
852
+ target[lastKey] = value;
853
+ }
854
+ /**
855
+ * Resolves a path value (string or array of strings) relative to the config directory.
856
+ */ function resolvePathValue(value, configDir, resolveArrayElements) {
857
+ if (typeof value === 'string') {
858
+ return resolveSinglePath(value, configDir);
859
+ }
860
+ if (Array.isArray(value) && resolveArrayElements) {
861
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
862
+ }
863
+ return value;
864
+ }
865
+ /**
866
+ * Resolves a single path string relative to the config directory if it's a relative path.
867
+ */ function resolveSinglePath(pathStr, configDir) {
868
+ if (!pathStr || path__namespace.isAbsolute(pathStr)) {
869
+ return pathStr;
870
+ }
871
+ return path__namespace.resolve(configDir, pathStr);
872
+ }
678
873
  /**
679
874
  * Validates and secures a user-provided path to prevent path traversal attacks.
680
875
  *
@@ -758,7 +953,7 @@ const create$1 = (params)=>{
758
953
  * // config is fully typed based on your schema
759
954
  * ```
760
955
  */ const read = async (args, options)=>{
761
- var _options_defaults;
956
+ var _options_defaults, _options_defaults_pathResolution;
762
957
  const logger = options.logger;
763
958
  const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
764
959
  if (!rawConfigDir) {
@@ -771,6 +966,7 @@ const create$1 = (params)=>{
771
966
  if (options.features.includes('hierarchical')) {
772
967
  logger.verbose('Hierarchical configuration discovery enabled');
773
968
  try {
969
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
774
970
  // Extract the config directory name from the path for hierarchical discovery
775
971
  const configDirName = path__namespace.basename(resolvedConfigDir);
776
972
  const startingDir = path__namespace.dirname(resolvedConfigDir);
@@ -780,7 +976,10 @@ const create$1 = (params)=>{
780
976
  configFileName: options.defaults.configFile,
781
977
  startingDir,
782
978
  encoding: options.defaults.encoding,
783
- logger
979
+ logger,
980
+ pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
981
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
982
+ fieldOverlaps: options.defaults.fieldOverlaps
784
983
  });
785
984
  rawFileConfig = hierarchicalResult.config;
786
985
  if (hierarchicalResult.discoveredDirs.length > 0) {
@@ -805,8 +1004,13 @@ const create$1 = (params)=>{
805
1004
  logger.verbose('Using single directory configuration loading');
806
1005
  rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
807
1006
  }
1007
+ // Apply path resolution if configured
1008
+ let processedConfig = rawFileConfig;
1009
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
1010
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
1011
+ }
808
1012
  const config = clean({
809
- ...rawFileConfig,
1013
+ ...processedConfig,
810
1014
  ...{
811
1015
  configDirectory: resolvedConfigDir
812
1016
  }
@@ -1175,6 +1379,7 @@ class ConfigurationError extends Error {
1175
1379
  * @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
1176
1380
  * @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
1177
1381
  * @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
1382
+ * @param pOptions.defaults.pathResolution - Configuration for resolving relative paths in config values relative to the config file's directory (optional)
1178
1383
  * @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
1179
1384
  * @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
1180
1385
  * @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
@@ -1189,15 +1394,31 @@ class ConfigurationError extends Error {
1189
1394
  * apiKey: z.string().min(1),
1190
1395
  * timeout: z.number().default(5000),
1191
1396
  * debug: z.boolean().default(false),
1397
+ * contextDirectories: z.array(z.string()).optional(),
1192
1398
  * });
1193
1399
  *
1194
1400
  * const cardigantime = create({
1195
1401
  * defaults: {
1196
1402
  * configDirectory: './config',
1197
1403
  * configFile: 'myapp.yaml',
1404
+ * // Resolve relative paths in contextDirectories relative to config file location
1405
+ * pathResolution: {
1406
+ * pathFields: ['contextDirectories'],
1407
+ * resolvePathArray: ['contextDirectories']
1408
+ * },
1409
+ * // Configure how array fields are merged in hierarchical mode
1410
+ * fieldOverlaps: {
1411
+ * 'features': 'append', // Accumulate features from all levels
1412
+ * 'excludePatterns': 'prepend' // Higher precedence patterns come first
1413
+ * }
1198
1414
  * },
1199
1415
  * configShape: MyConfigSchema.shape,
1416
+ * features: ['config', 'hierarchical'], // Enable hierarchical discovery
1200
1417
  * });
1418
+ *
1419
+ * // If config file is at ../config/myapp.yaml and contains:
1420
+ * // contextDirectories: ['./context', './data']
1421
+ * // These paths will be resolved relative to ../config/ directory
1201
1422
  * ```
1202
1423
  */ const create = (pOptions)=>{
1203
1424
  const defaults = {