configorama 0.6.8 → 0.6.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/src/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const os = require('os')
2
2
  const path = require('path')
3
3
  const fs = require('fs')
4
+ const enrichMetadata = require('./utils/enrichMetadata')
4
5
  /* // disable logs to find broken tests
5
6
  console.log = () => {}
6
7
  // process.exit(1)
@@ -14,6 +15,7 @@ const traverse = require('traverse')
14
15
  const dotProp = require('dot-prop')
15
16
  const chalk = require('./utils/chalk')
16
17
  const { resolveAlias } = require('./utils/resolveAlias')
18
+ const { resolveFilePathFromMatch } = require('./utils/getFullFilePath')
17
19
 
18
20
  /* Default Value resolvers */
19
21
  const getValueFromString = require('./resolvers/valueFromString')
@@ -27,6 +29,7 @@ const createGitResolver = require('./resolvers/valueFromGit')
27
29
  const YAML = require('./parsers/yaml')
28
30
  const TOML = require('./parsers/toml')
29
31
  const INI = require('./parsers/ini')
32
+ const JSON5 = require('./parsers/json5')
30
33
  /* functions */
31
34
  const md5Function = require('./functions/md5')
32
35
 
@@ -50,13 +53,16 @@ const { splitCsv } = require('./utils/splitCsv')
50
53
  const { replaceAll } = require('./utils/replaceAll')
51
54
  const { getTextAfterOccurrence, findNestedVariable } = require('./utils/textUtils')
52
55
  const { getFallbackString, verifyVariable } = require('./utils/variableUtils')
53
- const { encodeUnknown, decodeUnknown } = require('./utils/unknownValues')
56
+ const { encodeUnknown, decodeUnknown } = require('./utils/encoders/unknown-values')
57
+ const { decodeEncodedValue } = require('./utils/encoders')
58
+ const { encodeJsSyntax, decodeJsSyntax, hasParenthesesPlaceholder } = require('./utils/encoders/js-fixes')
54
59
  const { mergeByKeys } = require('./utils/mergeByKeys')
55
60
  const { arrayToJsonPath } = require('./utils/arrayToJsonPath')
56
61
  const { findNestedVariables } = require('./utils/find-nested-variables')
57
62
  const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
58
63
  const { logHeader } = require('./utils/logs')
59
64
  const { createEditorLink } = require('./utils/createEditorLink')
65
+ const { runConfigWizard } = require('./utils/configWizard')
60
66
  /**
61
67
  * Maintainer's notes:
62
68
  *
@@ -90,6 +96,7 @@ const logLines = '────────────────────
90
96
 
91
97
  let DEBUG = process.argv.includes('--debug') ? true : false
92
98
  let VERBOSE = process.argv.includes('--verbose') ? true : false
99
+ let SETUP_MODE = process.argv.includes('--setup') ? true : false
93
100
  // DEBUG = true
94
101
  let DEBUG_TYPE = false
95
102
  const ENABLE_FUNCTIONS = true
@@ -123,6 +130,7 @@ function preProcess(configObject, variableSyntax) {
123
130
  if (typeof str !== 'string') return str
124
131
 
125
132
  let result = str
133
+ // result = result.replace(/\$\{self:/g, '${')
126
134
  let changed = true
127
135
 
128
136
  // Keep iterating until no more changes (to handle nested variables)
@@ -248,11 +256,18 @@ class Configorama {
248
256
  allowUnknownVars: false,
249
257
  // Allow undefined to be an end result.
250
258
  allowUndefinedValues: false,
259
+ // Allow unknown file refs to pass through without throwing errors
260
+ allowUnknownFileRefs: false,
261
+ // Return metadata
262
+ returnMetadata: false,
263
+ // Return preResolvedVariableDetails
264
+ returnPreResolvedVariableDetails: false,
251
265
  }, options)
252
266
 
253
267
  this.filterCache = {}
254
268
 
255
269
  this.foundVariables = []
270
+ this.fileRefsFound = []
256
271
 
257
272
  // Track variable resolutions for metadata (keyed by path)
258
273
  this.resolutionTracking = {}
@@ -343,6 +358,8 @@ class Configorama {
343
358
  {
344
359
  type: 'self',
345
360
  prefix: 'self',
361
+ syntax: '${self:pathToKeyInConfig}',
362
+ description: `Resolves values from the current config object. Supports sub-properties via :key lookup.`,
346
363
  match: selfRefSyntax,
347
364
  resolver: (varString, o, x, pathValue) => {
348
365
  return this.getValueFromSelf(varString, o, x, pathValue)
@@ -357,6 +374,8 @@ class Configorama {
357
374
  {
358
375
  type: 'file',
359
376
  prefix: 'file',
377
+ syntax: '${file(pathToFile.json)}',
378
+ description: `Resolves values from files. Supports sub-properties via :key lookup.`,
360
379
  match: fileRefSyntax,
361
380
  resolver: (varString, o, x, pathValue) => {
362
381
  return this.getValueFromFile(varString, { context: pathValue })
@@ -387,6 +406,7 @@ class Configorama {
387
406
  /* Resolve deep references */
388
407
  {
389
408
  type: 'deep',
409
+ internal: true,
390
410
  match: deepRefSyntax,
391
411
  resolver: (varString, o, x, pathValue) => {
392
412
  // console.log('>>>>>getValueFromDeep', varString)
@@ -399,23 +419,23 @@ class Configorama {
399
419
 
400
420
  /* Nicer self: references. Match key in object */
401
421
  const fallThroughSelfMatcher = {
402
- type: 'fallthrough',
422
+ type: 'dot.prop',
403
423
  match: (varString, fullObject, valueObject) => {
404
424
  /*
405
- console.log('fallthrough varString', varString)
406
- console.log('fallthrough valueObject', valueObject)
425
+ console.log('fallThroughSelfMatcher varString', varString)
426
+ console.log('fallThroughSelfMatcher valueObject', valueObject)
407
427
  console.log('fullObject', fullObject)
408
428
  /** */
409
429
  /* its file ref so we need to shift lookup for self in nested files */
410
430
  if (valueObject.isFileRef) {
411
431
  const exists = dotProp.get(fullObject, varString)
412
- // console.log('fallthrough exists', exists)
432
+ // console.log('fallThroughSelfMatcher exists', exists)
413
433
  if (!exists) {
414
434
  // @ Todo make recursive
415
435
  const deepProperties = [valueObject.path[0]].concat(varString)
416
436
  const dotPropPath = deepProperties.join('.')
417
437
  const deeperExists = dotProp.get(fullObject, dotPropPath)
418
- // console.log('fallthrough deeper', deeperExists)
438
+ // console.log('fallThroughSelfMatcher deeper', deeperExists)
419
439
  return deeperExists
420
440
  }
421
441
  }
@@ -426,10 +446,10 @@ class Configorama {
426
446
  },
427
447
  resolver: (varString, options, config, pathValue) => {
428
448
  /*
429
- console.log('fallthrough resolver', varString)
430
- console.log('fallthrough options', options)
431
- console.log('fallthrough config', config)
432
- console.log('fallthrough pathValue', pathValue)
449
+ console.log('fallThroughSelfMatcher resolver', varString)
450
+ console.log('fallThroughSelfMatcher options', options)
451
+ console.log('fallThroughSelfMatcher config', config)
452
+ console.log('fallThroughSelfMatcher pathValue', pathValue)
433
453
  /** */
434
454
  return this.getValueFromSelf(varString, options, config, pathValue)
435
455
  },
@@ -437,6 +457,19 @@ class Configorama {
437
457
 
438
458
  /* Apply user defined variable sources */
439
459
  if (options.variableSources) {
460
+
461
+ // ensure each variable source has a type
462
+ options.variableSources.forEach((v) => {
463
+ if (!v.type) {
464
+ console.log('Variable', v)
465
+ throw new Error('Variable source must have a type')
466
+ }
467
+ if (!v.match || !v.resolver) {
468
+ console.log('Variable', v)
469
+ throw new Error('Variable source must have a match and resolver functions')
470
+ }
471
+ })
472
+
440
473
  this.variableTypes = this.variableTypes.concat(options.variableSources)
441
474
  }
442
475
 
@@ -482,7 +515,7 @@ class Configorama {
482
515
  toKebabCase: (val) => {
483
516
  return kebabCase(val)
484
517
  },
485
- /* Type filters */
518
+ /* Type filters for coercion */
486
519
  toNumber: (val, from) => {
487
520
  const newVal = Number(val)
488
521
  return newVal
@@ -497,7 +530,37 @@ class Configorama {
497
530
  return JSON.stringify(val)
498
531
  },
499
532
  toObject: (val) => {
500
- return JSON.parse(val)
533
+ return JSON5.parse(val)
534
+ },
535
+ /* Type validation filters */
536
+ Number: (value) => {
537
+ const n = Number(value)
538
+ if (isNaN(n)) throw new Error(`Configorama Error: Expected Number, got "${value}"`)
539
+ return n
540
+ },
541
+ Boolean: (value) => {
542
+ if (typeof value === 'boolean') return value
543
+ const v = String(value).toLowerCase()
544
+ if (['true', '1', 'yes', 'on'].includes(v)) return true
545
+ if (['false', '0', 'no', 'off'].includes(v)) return false
546
+ throw new Error(`Configorama Error: Expected Boolean, got "${value}"`)
547
+ },
548
+ String: (value) => {
549
+ if (value === undefined || value === null || value === 'null') return ''
550
+ return String(value)
551
+ },
552
+ Json: (value) => {
553
+ try {
554
+ return typeof value === 'string' ? JSON.parse(value) : value
555
+ } catch (e) {
556
+ throw new Error(`Configorama Error: Invalid JSON in variable`)
557
+ }
558
+ },
559
+ /* Help filter - identity function that preserves value but provides metadata for wizard */
560
+ help: (value, helpText) => {
561
+ // Identity function - returns value unchanged
562
+ // The helpText argument is extracted during metadata collection for the wizard
563
+ return value
501
564
  },
502
565
  }
503
566
 
@@ -507,7 +570,9 @@ class Configorama {
507
570
  }
508
571
 
509
572
  // (\|\s*(toUpperCase|toLowerCase|toCamelCase|toKebabCase|capitalize)\s*)+$
510
- this.filterMatch = new RegExp(`(\\|\\s*(${Object.keys(this.filters).join('|')})\\s*)+}?$`)
573
+ // Updated to support function-style filters like help('text') with nested parens
574
+ // Use a more permissive pattern that matches anything between parens including nested parens
575
+ this.filterMatch = new RegExp(`(\\|\\s*(${Object.keys(this.filters).join('|')})(?:\\s*\\([^)]*(?:\\([^)]*\\))?[^)]*\\))?\\s*)+}?$`)
511
576
  // console.log('this.filterMatch', this.filterMatch)
512
577
 
513
578
  this.functions = {
@@ -603,7 +668,7 @@ class Configorama {
603
668
  this.opts
604
669
  )
605
670
  this.configFileContents = ''
606
- if (VERBOSE || showFoundVariables) {
671
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
607
672
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
608
673
  }
609
674
  /*
@@ -630,13 +695,37 @@ class Configorama {
630
695
  const variableSyntax = this.variableSyntax
631
696
  const variablesKnownTypes = this.variablesKnownTypes
632
697
 
633
- if (VERBOSE || showFoundVariables) {
698
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
634
699
  // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
635
700
  const metadata = this.collectVariableMetadata()
636
- deepLog('metadata', metadata)
637
- process.exit(1)
701
+
702
+ const enrich = enrichMetadata(
703
+ metadata,
704
+ this.resolutionTracking,
705
+ this.variableSyntax,
706
+ this.fileRefsFound,
707
+ this.originalConfig,
708
+ this.configFilePath,
709
+ Object.keys(this.filters)
710
+ )
711
+
712
+ if (showFoundVariables) {
713
+ /*
714
+ deepLog('metadata', metadata)
715
+ fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
716
+ deepLog('enrich', enrich)
717
+ // process.exit(1)
718
+ /** */
719
+ }
720
+
638
721
  const variableData = metadata.variables
722
+ const uniqueVariables = metadata.uniqueVariables
639
723
  const varKeys = Object.keys(variableData)
724
+ const uniqueVarKeys = Object.keys(uniqueVariables)
725
+
726
+ // if (this.opts.returnPreResolvedVariableDetails) {
727
+ // return metadata
728
+ // }
640
729
 
641
730
  if (!varKeys.length) {
642
731
  logHeader('No Variables Found in Config')
@@ -648,9 +737,9 @@ class Configorama {
648
737
 
649
738
  const varTypes = Object.keys(this.variableTypes)
650
739
  if (varTypes.length) {
651
- const exclude = ['fallthrough', 'deep']
740
+ const exclude = ['dot.prop', 'deep']
652
741
  console.log('\nAllowed variable types:')
653
- varTypes.filter((v) => v.type !== 'fallthrough').forEach((v) => {
742
+ varTypes.filter((v) => v.type !== 'dot.prop').forEach((v) => {
654
743
  const vData = this.variableTypes[v]
655
744
  if (exclude.includes(vData.type)) {
656
745
  return
@@ -673,164 +762,231 @@ class Configorama {
673
762
  const longestKey = varKeys.reduce((acc, k) => {
674
763
  return Math.max(acc, k.length)
675
764
  }, 0)
676
- // Count all references including nested ones within other variables
677
- const countAllReferences = (targetVariable) => {
678
- // Start with direct references
679
- let count = variableData[targetVariable].length
680
-
681
- // Check all other variables for nested references to this variable
682
- varKeys.forEach((otherKey) => {
683
- if (otherKey === targetVariable) return
684
-
685
- variableData[otherKey].forEach((instance) => {
686
- if (instance.resolveDetails) {
687
- instance.resolveDetails.forEach((detail) => {
688
- // Check if this resolveDetail references our target variable
689
- if (detail.fullMatch === targetVariable) {
690
- count++
691
- }
692
- })
693
- }
694
- })
695
- })
696
-
697
- return count
698
- }
699
765
 
700
- console.log(varKeys.map((k) => {
701
- const refCount = countAllReferences(k)
766
+ // Use uniqueVariables for simpler reference counting
767
+ const referenceData = varKeys.map((k) => {
768
+ // Map from varMatch (e.g., '${env:API_KEY}') to variable name (e.g., 'env:API_KEY')
769
+ // Extract the variable name from the key by removing ${ and }
770
+ const varName = k.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
771
+ const uniqueVar = uniqueVariables[varName]
772
+ const refCount = uniqueVar ? uniqueVar.occurrences.length : variableData[k].length
702
773
  const placesWord = refCount > 1 ? 'places' : 'place'
703
774
  return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${refCount} ${placesWord}`
704
- }).join('\n'))
705
- console.log()
775
+ }).join('\n')
776
+
777
+ console.log(`${referenceData}\n`)
706
778
  }
707
779
 
708
780
  logHeader('Variable Details')
709
781
 
710
- const lines = this.configFileContents.split('\n')
711
- // console.log('lines', lines)
712
-
782
+ const lines = this.configFileContents ? this.configFileContents.split('\n') : []
783
+
713
784
  const indent = ''
714
785
  const boxes = varKeys.map((key, i) => {
715
786
  const variableInstances = variableData[key]
716
787
  // console.log('variableInstances', variableInstances)
717
-
718
788
  const firstInstance = variableInstances[0]
719
789
 
720
- let requiredText = ''
721
- let defaultValueSrc = ''
722
- if (typeof firstInstance.defaultValue === 'undefined') {
723
- // console.log('no default value', firstInstance)
724
-
725
- let dotPropArr = []
726
- if (firstInstance.defaultValueIsVar && (
727
- firstInstance.defaultValueIsVar.varType === 'self:' ||
728
- firstInstance.defaultValueIsVar.varType === 'dot.prop'
729
- )) {
730
- dotPropArr = [firstInstance.defaultValueIsVar]
731
- }
732
- /* Check if the fallback variable is a self reference */
733
- const hasDotPropOrSelf = variableInstances.reduce((acc, v) => {
734
- const dotProp = v.resolveDetails.find((d) => {
735
- // console.log('d', d)
736
- return d.varType === 'dot.prop'
737
- })
738
- if (dotProp) {
739
- acc.push(dotProp)
740
- }
741
- if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
742
- // console.log('dot.prop', v.resolveDetails)
743
- acc.push(v.resolveDetails[0])
744
- }
745
- return acc
746
- }, dotPropArr)
747
- // console.log('hasDotPropOrSelf', hasDotPropOrSelf)
748
-
749
- if (!hasDotPropOrSelf.length) {
750
- const debug = (false) ? JSON.stringify(firstInstance, null, 2) : ''
751
- requiredText = `[Required Variable] ${debug}`
752
- } else {
753
- const fallBackValues = variableInstances.filter((v) => v.resolveDetails.find((d) => d.hasFallback)).map((v) => v.resolveDetails)
754
- // console.log('fallBackValues', fallBackValues)
755
- if (fallBackValues.length) {
756
- // console.log('fallBackValues.resolveDetails', fallBackValues)
757
- }
790
+ // Get uniqueVariable data for description and other metadata
791
+ const varName = key.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
792
+ const uniqueVar = uniqueVariables[varName]
758
793
 
759
- const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
760
- defaultValueSrc = cleanPath
761
- // Find the dot prop value in the original config
762
- const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
763
- // console.log('dotPropValue', dotPropValue)
764
- if (typeof dotPropValue !== 'undefined') {
765
- requiredText = ''
766
- const niceString = typeof dotPropValue === 'object' ? JSON.stringify(dotPropValue) : dotPropValue
767
- // truncate niceString to 100 characters
768
- const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
769
- firstInstance.defaultValue = truncatedString
770
- } else {
771
- deepLog('Missing default var', firstInstance)
772
- throw new Error(`Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`)
773
- }
774
- }
775
- //this.originalConfig[key] = undefined
776
- }
794
+ // Build display message from enriched metadata
777
795
  const spacing = ' '
778
796
  const titleText = `Variable:${spacing}`
779
- const reqText = (requiredText) ? `${chalk.red.bold(requiredText)}\n` : ''
780
- let varMsg = `${reqText}`
781
- const VALUE_HEX = '#899499' // '#708090'
797
+ const VALUE_HEX = '#899499'
782
798
  const keyChalk = chalk.whiteBright
783
799
  const valueChalk = chalk.hex(VALUE_HEX)
784
800
 
801
+ let varMsg = ''
802
+ let requiredMessage = ''
803
+
804
+ // Show required status from metadata
805
+ if (firstInstance.isRequired) {
806
+ requiredMessage = `${chalk.red.bold('[Required]')}`
807
+ }
808
+
809
+ // Show type filter if present (Boolean, String, Number, etc.)
810
+ if (uniqueVar && uniqueVar.occurrences.length > 0) {
811
+ const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
812
+ const foundTypes = new Set()
813
+
814
+ uniqueVar.occurrences.forEach(occ => {
815
+ if (occ.filters && Array.isArray(occ.filters)) {
816
+ occ.filters.forEach(filter => {
817
+ if (typeFilters.includes(filter)) {
818
+ foundTypes.add(filter)
819
+ }
820
+ })
821
+ }
822
+ })
823
+
824
+ if (foundTypes.size > 0) {
825
+ const typeText = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
826
+ varMsg += `${typeText} ${valueChalk(Array.from(foundTypes).join(', '))}\n`
827
+ }
828
+ }
829
+
830
+ // Show description from uniqueVariables if available
831
+ if (uniqueVar && uniqueVar.occurrences.length > 0) {
832
+ // Collect unique descriptions from all occurrences
833
+ const descriptions = uniqueVar.occurrences
834
+ .map(occ => occ.description)
835
+ .filter((desc, index, self) => desc && self.indexOf(desc) === index)
836
+
837
+ if (descriptions.length > 0) {
838
+ const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
839
+ const combinedDesc = descriptions.join('. ')
840
+ varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
841
+ }
842
+ }
843
+
844
+
845
+
846
+ // Show default value from metadata
785
847
  if (typeof firstInstance.defaultValue !== 'undefined') {
786
- // console.log('firstInstance.defaultValue', firstInstance.defaultValue)
787
848
  const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
788
- const defaultValueText = `${indent}${keyChalk(`Default value:`.padEnd(titleText.length, ' '))}`
789
- // ensure padding is even
790
- varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}`
849
+ const defaultValueText = `${indent}${keyChalk('Default value:'.padEnd(titleText.length, ' '))}`
850
+ varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
791
851
  }
792
852
 
793
- if (defaultValueSrc) {
794
- varMsg += `\n${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
795
- varMsg += `${valueChalk(defaultValueSrc)}`
853
+ // Show default value source path from metadata
854
+ if (firstInstance.defaultValueSrc) {
855
+ varMsg += `${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
856
+ varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
796
857
  }
797
858
 
859
+ // Show resolve order from metadata
798
860
  if (firstInstance.resolveOrder.length > 1) {
799
- varMsg += `\n${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
861
+ varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
800
862
  const resolveOrder = firstInstance.resolveOrder.join(', ')
801
- varMsg += ` ${valueChalk(resolveOrder)}`
863
+ varMsg += ` ${valueChalk(resolveOrder)}\n`
802
864
  }
803
865
 
866
+ // Show path(s) from metadata
804
867
  let locationRender = valueChalk(variableInstances[0].path)
805
-
806
- let locationLabel = `${indent}${keyChalk('Path:'.padEnd(titleText.length, ' '))}`
868
+ let locationLabel = `${indent}${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
869
+ let typeText = ''
807
870
  if (variableInstances.length > 1) {
808
- locationRender = `\n${variableInstances.map((v) => valueChalk(`${indent}- ${v.path}`)).join('\n')}`
809
- const locationLabelText = `${indent}${keyChalk('Paths:')}`
810
- locationLabel = locationLabelText
871
+ const pathIndent = ' '.repeat(titleText.length + 1)
872
+ const pathItems = variableInstances.map((v, idx) => {
873
+ // Show type filter per path if different
874
+ if (uniqueVar && uniqueVar.occurrences.length > 1) {
875
+ const occurrence = uniqueVar.occurrences.find(occ => occ.path === v.path)
876
+ const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
877
+ const pathType = occurrence && occurrence.filters
878
+ ? occurrence.filters.find(f => typeFilters.includes(f))
879
+ : null
880
+
881
+ typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
882
+ const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
883
+ return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
884
+ }
885
+ const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
886
+ return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
887
+ })
888
+ locationRender = pathItems.join('\n')
889
+ locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
890
+ } else {
891
+ // look for type filter in the first instance
892
+ const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
893
+ const pathType = firstInstance.filters
894
+ ? firstInstance.filters.find(f => typeFilters.includes(f))
895
+ : null
896
+
897
+ typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
811
898
  }
899
+ varMsg += `${locationLabel} ${locationRender}`
900
+
901
+ // Find line number in config file based on format (YAML, TOML, JSON, INI)
902
+ const configKey = firstInstance.key
903
+ const line = lines.findIndex((line) => {
904
+ const fileType = this.configFileType
905
+ const escapedKey = configKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
906
+ // YAML: key: or key :
907
+ if (fileType === '.yml' || fileType === '.yaml') {
908
+ return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
909
+ }
910
+ // TOML: key = or key=
911
+ if (fileType === '.toml') {
912
+ return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
913
+ }
914
+ // JSON: "key": or "key" :
915
+ if (fileType === '.json' || fileType === '.json5') {
916
+ return new RegExp(`"${escapedKey}"\\s*:`).test(line)
917
+ }
918
+ // INI: key = or key=
919
+ if (fileType === '.ini') {
920
+ return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
921
+ }
922
+ // JS/TS/ESM: key: or "key": or 'key': or `key`: or [`key`]:
923
+ if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(fileType)) {
924
+ return new RegExp(`(?:${escapedKey}|"${escapedKey}"|'${escapedKey}'|\`${escapedKey}\`|\\[\`${escapedKey}\`\\])\\s*:`).test(line)
925
+ }
926
+ // Default fallback: try YAML-style
927
+ return line.includes(`${configKey}:`)
928
+ })
929
+ const lineNumber = line !== -1 ? line + 1 : 0
812
930
 
813
- varMsg += `\n${locationLabel} ${locationRender}`
814
931
 
815
- // find the match in our lines
816
- const line = lines.findIndex((line) => line.includes(key))
817
- const lineNumber = line + 1
818
-
819
- // console.log(` ${chalk.bold(key)}`)
820
932
  return {
821
- text: varMsg,
933
+ content: {
934
+ left: varMsg,
935
+ backgroundColor: 'red',
936
+ width: '100%',
937
+ },
822
938
  title: {
823
- left: `▶ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
824
- right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `Line: ${lineNumber}`, 'gray') : '',
939
+ left: `▷ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
940
+ right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `${requiredMessage} ${lineNumber ? `Line: ${lineNumber.toString().padEnd(2, ' ')}` : ''}`, 'gray') : '',
941
+ center: typeText,
942
+ paddingBottom: 1,
943
+ paddingTop: (i === 0) ? 1 : 0,
944
+ truncate: true,
825
945
  },
946
+ width: '100%',
826
947
  }
827
948
  })
828
949
 
829
950
  console.log(makeStackedBoxes(boxes, {
951
+ borderText: 'Variable Details. Click on titles to open in editor.',
830
952
  borderColor: 'gray',
831
- minWidth: 120,
953
+ minWidth: '96%',
832
954
  borderStyle: 'bold',
955
+ disableTitleSeparator: true,
833
956
  }))
957
+ // process.exit(1)
958
+ }
959
+
960
+
961
+ // WALK through CLI prompt if --setup flag is set
962
+ if (SETUP_MODE) {
963
+ logHeader('Setup Mode')
964
+ // deepLog('enrich', enrich)
965
+ const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
966
+
967
+ console.log('\n')
968
+ logHeader('User Inputs Summary')
969
+ console.log(JSON.stringify(userInputs, null, 2))
970
+
971
+ // TODO set values
972
+
973
+ // Apply user inputs to options and environment
974
+ if (userInputs.options) {
975
+ Object.assign(this.opts, userInputs.options)
976
+ }
977
+ if (userInputs.env) {
978
+ Object.assign(process.env, userInputs.env)
979
+ }
980
+ // Note: self references are in the config, so no need to apply them
981
+
982
+ console.log()
983
+ logHeader('Resolving Configuration')
984
+ console.log()
985
+
986
+ // process.exit(1)
987
+
988
+ // Continue with normal resolution flow using the new values
989
+ // Don't exit - let it fall through to resolve the config
834
990
  }
835
991
 
836
992
  /* Exit early if list or info flag is set */
@@ -926,9 +1082,8 @@ class Configorama {
926
1082
  }
927
1083
 
928
1084
  /* fix for file(JS-ref.js, raw) to keep parens and inline code */
929
- const OPEN_PAREN_PLACEHOLDER_PATTERN = /__PH_PAREN_OPEN__/g
930
- if (rawValue.match(OPEN_PAREN_PLACEHOLDER_PATTERN)) {
931
- rawValue = rawValue.replace(OPEN_PAREN_PLACEHOLDER_PATTERN, '(')
1085
+ if (hasParenthesesPlaceholder(rawValue)) {
1086
+ rawValue = decodeJsSyntax(rawValue)
932
1087
  this.update(rawValue)
933
1088
  }
934
1089
 
@@ -972,6 +1127,8 @@ class Configorama {
972
1127
  collectVariableMetadata() {
973
1128
  const variableSyntax = this.variableSyntax
974
1129
  const variablesKnownTypes = this.variablesKnownTypes
1130
+ const variableTypes = this.variableTypes
1131
+ const filterMatch = this.filterMatch
975
1132
  const foundVariables = []
976
1133
  const variableData = {}
977
1134
  const fileRefs = []
@@ -986,29 +1143,117 @@ class Configorama {
986
1143
  return
987
1144
  }
988
1145
 
989
- const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
1146
+ const nested = findNestedVariables(
1147
+ rawValue,
1148
+ variableSyntax,
1149
+ variablesKnownTypes,
1150
+ configValuePath,
1151
+ variableTypes
1152
+ )
990
1153
 
991
1154
  const lastItem = nested[nested.length - 1]
992
1155
  const lastKeyPath = this.path[this.path.length - 1]
993
1156
  const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
994
- const key = lastItem.fullMatch
1157
+
1158
+ // Extract filters from varMatch
1159
+ const originalSrc = lastItem.varMatch || ''
1160
+ const hasFilters = filterMatch && originalSrc.match(filterMatch)
1161
+ let foundFilters = []
1162
+ let keyWithoutFilters = originalSrc
1163
+
1164
+ if (hasFilters) {
1165
+ // Extract filter names from the match (e.g., "| String}" -> ["String"])
1166
+ const filterPart = hasFilters[0].replace(/}?$/, '') // Remove trailing }
1167
+ foundFilters = filterPart
1168
+ .split('|')
1169
+ .map((filter) => filter.trim())
1170
+ .filter(Boolean)
1171
+
1172
+ // Remove filters from the key (replace "| String}" with "}")
1173
+ // Also clean up any trailing whitespace before the closing brace
1174
+ keyWithoutFilters = originalSrc.replace(filterMatch, '}').replace(/\s+}$/, '}')
1175
+ }
1176
+
1177
+ const key = keyWithoutFilters
1178
+
1179
+ // Strip filters from resolveDetails
1180
+ const cleanedResolveDetails = nested.map(detail => {
1181
+ const cleaned = { ...detail }
1182
+ if (cleaned.varMatch && filterMatch) {
1183
+ const match = cleaned.varMatch.match(filterMatch)
1184
+ if (match) {
1185
+ cleaned.varMatch = cleaned.varMatch.replace(filterMatch, '').replace(/\s+$/, '') + '}'
1186
+ }
1187
+ }
1188
+ if (cleaned.variable && filterMatch) {
1189
+ const match = cleaned.variable.match(filterMatch)
1190
+ if (match) {
1191
+ cleaned.variable = cleaned.variable.replace(filterMatch, '').replace(/\s+$/, '')
1192
+ }
1193
+ }
1194
+ if (cleaned.varString && filterMatch) {
1195
+ const match = cleaned.varString.match(filterMatch)
1196
+ if (match) {
1197
+ cleaned.varString = cleaned.varString.replace(filterMatch, '').trim()
1198
+ }
1199
+ }
1200
+ // Also clean fallbackValues if present
1201
+ if (cleaned.fallbackValues && Array.isArray(cleaned.fallbackValues)) {
1202
+ cleaned.fallbackValues = cleaned.fallbackValues.map(fb => {
1203
+ const cleanedFb = { ...fb }
1204
+ if (cleanedFb.varMatch && filterMatch) {
1205
+ const match = cleanedFb.varMatch.match(filterMatch)
1206
+ if (match) {
1207
+ cleanedFb.varMatch = cleanedFb.varMatch.replace(filterMatch, '').trim()
1208
+ }
1209
+ }
1210
+ if (cleanedFb.variable && filterMatch) {
1211
+ const match = cleanedFb.variable.match(filterMatch)
1212
+ if (match) {
1213
+ cleanedFb.variable = cleanedFb.variable.replace(filterMatch, '').trim()
1214
+ }
1215
+ }
1216
+ if (cleanedFb.stringValue && filterMatch) {
1217
+ const match = cleanedFb.stringValue.match(filterMatch)
1218
+ if (match) {
1219
+ cleanedFb.stringValue = cleanedFb.stringValue.replace(filterMatch, '').trim()
1220
+ }
1221
+ }
1222
+ return cleanedFb
1223
+ })
1224
+ }
1225
+ return cleaned
1226
+ })
1227
+
995
1228
  const varData = {
996
1229
  path: configValuePath,
997
1230
  key: itemKey,
998
- value: rawValue,
999
- variable: lastItem.fullMatch,
1231
+ originalStringValue: rawValue,
1232
+ variable: keyWithoutFilters,
1233
+ variableWithFilters: originalSrc,
1000
1234
  isRequired: false,
1001
1235
  defaultValue: undefined,
1002
1236
  matchIndex: matchCount++,
1003
1237
  resolveOrder: [],
1004
- resolveDetails: nested,
1238
+ resolveDetails: cleanedResolveDetails,
1239
+ ...(foundFilters.length > 0 && { filters: foundFilters }),
1005
1240
  }
1006
1241
  let defaultValueIsVar = false
1007
1242
 
1008
1243
  function calculateResolveOrder(item) {
1244
+ // Helper to strip filters from variable strings
1245
+ const stripFilters = (str) => {
1246
+ if (!str || !filterMatch) return str
1247
+ const match = str.match(filterMatch)
1248
+ if (match) {
1249
+ return str.replace(filterMatch, '').trim()
1250
+ }
1251
+ return str
1252
+ }
1253
+
1009
1254
  if (item && item.fallbackValues) {
1010
1255
  let hasResolvedFallback
1011
- const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
1256
+ const order = ([stripFilters(item.valueBeforeFallback)]).concat(item.fallbackValues.map((f, i) => {
1012
1257
  if (f.fallbackValues) {
1013
1258
  const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1014
1259
  if (!hasResolvedFallback && nestedResolvedFallback) {
@@ -1018,21 +1263,22 @@ class Configorama {
1018
1263
  }
1019
1264
 
1020
1265
  if (!hasResolvedFallback && f.isResolvedFallback) {
1021
- hasResolvedFallback = f.stringValue
1266
+ hasResolvedFallback = stripFilters(f.stringValue)
1022
1267
  }
1023
1268
  if (f.isResolvedFallback) {
1024
- hasResolvedFallback = f.stringValue
1269
+ hasResolvedFallback = stripFilters(f.stringValue)
1025
1270
  }
1026
1271
 
1027
1272
  if (!hasResolvedFallback && f.isVariable) {
1028
1273
  defaultValueIsVar = f
1029
1274
  }
1030
- return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
1275
+ const valueStr = stripFilters(f.stringValue || f.variable)
1276
+ return `${valueStr}${f.isResolvedFallback ? ' (default)' : ''}`
1031
1277
  })).flat()
1032
1278
 
1033
1279
  return [order, hasResolvedFallback]
1034
1280
  }
1035
- return [[item.variable], undefined]
1281
+ return [[stripFilters(item.variable)], undefined]
1036
1282
  }
1037
1283
 
1038
1284
  const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
@@ -1056,10 +1302,9 @@ class Configorama {
1056
1302
 
1057
1303
  // Extract file references
1058
1304
  nested.forEach((detail) => {
1059
- if (detail.varType &&
1060
- (detail.varType.startsWith('file(') || detail.varType.startsWith('text('))
1061
- ) {
1062
- const fileMatch = detail.varType.match(/^(?:file|text)\((.*?)\)/)
1305
+ // console.log('detail', detail)
1306
+ if (detail.variableType && (detail.variableType === 'file' || detail.variableType === 'text')) {
1307
+ const fileMatch = detail.variable.match(/^(?:file|text)\((.*?)\)/)
1063
1308
  if (fileMatch && fileMatch[1]) {
1064
1309
  let fileContent = fileMatch[1].trim()
1065
1310
 
@@ -1125,19 +1370,19 @@ class Configorama {
1125
1370
  // Check for self-references that resolve to config values
1126
1371
  let dotPropArr = []
1127
1372
  if (firstInstance.defaultValueIsVar && (
1128
- firstInstance.defaultValueIsVar.varType === 'self:' ||
1129
- firstInstance.defaultValueIsVar.varType === 'dot.prop'
1373
+ firstInstance.defaultValueIsVar.variableType === 'self:' ||
1374
+ firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1130
1375
  )) {
1131
1376
  dotPropArr = [firstInstance.defaultValueIsVar]
1132
1377
  }
1133
1378
 
1134
1379
  const hasDotPropOrSelf = instances.reduce((acc, v) => {
1135
- const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
1136
- if (dotProp) {
1137
- acc.push(dotProp)
1138
- }
1139
- if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
1140
- acc.push(v.resolveDetails[0])
1380
+ // Only check the outermost variable (last in resolveDetails)
1381
+ if (v.resolveDetails && v.resolveDetails.length > 0) {
1382
+ const outermostDetail = v.resolveDetails[v.resolveDetails.length - 1]
1383
+ if (outermostDetail.variableType === 'dot.prop' || outermostDetail.variableType === 'self') {
1384
+ acc.push(outermostDetail)
1385
+ }
1141
1386
  }
1142
1387
  return acc
1143
1388
  }, dotPropArr)
@@ -1150,10 +1395,20 @@ class Configorama {
1150
1395
  const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1151
1396
  if (typeof dotPropValue === 'undefined') {
1152
1397
  isTrulyRequired = true
1398
+ } else {
1399
+ // Enrich with default value from self-reference
1400
+ firstInstance.defaultValueSrc = cleanPath
1401
+ const niceString = typeof dotPropValue === 'object' ? JSON.stringify(dotPropValue) : dotPropValue
1402
+ const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
1403
+ firstInstance.defaultValue = truncatedString
1404
+ firstInstance.isRequired = false
1153
1405
  }
1154
1406
  }
1155
1407
  }
1156
1408
 
1409
+ // Update isRequired based on computed isTrulyRequired
1410
+ firstInstance.isRequired = isTrulyRequired
1411
+
1157
1412
  if (isTrulyRequired) {
1158
1413
  requiredCount++
1159
1414
  } else {
@@ -1163,13 +1418,23 @@ class Configorama {
1163
1418
 
1164
1419
  return {
1165
1420
  variables: variableData,
1421
+ uniqueVariables: {},
1422
+ fileDependencies: {
1423
+ globPatterns: fileGlobPatterns,
1424
+ // all: fileRefs,
1425
+ dynamicPaths: fileRefs.filter(ref => ref.indexOf('*') !== -1 || ref.match(variableSyntax)),
1426
+ // resolve files are those that are paths with no * and no inner variables
1427
+ resolvedPaths: fileRefs.filter(ref => ref.indexOf('*') === -1 && !ref.match(variableSyntax)),
1428
+ // Set in enrichMetadata
1429
+ byConfigPath: undefined,
1430
+ // Set in enrichMetadata
1431
+ references: undefined,
1432
+ },
1166
1433
  summary: {
1167
1434
  totalVariables: varKeys.length,
1168
1435
  requiredVariables: requiredCount,
1169
1436
  variablesWithDefaults: withDefaultsCount
1170
1437
  },
1171
- fileRefs: fileRefs,
1172
- fileGlobPatterns: fileGlobPatterns,
1173
1438
  }
1174
1439
  }
1175
1440
  runFunction(variableString) {
@@ -1193,6 +1458,7 @@ class Configorama {
1193
1458
  let argsToPass
1194
1459
  if (rawArgs && rawArgs.match(/^{([^}]+)}$/)) {
1195
1460
  // console.log('OBJECT', hasFunc[2])
1461
+ // TODO use JSON5
1196
1462
  argsToPass = [JSON.parse(rawArgs)]
1197
1463
  } else {
1198
1464
  // TODO fix how commas + spaces are ned
@@ -1459,48 +1725,79 @@ class Configorama {
1459
1725
  if (matches.length === 1) {
1460
1726
  valueObject.currentVarDetails = matches[0]
1461
1727
  valueObject.currentVarDetails.result = results[0]
1728
+ }
1729
+
1730
+ // Initialize resolution history if needed
1731
+ if (!valueObject.resolutionHistory) {
1732
+ valueObject.resolutionHistory = []
1733
+ }
1734
+
1735
+ let result = valueObject.value
1736
+ for (let i = 0; i < matches.length; i += 1) {
1737
+ this.warnIfNotFound(matches[i].variable, results[i])
1462
1738
 
1463
1739
  // Extract metadata from result if present
1464
- let actualResult = results[0]
1740
+ let actualResult = results[i]
1465
1741
  let resolverType = undefined
1466
- if (results[0] && typeof results[0] === 'object') {
1467
- if (results[0].__internal_metadata) {
1468
- actualResult = results[0].value
1469
- resolverType = results[0].__resolverType
1470
- } else if (results[0].__internal_only_flag) {
1471
- // Don't extract value from __internal_only_flag objects, but grab resolverType if present
1472
- actualResult = results[0]
1473
- resolverType = results[0].__resolverType
1742
+ if (results[i] && typeof results[i] === 'object') {
1743
+ if (results[i].__internal_metadata) {
1744
+ actualResult = results[i].value
1745
+ resolverType = results[i].__resolverType
1746
+ } else if (results[i].__internal_only_flag) {
1747
+ actualResult = results[i]
1748
+ resolverType = results[i].__resolverType
1474
1749
  }
1475
1750
  }
1476
- // valueObject.currentVarDetails.varType = results[0].__resolverType
1477
1751
 
1478
- // Track resolution history
1479
- if (!valueObject.resolutionHistory) {
1480
- valueObject.resolutionHistory = []
1481
- }
1482
-
1483
1752
  // Extract clean result to avoid circular references
1484
- // For __internal_only_flag objects (like deep resolver results), extract the value
1485
- // For real data objects (like file contents), keep them as-is
1486
1753
  let cleanResult = actualResult
1487
1754
  if (actualResult && typeof actualResult === 'object' && actualResult.__internal_only_flag) {
1488
1755
  cleanResult = actualResult.value
1489
1756
  }
1490
-
1491
- const historyEntry = {
1492
- match: matches[0].match,
1493
- variable: matches[0].variable,
1494
- result: cleanResult,
1495
- resultType: typeof cleanResult,
1496
- valueBeforeResolution: valueObject.value,
1757
+
1758
+ let valueBeforeResolution = result
1759
+
1760
+ if (typeof valueBeforeResolution === 'object' && valueBeforeResolution.__internal_only_flag) {
1761
+ valueBeforeResolution = valueBeforeResolution.value
1762
+ }
1763
+
1764
+ const finalResult = decodeEncodedValue(cleanResult)
1765
+
1766
+ // Track this resolution step in history
1767
+ const historyEntry = {}
1768
+
1769
+ historyEntry.match = matches[i].match
1770
+ historyEntry.variable = matches[i].variable
1771
+ if (historyEntry.resultType === 'string' && historyEntry.result.match(/^>passthrough\[/)) {
1772
+ historyEntry.variableType = 'encodedUnknown'
1497
1773
  }
1498
1774
  if (resolverType) {
1499
- historyEntry.varType = resolverType
1775
+ historyEntry.variableType = resolverType
1776
+ }
1777
+ historyEntry.result = finalResult
1778
+
1779
+ const isDeepResult = typeof finalResult === 'string' && finalResult.match(/^\$\{deep:\d+\}$/)
1780
+
1781
+ if (isDeepResult) {
1782
+ historyEntry.resultAfterDeep = 'TBD'
1783
+ }
1784
+
1785
+ historyEntry.resultType = typeof finalResult
1786
+ historyEntry.valueBeforeResolution = valueBeforeResolution
1787
+ historyEntry.from = 'renderMatches'
1788
+ if (isDeepResult) {
1789
+ historyEntry.resultIsDeep = true
1500
1790
  }
1501
1791
 
1792
+ if (finalResult !== cleanResult) {
1793
+ historyEntry.resultEncoded = cleanResult
1794
+ }
1795
+
1796
+
1797
+
1798
+
1502
1799
  // Check if variable has fallback values (comma-separated)
1503
- const variableParts = splitByComma(matches[0].variable)
1800
+ const variableParts = splitByComma(matches[i].variable)
1504
1801
  if (variableParts.length > 1) {
1505
1802
  historyEntry.hasFallback = true
1506
1803
  historyEntry.valueBeforeFallback = variableParts[0]
@@ -1529,10 +1826,10 @@ class Configorama {
1529
1826
  fallbackData.isResolvedFallback = true
1530
1827
  }
1531
1828
  } else {
1532
- // Extract varType from variable references
1829
+ // Extract variableType from variable references
1533
1830
  const varTypeMatch = trimmedFallback.match(this.variablesKnownTypes)
1534
1831
  if (varTypeMatch && varTypeMatch[1]) {
1535
- fallbackData.varType = varTypeMatch[1]
1832
+ fallbackData.variableType = varTypeMatch[1]
1536
1833
  }
1537
1834
  }
1538
1835
 
@@ -1541,33 +1838,16 @@ class Configorama {
1541
1838
  }
1542
1839
 
1543
1840
  // Only add to history if not a duplicate (same match + variable)
1544
- const isDuplicate = valueObject.resolutionHistory.some(entry =>
1545
- entry.match === historyEntry.match &&
1841
+ const isDuplicate = valueObject.resolutionHistory.some(entry =>
1842
+ entry.match === historyEntry.match &&
1546
1843
  entry.variable === historyEntry.variable
1547
1844
  )
1548
-
1845
+
1549
1846
  if (!isDuplicate) {
1550
1847
  valueObject.resolutionHistory.push(historyEntry)
1551
1848
  }
1552
1849
 
1553
- // Save resolution history to tracking map for persistence across iterations
1554
- if (valueObject.path && valueObject.path.length) {
1555
- const pathKey = valueObject.path.join('.')
1556
- if (!this.resolutionTracking[pathKey]) {
1557
- this.resolutionTracking[pathKey] = {
1558
- path: pathKey,
1559
- originalPropertyString: valueObject.originalSource,
1560
- calls: []
1561
- }
1562
- }
1563
- this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1564
- }
1565
- }
1566
-
1567
- let result = valueObject.value
1568
- for (let i = 0; i < matches.length; i += 1) {
1569
- this.warnIfNotFound(matches[i].variable, results[i])
1570
- // console.log('Render MATCHES', results[i])
1850
+ // Process the match
1571
1851
  let valueToPop = results[i]
1572
1852
  // TODO refactor this. __internal_only_flag needed to stop clash with sync/async file resolution
1573
1853
  if (results[i] && typeof results[i] === 'object' && (results[i].__internal_only_flag || results[i].__internal_metadata)) {
@@ -1581,6 +1861,21 @@ class Configorama {
1581
1861
  console.log(this.deep)
1582
1862
  /** */
1583
1863
  }
1864
+
1865
+ // Save resolution history to tracking map for persistence across iterations
1866
+ if (valueObject.path && valueObject.path.length) {
1867
+ const pathKey = valueObject.path.join('.')
1868
+ if (!this.resolutionTracking[pathKey]) {
1869
+ this.resolutionTracking[pathKey] = {
1870
+ path: pathKey,
1871
+ originalPropertyString: valueObject.originalSource,
1872
+ resolvedPropertyValue: undefined,
1873
+ calls: []
1874
+ }
1875
+ }
1876
+ this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1877
+ }
1878
+
1584
1879
  return result
1585
1880
  }
1586
1881
  /**
@@ -1729,7 +2024,7 @@ class Configorama {
1729
2024
  if (currentDetails &&
1730
2025
  currentDetails.resultType === 'number' &&
1731
2026
  parentDetails && parentDetails.resultType === 'string' &&
1732
- parentDetails.result.match(/^\d+$/) && parentDetails.varType === 'env'
2027
+ parentDetails.result.match(/^\d+$/) && parentDetails.variableType === 'env'
1733
2028
  ) {
1734
2029
  if (Number(parentDetails.result) === currentDetails.result) {
1735
2030
  property = String(valueToPopulate)
@@ -1985,7 +2280,23 @@ Missing Value ${missingValue} - ${matchedString}
1985
2280
  })
1986
2281
  }
1987
2282
  property = foundFilters.reduce((acc, filter) => {
1988
- const newVal = this.filters[filter](acc, 'from populateVariable')
2283
+ // Check if filter has function-style arguments
2284
+ const funcMatch = filter.match(/^(\w+)\((.*)\)$/)
2285
+ let filterName = filter
2286
+ let filterArgs = []
2287
+
2288
+ if (funcMatch) {
2289
+ filterName = funcMatch[1]
2290
+ const rawArgs = funcMatch[2]
2291
+ if (rawArgs) {
2292
+ const splitter = splitCsv(rawArgs, ', ')
2293
+ filterArgs = formatFunctionArgs(splitter)
2294
+ }
2295
+ }
2296
+
2297
+ const newVal = filterArgs.length > 0
2298
+ ? this.filters[filterName](acc, ...filterArgs, 'from populateVariable')
2299
+ : this.filters[filterName](acc, 'from populateVariable')
1989
2300
  // console.log('PROPERTY', newVal)
1990
2301
  return newVal
1991
2302
  }, property)
@@ -2095,10 +2406,27 @@ Missing Value ${missingValue} - ${matchedString}
2095
2406
  this.resolutionTracking[pathKey] = {
2096
2407
  path: pathKey,
2097
2408
  originalPropertyString: propertyString,
2409
+ resolvedPropertyValue: undefined,
2098
2410
  calls: []
2099
2411
  }
2100
2412
  }
2101
2413
 
2414
+ // this.resolutionTracking[pathKey].resolutionHistory = this.resolutionTracking[pathKey].resolutionHistory || []
2415
+
2416
+ // const isDuplicate = this.resolutionTracking[pathKey].resolutionHistory.some(entry =>
2417
+ // entry.variableString === variableString
2418
+ // )
2419
+
2420
+ // if (!isDuplicate) {
2421
+ // this.resolutionTracking[pathKey].resolutionHistory.push({
2422
+ // variableString: variableString,
2423
+ // propertyString: propertyString,
2424
+ // caller: caller,
2425
+ // lol: 'what'
2426
+ // })
2427
+ // }
2428
+
2429
+
2102
2430
  this.resolutionTracking[pathKey].calls.push({
2103
2431
  variableString: variableString,
2104
2432
  propertyString: propertyString,
@@ -2205,7 +2533,6 @@ Missing Value ${missingValue} - ${matchedString}
2205
2533
  this.options,
2206
2534
  this.config,
2207
2535
  valueObject,
2208
-
2209
2536
  ).then((val) => {
2210
2537
  // Update the last call with the resolved value
2211
2538
  if (pathValue && pathValue.length) {
@@ -2214,7 +2541,8 @@ Missing Value ${missingValue} - ${matchedString}
2214
2541
  // Find the most recent call for this variableString
2215
2542
  for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
2216
2543
  if (this.resolutionTracking[pathKey].calls[i].variableString === variableString) {
2217
- this.resolutionTracking[pathKey].calls[i].resolvedValue = val
2544
+ const v = (typeof val === 'object' && val.__internal_only_flag) ? val.value : val
2545
+ this.resolutionTracking[pathKey].calls[i].resolvedValue = v
2218
2546
  this.resolutionTracking[pathKey].calls[i].resolverType = resolverType
2219
2547
  break
2220
2548
  }
@@ -2255,17 +2583,22 @@ Missing Value ${missingValue} - ${matchedString}
2255
2583
  // console.log('valueCount', valueCount)
2256
2584
  // TODO throw on empty values?
2257
2585
  // No fallback value found AND this is undefined, throw error
2258
- const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes)
2586
+ const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes, undefined, this.variableTypes)
2259
2587
  // console.log('nestedVars', nestedVars)
2260
2588
  const noNestedVars = nestedVars.length < 2
2589
+
2590
+ if (this.opts.allowUnknownFileRefs && variableString.match(fileRefSyntax)) {
2591
+ // Encode the unknown file variable to pass through resolution
2592
+ return Promise.resolve(encodeUnknown(propertyString))
2593
+ }
2594
+
2261
2595
  if (valueCount.length === 1 && noNestedVars) {
2262
- const configFilePath = (this.configFilePath) ? ` in ${this.configFilePath}` : ''
2263
- throw new Error(`
2264
- Unable to resolve configuration variable
2596
+ const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath} ` : ''
2597
+ const fromLine = (propertyString !== valueObject.originalSource) ? `\n From "${valueObject.originalSource}"\n` : ''
2598
+
2599
+
2265
2600
 
2266
- Value "${propertyString}"
2267
- From "${valueObject.originalSource}"
2268
- At location ${valueObject.path ? `"${arrayToJsonPath(valueObject.path)}"` : 'na'}${configFilePath}
2601
+ throw new Error(`Unable to resolve config variable "${propertyString}".\n${configFilePathMsg}at location ${valueObject.path ? `"${arrayToJsonPath(valueObject.path)}"` : 'n/a'}${fromLine}
2269
2602
  \nFix this reference, your inputs and/or provide a valid fallback value.
2270
2603
  \nExample of setting a fallback value: \${${variableString}, "fallbackValue"\}\n`)
2271
2604
  }
@@ -2300,13 +2633,28 @@ Unable to resolve configuration variable
2300
2633
  }
2301
2634
 
2302
2635
  const newUse = newHasFilter.reduce((acc, currentFilter, i) => {
2303
- if (!this.filters[currentFilter]) {
2304
- throw new Error(`Filter "${currentFilter}" not found`)
2636
+ // Check if filter has function-style arguments: filterName(arg1, arg2)
2637
+ const funcMatch = currentFilter.match(/^(\w+)\((.*)\)$/)
2638
+ let filterName = currentFilter
2639
+ let filterArgs = null
2640
+
2641
+ if (funcMatch) {
2642
+ filterName = funcMatch[1]
2643
+ const rawArgs = funcMatch[2]
2644
+ // Parse arguments using the same logic as functions
2645
+ if (rawArgs) {
2646
+ const splitter = splitCsv(rawArgs, ', ')
2647
+ filterArgs = formatFunctionArgs(splitter)
2648
+ }
2649
+ }
2650
+
2651
+ if (!this.filters[filterName]) {
2652
+ throw new Error(`Filter "${filterName}" not found`)
2305
2653
  }
2306
2654
  return acc.concat({
2307
- filter: this.filters[currentFilter],
2308
- filterName: currentFilter,
2309
- // args: argsToPass
2655
+ filter: this.filters[filterName],
2656
+ filterName: filterName,
2657
+ args: filterArgs
2310
2658
  })
2311
2659
  }, [])
2312
2660
  // console.log('pathValue', pathValue)
@@ -2568,7 +2916,7 @@ Unable to resolve configuration variable
2568
2916
  // console.log('matchedFileString', matchedFileString)
2569
2917
 
2570
2918
  // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
2571
- // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2919
+ // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2572
2920
  var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
2573
2921
  // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
2574
2922
  var hasParams = funcParamsRegex.exec(matchedFileString)
@@ -2592,45 +2940,37 @@ Unable to resolve configuration variable
2592
2940
  }
2593
2941
  // console.log('argsToPass', argsToPass)
2594
2942
 
2595
- const relativePath = trimSurroundingQuotes(
2596
- matchedFileString.replace(syntax, (match, varName) => varName.trim()).replace('~', os.homedir()),
2597
- )
2598
-
2599
- // Resolve alias if the path contains alias syntax
2600
- const resolvedPath = resolveAlias(relativePath, this.configPath)
2601
- // console.log('resolvedPath', resolvedPath)
2602
-
2603
- let fullFilePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(this.configPath, resolvedPath)
2943
+ const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, this.configPath)
2944
+ // console.log('fileDetails', fileDetails)
2604
2945
 
2605
- // console.log('fullFilePath', fullFilePath)
2946
+ const { fullFilePath, resolvedPath, relativePath } = fileDetails
2606
2947
 
2607
- if (fs.existsSync(fullFilePath)) {
2608
- // Get real path to handle potential symlinks (but don't fatal error)
2609
- fullFilePath = fs.realpathSync(fullFilePath)
2948
+ const exists = fs.existsSync(fullFilePath)
2610
2949
 
2611
- // Only match files that are relative
2612
- } else if (resolvedPath.match(/\.\//)) {
2613
- // TODO test higher parent refs
2614
- const cleanName = path.basename(resolvedPath)
2615
- const findUpResult = findUp.sync(cleanName, { cwd: this.configPath })
2616
- if (findUpResult) {
2617
- fullFilePath = findUpResult
2618
- }
2619
- }
2950
+ this.fileRefsFound.push({
2951
+ // location: options.context.path.join('.'),
2952
+ filePath: fullFilePath,
2953
+ relativePath,
2954
+ resolvedVariableString: options.context.value,
2955
+ originalVariableString: options.context.originalSource,
2956
+ containsVariables: options.context.value !== options.context.originalSource,
2957
+ exists,
2958
+ })
2620
2959
 
2621
2960
  let fileExtension = resolvedPath.split('.')
2622
2961
 
2623
2962
  fileExtension = fileExtension[fileExtension.length - 1]
2624
2963
 
2625
2964
  // Validate file exists
2626
- if (!fs.existsSync(fullFilePath)) {
2965
+ if (!exists) {
2627
2966
  const originalVar = options.context && options.context.originalSource
2628
2967
 
2629
2968
  const findNestedResult = findNestedVariables(
2630
- originalVar,
2631
- this.variableSyntax,
2632
- this.variablesKnownTypes,
2633
- options.context.path
2969
+ originalVar,
2970
+ this.variableSyntax,
2971
+ this.variablesKnownTypes,
2972
+ options.context.path,
2973
+ this.variableTypes
2634
2974
  )
2635
2975
  // console.log('findNestedResult', findNestedResult)
2636
2976
  let hasFallback = false
@@ -2644,9 +2984,10 @@ Unable to resolve configuration variable
2644
2984
  // console.log('NO FILE FOUND', fullFilePath)
2645
2985
  // console.log('variableString', variableString)
2646
2986
 
2647
- if (!hasFallback) {
2987
+ if (!hasFallback && !this.opts.allowUnknownFileRefs) {
2648
2988
  const errorMsg = makeBox({
2649
2989
  title: `File Not Found in ${originalVar}`,
2990
+ minWidth: '100%',
2650
2991
  text: `Variable ${variableString} cannot resolve due to missing file.
2651
2992
 
2652
2993
  File not found ${fullFilePath}
@@ -2662,13 +3003,19 @@ ${JSON.stringify(options.context, null, 2)}`,
2662
3003
  return Promise.resolve(undefined)
2663
3004
  }
2664
3005
 
3006
+
3007
+
2665
3008
  let valueToPopulate
2666
3009
 
2667
3010
  const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
2668
3011
 
2669
3012
  /* handle case for referencing raw JS files to inline them */
2670
- if (argsToPass.length && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw') || opts.asRawText) {
2671
- valueToPopulate = variableFileContents.replace(/\(/g, '__PH_PAREN_OPEN__')
3013
+ if (argsToPass.length
3014
+ && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
3015
+ || opts.asRawText
3016
+ ) {
3017
+ // Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
3018
+ valueToPopulate = encodeJsSyntax(variableFileContents)
2672
3019
  return Promise.resolve(valueToPopulate)
2673
3020
  }
2674
3021
 
@@ -3015,25 +3362,25 @@ Please use ":" to reference sub properties. ${deepProperties}`
3015
3362
  }
3016
3363
 
3017
3364
  warnIfNotFound(variableString, valueToPopulate) {
3018
- let varType
3365
+ let variableTypeText
3019
3366
  if (variableString.match(envRefSyntax)) {
3020
- varType = 'environment variable'
3367
+ variableTypeText = 'environment variable'
3021
3368
  } else if (variableString.match(optRefSyntax)) {
3022
- varType = 'option'
3369
+ variableTypeText = 'option'
3023
3370
  } else if (variableString.match(selfRefSyntax)) {
3024
- varType = 'config attribute'
3371
+ variableTypeText = 'config attribute'
3025
3372
  } else if (variableString.match(fileRefSyntax)) {
3026
- varType = 'file'
3373
+ variableTypeText = 'file'
3027
3374
  } else if (variableString.match(deepRefSyntax)) {
3028
- varType = 'deep'
3375
+ variableTypeText = 'deep'
3029
3376
  } else if (variableString.match(textRefSyntax)) {
3030
- varType = 'text'
3377
+ variableTypeText = 'text'
3031
3378
  }
3032
3379
  if (!isValidValue(valueToPopulate)) {
3033
3380
  // console.log("MISSING", variableString)
3034
3381
  // console.log(this.deep)
3035
3382
  // console.log(valueToPopulate)
3036
- const notFoundMsg = `No ${varType} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3383
+ const notFoundMsg = `No ${variableTypeText} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3037
3384
  if (DEBUG) {
3038
3385
  console.log(notFoundMsg)
3039
3386
  }