configorama 0.11.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +429 -123
  2. package/cli.js +282 -49
  3. package/index.d.ts +43 -1
  4. package/package.json +5 -1
  5. package/src/capabilities.js +59 -0
  6. package/src/capabilities.test.js +44 -0
  7. package/src/display.js +70 -7
  8. package/src/display.test.js +82 -0
  9. package/src/errors.js +73 -0
  10. package/src/index.js +91 -1
  11. package/src/main.js +117 -15
  12. package/src/resolvers/valueFromEval.js +11 -1
  13. package/src/resolvers/valueFromFile.js +5 -0
  14. package/src/resolvers/valueFromGit.js +43 -17
  15. package/src/resolvers/valueFromOptions.js +5 -4
  16. package/src/utils/filters/filterArgs.js +57 -0
  17. package/src/utils/filters/oneOf.js +77 -0
  18. package/src/utils/introspection/audit.js +78 -0
  19. package/src/utils/introspection/graph.js +43 -0
  20. package/src/utils/introspection/model.js +150 -0
  21. package/src/utils/introspection/model.test.js +93 -0
  22. package/src/utils/parsing/commentAnnotations.js +107 -0
  23. package/src/utils/parsing/commentAnnotations.test.js +123 -0
  24. package/src/utils/parsing/enrichMetadata.js +64 -1
  25. package/src/utils/parsing/enrichMetadata.test.js +84 -0
  26. package/src/utils/parsing/extractComment.js +145 -0
  27. package/src/utils/parsing/extractComment.test.js +182 -0
  28. package/src/utils/parsing/preProcess.js +2 -1
  29. package/src/utils/paths/findLineForKey.js +2 -2
  30. package/src/utils/paths/ignorePaths.js +21 -9
  31. package/src/utils/redaction/redact.js +78 -0
  32. package/src/utils/redaction/redact.test.js +38 -0
  33. package/src/utils/redaction/setupRedaction.js +47 -0
  34. package/src/utils/redaction/setupRedaction.test.js +68 -0
  35. package/src/utils/requirements/configRequirements.js +351 -0
  36. package/src/utils/requirements/configRequirements.test.js +380 -0
  37. package/src/utils/requirements/serializeRequirements.js +120 -0
  38. package/src/utils/requirements/serializeRequirements.test.js +211 -0
  39. package/src/utils/security/evalSafety.js +86 -0
  40. package/src/utils/security/evalSafety.test.js +61 -0
  41. package/src/utils/security/safetyPolicy.js +110 -0
  42. package/src/utils/security/safetyPolicy.test.js +29 -0
  43. package/src/utils/strings/didYouMean.js +70 -0
  44. package/src/utils/strings/didYouMean.test.js +52 -0
  45. package/src/utils/strings/formatFunctionArgs.js +6 -1
  46. package/src/utils/strings/splitByComma.js +5 -0
  47. package/src/utils/ui/configWizard.js +208 -34
  48. package/src/utils/ui/createEditorLink.js +17 -1
  49. package/src/utils/ui/promptDescriptors.js +196 -0
  50. package/src/utils/ui/promptDescriptors.test.js +162 -0
  51. package/src/utils/variables/cleanVariable.js +22 -0
  52. package/src/utils/variables/getVariableType.js +1 -0
@@ -0,0 +1,162 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const {
4
+ createPromptDescriptor,
5
+ createPromptDescriptors,
6
+ groupRequirementsForWizard,
7
+ normalizePromptValue,
8
+ selectPromptType,
9
+ stripPromptQuotes,
10
+ validatePromptValue,
11
+ } = require('./promptDescriptors')
12
+
13
+ function req(overrides) {
14
+ return {
15
+ name: 'stage',
16
+ variable: 'option:stage',
17
+ variableType: 'option',
18
+ type: 'string',
19
+ required: true,
20
+ sensitive: false,
21
+ description: null,
22
+ default: null,
23
+ allowedValues: null,
24
+ paths: ['stage'],
25
+ conflicts: [],
26
+ ...overrides,
27
+ }
28
+ }
29
+
30
+ test('groupRequirementsForWizard maps requirements to wizard groups', () => {
31
+ const grouped = groupRequirementsForWizard([
32
+ req({ variableType: 'option' }),
33
+ req({ variableType: 'env' }),
34
+ req({ variableType: 'self' }),
35
+ req({ variableType: 'dotProp' }),
36
+ req({ variableType: 'file' }),
37
+ ])
38
+
39
+ assert.is(grouped.options.length, 1)
40
+ assert.is(grouped.env.length, 1)
41
+ assert.is(grouped.self.length, 1)
42
+ assert.is(grouped.dotProp.length, 1)
43
+ assert.is(grouped.files.length, 1)
44
+ })
45
+
46
+ test('groupRequirementsForWizard supports annotation display groups', () => {
47
+ const grouped = groupRequirementsForWizard([
48
+ req({ variableType: 'env', group: 'Payments' }),
49
+ ])
50
+
51
+ assert.is(grouped.Payments.length, 1)
52
+ assert.is(grouped.env.length, 0)
53
+ })
54
+
55
+ test('selectPromptType covers sensitivity, enum, boolean, array, and text modes', () => {
56
+ assert.is(selectPromptType(req({ sensitive: true })), 'password')
57
+ assert.is(selectPromptType(req({ allowedValues: ['dev', 'prod'] })), 'select')
58
+ assert.is(selectPromptType(req({ type: 'array', allowedValues: ['a', 'b'] })), 'multiselect')
59
+ assert.is(selectPromptType(req({ type: 'boolean' })), 'confirm')
60
+ assert.is(selectPromptType(req({ type: 'object' })), 'text')
61
+ assert.is(selectPromptType(req({ type: 'json' })), 'text')
62
+ })
63
+
64
+ test('createPromptDescriptor strips quoted defaults and reads current env values', () => {
65
+ const envName = 'CONFIGORAMA_DESCRIPTOR_ENV'
66
+ const previous = process.env[envName]
67
+ process.env[envName] = 'from-env'
68
+
69
+ try {
70
+ const optionDescriptor = createPromptDescriptor(req({
71
+ default: '"dev"',
72
+ }))
73
+ assert.is(optionDescriptor.defaultValue, 'dev')
74
+ assert.is(optionDescriptor.placeholder, 'dev')
75
+
76
+ const envDescriptor = createPromptDescriptor(req({
77
+ name: envName,
78
+ variable: `env:${envName}`,
79
+ variableType: 'env',
80
+ default: null,
81
+ }))
82
+ assert.is(envDescriptor.defaultValue, 'from-env')
83
+ assert.is(envDescriptor.placeholder, 'from-env')
84
+ } finally {
85
+ if (previous === undefined) delete process.env[envName]
86
+ else process.env[envName] = previous
87
+ }
88
+ })
89
+
90
+ test('createPromptDescriptor includes conflict warning metadata without throwing', () => {
91
+ const descriptor = createPromptDescriptor(req({
92
+ conflicts: [
93
+ { field: 'type', paths: ['stage', 'provider.stage'], values: [] }
94
+ ]
95
+ }))
96
+
97
+ assert.equal(descriptor.conflicts, [
98
+ { field: 'type', paths: ['stage', 'provider.stage'], values: [] }
99
+ ])
100
+ assert.is(descriptor.conflictWarning, 'type conflict at stage, provider.stage')
101
+ })
102
+
103
+ test('createPromptDescriptor carries annotation metadata without changing runtime defaults', () => {
104
+ const descriptor = createPromptDescriptor(req({
105
+ name: 'API_KEY',
106
+ variable: 'env:API_KEY',
107
+ variableType: 'env',
108
+ group: 'Payments',
109
+ sensitive: false,
110
+ obtainHint: 'Stripe dashboard > Developers > API keys',
111
+ examples: ['sk_live_...'],
112
+ defaultHint: 'Set in CI',
113
+ deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
114
+ default: null,
115
+ }))
116
+
117
+ assert.is(descriptor.group, 'Payments')
118
+ assert.is(descriptor.sensitive, false)
119
+ assert.is(descriptor.promptType, 'text')
120
+ assert.is(descriptor.obtainHint, 'Stripe dashboard > Developers > API keys')
121
+ assert.equal(descriptor.examples, ['sk_live_...'])
122
+ assert.is(descriptor.defaultHint, 'Set in CI')
123
+ assert.is(descriptor.deprecationMessage, 'Use STRIPE_RESTRICTED_KEY instead')
124
+ assert.is(descriptor.defaultValue, null)
125
+ assert.is(descriptor.placeholder, 'Enter environment variable for API_KEY')
126
+ })
127
+
128
+ test('createPromptDescriptor uses sensitive override for prompt type', () => {
129
+ assert.is(createPromptDescriptor(req({ name: 'PUBLIC_VALUE', sensitive: true })).promptType, 'password')
130
+ assert.is(createPromptDescriptor(req({ name: 'API_KEY', sensitive: false })).promptType, 'text')
131
+ })
132
+
133
+ test('normalizePromptValue coerces descriptor values', () => {
134
+ assert.is(stripPromptQuotes("'dev'"), 'dev')
135
+ assert.is(normalizePromptValue('4', { type: 'number' }), 4)
136
+ assert.is(normalizePromptValue('yes', { type: 'boolean' }), true)
137
+ assert.equal(normalizePromptValue('a, b,c', { type: 'array' }), ['a', 'b', 'c'])
138
+ assert.equal(normalizePromptValue('{"a":1}', { type: 'object' }), { a: 1 })
139
+ assert.is(normalizePromptValue('', { type: 'string', defaultValue: 'fallback' }), 'fallback')
140
+ })
141
+
142
+ test('validatePromptValue validates required, allowed, number, boolean, and JSON values', () => {
143
+ assert.is(validatePromptValue('', { required: true, type: 'string' }), 'This value is required')
144
+ assert.match(validatePromptValue('qa', { type: 'string', allowedValues: ['dev', 'prod'] }), /Must be one of/)
145
+ assert.match(validatePromptValue('abc', { type: 'number' }), /valid number/)
146
+ assert.match(validatePromptValue('maybe', { type: 'boolean' }), /boolean/)
147
+ assert.match(validatePromptValue('{bad', { type: 'json' }), /valid JSON/)
148
+ assert.is(validatePromptValue('{"ok":true}', { type: 'json' }), undefined)
149
+ })
150
+
151
+ test('createPromptDescriptors returns serializable prompt decisions plus pure helpers', () => {
152
+ const descriptors = createPromptDescriptors([
153
+ req({ allowedValues: ['dev', 'prod'] }),
154
+ ])
155
+
156
+ assert.is(descriptors.length, 1)
157
+ assert.is(descriptors[0].promptType, 'select')
158
+ assert.is(descriptors[0].validate('dev'), undefined)
159
+ assert.is(descriptors[0].normalize('prod'), 'prod')
160
+ })
161
+
162
+ test.run()
@@ -2,6 +2,9 @@ const { findNestedVariables } = require('./findNestedVariables')
2
2
  const { functionRegex, fileRefSyntax } = require('../regex')
3
3
 
4
4
  const DEBUG = false
5
+ // cleanVariable is a pure function of (match, variableSyntax); the same variable
6
+ // strings recur many times per run, so cache results per syntax regex.
7
+ const cleanCache = new WeakMap()
5
8
  /**
6
9
  * Convert variable into string
7
10
  * ${opt:foo} => 'opt:foo'
@@ -47,10 +50,29 @@ module.exports = function cleanVariable(
47
50
  // process.exit(1)
48
51
  /** */
49
52
 
53
+ const cacheable = variableSyntax && typeof variableSyntax === 'object'
54
+ let inner = cacheable ? cleanCache.get(variableSyntax) : undefined
55
+ if (inner) {
56
+ const hit = inner.get(varToClean)
57
+ if (hit !== undefined) {
58
+ // Mirror String.replace's reset of the global regex's lastIndex.
59
+ variableSyntax.lastIndex = 0
60
+ return hit
61
+ }
62
+ }
63
+
50
64
  const clean = varToClean.replace(variableSyntax, (context, contents) => {
51
65
  return contents.trim()
52
66
  })
53
67
 
68
+ if (cacheable) {
69
+ if (!inner) {
70
+ inner = new Map()
71
+ cleanCache.set(variableSyntax, inner)
72
+ }
73
+ inner.set(varToClean, clean)
74
+ }
75
+
54
76
  // if (recursive && clean.match(variableSyntax)) {
55
77
  // return cleanVariable(clean, variableSyntax, simple, caller, true)
56
78
  // }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fallbackMap = {
4
4
  opt: 'options',
5
+ option: 'options',
5
6
  file: 'file',
6
7
  text: 'text',
7
8
  }