@theunwalked/cardigantime 0.0.6 → 0.0.8

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.
@@ -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
  *
@@ -483,7 +540,7 @@ const create$1 = (params)=>{
483
540
  currentDir = parentDir;
484
541
  level++;
485
542
  }
486
- logger === null || logger === void 0 ? void 0 : logger.debug(`Discovery complete. Found ${discoveredDirs.length} config directories`);
543
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Discovery complete. Found ${discoveredDirs.length} config directories`);
487
544
  return discoveredDirs;
488
545
  }
489
546
  /**
@@ -493,14 +550,16 @@ 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
  });
501
560
  const configFilePath = path.join(configDir, configFileName);
502
561
  try {
503
- logger === null || logger === void 0 ? void 0 : logger.debug(`Attempting to load config file: ${configFilePath}`);
562
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Attempting to load config file: ${configFilePath}`);
504
563
  const exists = await storage.exists(configFilePath);
505
564
  if (!exists) {
506
565
  logger === null || logger === void 0 ? void 0 : logger.debug(`Config file does not exist: ${configFilePath}`);
@@ -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') {
517
- logger === null || logger === void 0 ? void 0 : logger.debug(`Successfully loaded config from: ${configFilePath}`);
518
- return parsedYaml;
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
+ }
581
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Successfully loaded config from: ${configFilePath}`);
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;
@@ -622,12 +686,12 @@ const create$1 = (params)=>{
622
686
  * // result.errors contains any non-fatal errors
623
687
  * ```
624
688
  */ async function loadHierarchicalConfig(options) {
625
- const { configFileName, encoding = 'utf8', logger } = options;
626
- logger === null || logger === void 0 ? void 0 : logger.debug('Starting hierarchical configuration loading');
689
+ const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray } = options;
690
+ logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
627
691
  // Discover all configuration directories
628
692
  const discoveredDirs = await discoverConfigDirectories(options);
629
693
  if (discoveredDirs.length === 0) {
630
- logger === null || logger === void 0 ? void 0 : logger.debug('No configuration directories found');
694
+ logger === null || logger === void 0 ? void 0 : logger.verbose('No configuration directories found');
631
695
  return {
632
696
  config: {},
633
697
  discoveredDirs: [],
@@ -643,7 +707,7 @@ const create$1 = (params)=>{
643
707
  ].sort((a, b)=>b.level - a.level);
644
708
  for (const dir of sortedDirs){
645
709
  try {
646
- const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger);
710
+ const config = await loadConfigFromDirectory(dir.path, configFileName, encoding, logger, pathFields, resolvePathArray);
647
711
  if (config !== null) {
648
712
  configs.push(config);
649
713
  logger === null || logger === void 0 ? void 0 : logger.debug(`Loaded config from level ${dir.level}: ${dir.path}`);
@@ -658,7 +722,7 @@ const create$1 = (params)=>{
658
722
  }
659
723
  // Merge all configurations with proper precedence
660
724
  const mergedConfig = deepMergeConfigs(configs);
661
- logger === null || logger === void 0 ? void 0 : logger.debug(`Hierarchical loading complete. Merged ${configs.length} configurations`);
725
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
662
726
  return {
663
727
  config: mergedConfig,
664
728
  discoveredDirs,
@@ -675,6 +739,68 @@ const create$1 = (params)=>{
675
739
  */ function clean(obj) {
676
740
  return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
677
741
  }
742
+ /**
743
+ * Resolves relative paths in configuration values relative to the configuration file's directory.
744
+ *
745
+ * @param config - The configuration object to process
746
+ * @param configDir - The directory containing the configuration file
747
+ * @param pathFields - Array of field names (using dot notation) that contain paths to be resolved
748
+ * @param resolvePathArray - Array of field names whose array elements should all be resolved as paths
749
+ * @returns The configuration object with resolved paths
750
+ */ function resolveConfigPaths(config, configDir, pathFields = [], resolvePathArray = []) {
751
+ if (!config || typeof config !== 'object' || pathFields.length === 0) {
752
+ return config;
753
+ }
754
+ const resolvedConfig = {
755
+ ...config
756
+ };
757
+ for (const fieldPath of pathFields){
758
+ const value = getNestedValue(resolvedConfig, fieldPath);
759
+ if (value !== undefined) {
760
+ const shouldResolveArrayElements = resolvePathArray.includes(fieldPath);
761
+ const resolvedValue = resolvePathValue(value, configDir, shouldResolveArrayElements);
762
+ setNestedValue(resolvedConfig, fieldPath, resolvedValue);
763
+ }
764
+ }
765
+ return resolvedConfig;
766
+ }
767
+ /**
768
+ * Gets a nested value from an object using dot notation.
769
+ */ function getNestedValue(obj, path) {
770
+ return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
771
+ }
772
+ /**
773
+ * Sets a nested value in an object using dot notation.
774
+ */ function setNestedValue(obj, path, value) {
775
+ const keys = path.split('.');
776
+ const lastKey = keys.pop();
777
+ const target = keys.reduce((current, key)=>{
778
+ if (!(key in current)) {
779
+ current[key] = {};
780
+ }
781
+ return current[key];
782
+ }, obj);
783
+ target[lastKey] = value;
784
+ }
785
+ /**
786
+ * Resolves a path value (string or array of strings) relative to the config directory.
787
+ */ function resolvePathValue(value, configDir, resolveArrayElements) {
788
+ if (typeof value === 'string') {
789
+ return resolveSinglePath(value, configDir);
790
+ }
791
+ if (Array.isArray(value) && resolveArrayElements) {
792
+ return value.map((item)=>typeof item === 'string' ? resolveSinglePath(item, configDir) : item);
793
+ }
794
+ return value;
795
+ }
796
+ /**
797
+ * Resolves a single path string relative to the config directory if it's a relative path.
798
+ */ function resolveSinglePath(pathStr, configDir) {
799
+ if (!pathStr || path__namespace.isAbsolute(pathStr)) {
800
+ return pathStr;
801
+ }
802
+ return path__namespace.resolve(configDir, pathStr);
803
+ }
678
804
  /**
679
805
  * Validates and secures a user-provided path to prevent path traversal attacks.
680
806
  *
@@ -758,19 +884,20 @@ const create$1 = (params)=>{
758
884
  * // config is fully typed based on your schema
759
885
  * ```
760
886
  */ const read = async (args, options)=>{
761
- var _options_defaults;
887
+ var _options_defaults, _options_defaults_pathResolution;
762
888
  const logger = options.logger;
763
889
  const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
764
890
  if (!rawConfigDir) {
765
891
  throw new Error('Configuration directory must be specified');
766
892
  }
767
893
  const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
768
- logger.debug('Resolved config directory');
894
+ logger.verbose('Resolved config directory');
769
895
  let rawFileConfig = {};
770
896
  // Check if hierarchical configuration discovery is enabled
771
897
  if (options.features.includes('hierarchical')) {
772
- logger.debug('Hierarchical configuration discovery enabled');
898
+ logger.verbose('Hierarchical configuration discovery enabled');
773
899
  try {
900
+ var _options_defaults_pathResolution1, _options_defaults_pathResolution2;
774
901
  // Extract the config directory name from the path for hierarchical discovery
775
902
  const configDirName = path__namespace.basename(resolvedConfigDir);
776
903
  const startingDir = path__namespace.dirname(resolvedConfigDir);
@@ -780,16 +907,18 @@ const create$1 = (params)=>{
780
907
  configFileName: options.defaults.configFile,
781
908
  startingDir,
782
909
  encoding: options.defaults.encoding,
783
- logger
910
+ logger,
911
+ 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
784
913
  });
785
914
  rawFileConfig = hierarchicalResult.config;
786
915
  if (hierarchicalResult.discoveredDirs.length > 0) {
787
- logger.debug(`Hierarchical discovery found ${hierarchicalResult.discoveredDirs.length} configuration directories`);
916
+ logger.verbose(`Hierarchical discovery found ${hierarchicalResult.discoveredDirs.length} configuration directories`);
788
917
  hierarchicalResult.discoveredDirs.forEach((dir)=>{
789
918
  logger.debug(` Level ${dir.level}: ${dir.path}`);
790
919
  });
791
920
  } else {
792
- logger.debug('No configuration directories found in hierarchy');
921
+ logger.verbose('No configuration directories found in hierarchy');
793
922
  }
794
923
  if (hierarchicalResult.errors.length > 0) {
795
924
  hierarchicalResult.errors.forEach((error)=>logger.warn(`Hierarchical config warning: ${error}`));
@@ -797,16 +926,21 @@ const create$1 = (params)=>{
797
926
  } catch (error) {
798
927
  logger.error('Hierarchical configuration loading failed: ' + (error.message || 'Unknown error'));
799
928
  // Fall back to single directory mode
800
- logger.debug('Falling back to single directory configuration loading');
929
+ logger.verbose('Falling back to single directory configuration loading');
801
930
  rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
802
931
  }
803
932
  } else {
804
933
  // Use traditional single directory configuration loading
805
- logger.debug('Using single directory configuration loading');
934
+ logger.verbose('Using single directory configuration loading');
806
935
  rawFileConfig = await loadSingleDirectoryConfig(resolvedConfigDir, options, logger);
807
936
  }
937
+ // Apply path resolution if configured
938
+ let processedConfig = rawFileConfig;
939
+ if ((_options_defaults_pathResolution = options.defaults.pathResolution) === null || _options_defaults_pathResolution === void 0 ? void 0 : _options_defaults_pathResolution.pathFields) {
940
+ processedConfig = resolveConfigPaths(rawFileConfig, resolvedConfigDir, options.defaults.pathResolution.pathFields, options.defaults.pathResolution.resolvePathArray || []);
941
+ }
808
942
  const config = clean({
809
- ...rawFileConfig,
943
+ ...processedConfig,
810
944
  ...{
811
945
  configDirectory: resolvedConfigDir
812
946
  }
@@ -825,7 +959,7 @@ const create$1 = (params)=>{
825
959
  log: logger.debug
826
960
  });
827
961
  const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);
828
- logger.debug('Attempting to load config file for cardigantime');
962
+ logger.verbose('Attempting to load config file for cardigantime');
829
963
  let rawFileConfig = {};
830
964
  try {
831
965
  const yamlContent = await storage.readFile(configFile, options.defaults.encoding);
@@ -833,13 +967,13 @@ const create$1 = (params)=>{
833
967
  const parsedYaml = yaml__namespace.load(yamlContent);
834
968
  if (parsedYaml !== null && typeof parsedYaml === 'object') {
835
969
  rawFileConfig = parsedYaml;
836
- logger.debug('Loaded configuration file successfully');
970
+ logger.verbose('Loaded configuration file successfully');
837
971
  } else if (parsedYaml !== null) {
838
972
  logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
839
973
  }
840
974
  } catch (error) {
841
975
  if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
842
- logger.debug('Configuration file not found. Using empty configuration.');
976
+ logger.verbose('Configuration file not found. Using empty configuration.');
843
977
  } else {
844
978
  // SECURITY FIX: Don't expose internal paths or detailed error information
845
979
  logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
@@ -1152,7 +1286,8 @@ class ConfigurationError extends Error {
1152
1286
  checkForExtraKeys(config, fullSchema, logger);
1153
1287
  if (!validationResult.success) {
1154
1288
  const formattedError = JSON.stringify(validationResult.error.format(), null, 2);
1155
- logger.error('Configuration validation failed: %s', formattedError);
1289
+ logger.error('Configuration validation failed. Check logs for details.');
1290
+ logger.silly('Configuration validation failed: %s', formattedError);
1156
1291
  throw ConfigurationError.validation('Configuration validation failed. Check logs for details.', validationResult.error);
1157
1292
  }
1158
1293
  return;
@@ -1174,6 +1309,7 @@ class ConfigurationError extends Error {
1174
1309
  * @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
1175
1310
  * @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
1176
1311
  * @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
1312
+ * @param pOptions.defaults.pathResolution - Configuration for resolving relative paths in config values relative to the config file's directory (optional)
1177
1313
  * @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
1178
1314
  * @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
1179
1315
  * @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
@@ -1188,15 +1324,25 @@ class ConfigurationError extends Error {
1188
1324
  * apiKey: z.string().min(1),
1189
1325
  * timeout: z.number().default(5000),
1190
1326
  * debug: z.boolean().default(false),
1327
+ * contextDirectories: z.array(z.string()).optional(),
1191
1328
  * });
1192
1329
  *
1193
1330
  * const cardigantime = create({
1194
1331
  * defaults: {
1195
1332
  * configDirectory: './config',
1196
1333
  * configFile: 'myapp.yaml',
1334
+ * // Resolve relative paths in contextDirectories relative to config file location
1335
+ * pathResolution: {
1336
+ * pathFields: ['contextDirectories'],
1337
+ * resolvePathArray: ['contextDirectories']
1338
+ * }
1197
1339
  * },
1198
1340
  * configShape: MyConfigSchema.shape,
1199
1341
  * });
1342
+ *
1343
+ * // If config file is at ../config/myapp.yaml and contains:
1344
+ * // contextDirectories: ['./context', './data']
1345
+ * // These paths will be resolved relative to ../config/ directory
1200
1346
  * ```
1201
1347
  */ const create = (pOptions)=>{
1202
1348
  const defaults = {