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.
- package/README.md +429 -123
- package/cli.js +282 -49
- package/index.d.ts +43 -1
- package/package.json +5 -1
- package/src/capabilities.js +59 -0
- package/src/capabilities.test.js +44 -0
- package/src/display.js +70 -7
- package/src/display.test.js +82 -0
- package/src/errors.js +73 -0
- package/src/index.js +91 -1
- package/src/main.js +117 -15
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +5 -0
- package/src/resolvers/valueFromGit.js +43 -17
- package/src/resolvers/valueFromOptions.js +5 -4
- package/src/utils/filters/filterArgs.js +57 -0
- package/src/utils/filters/oneOf.js +77 -0
- package/src/utils/introspection/audit.js +78 -0
- package/src/utils/introspection/graph.js +43 -0
- package/src/utils/introspection/model.js +150 -0
- package/src/utils/introspection/model.test.js +93 -0
- package/src/utils/parsing/commentAnnotations.js +107 -0
- package/src/utils/parsing/commentAnnotations.test.js +123 -0
- package/src/utils/parsing/enrichMetadata.js +64 -1
- package/src/utils/parsing/enrichMetadata.test.js +84 -0
- package/src/utils/parsing/extractComment.js +145 -0
- package/src/utils/parsing/extractComment.test.js +182 -0
- package/src/utils/parsing/preProcess.js +2 -1
- package/src/utils/paths/findLineForKey.js +2 -2
- package/src/utils/paths/ignorePaths.js +21 -9
- package/src/utils/redaction/redact.js +78 -0
- package/src/utils/redaction/redact.test.js +38 -0
- package/src/utils/redaction/setupRedaction.js +47 -0
- package/src/utils/redaction/setupRedaction.test.js +68 -0
- package/src/utils/requirements/configRequirements.js +351 -0
- package/src/utils/requirements/configRequirements.test.js +380 -0
- package/src/utils/requirements/serializeRequirements.js +120 -0
- package/src/utils/requirements/serializeRequirements.test.js +211 -0
- package/src/utils/security/evalSafety.js +86 -0
- package/src/utils/security/evalSafety.test.js +61 -0
- package/src/utils/security/safetyPolicy.js +110 -0
- package/src/utils/security/safetyPolicy.test.js +29 -0
- package/src/utils/strings/didYouMean.js +70 -0
- package/src/utils/strings/didYouMean.test.js +52 -0
- package/src/utils/strings/formatFunctionArgs.js +6 -1
- package/src/utils/strings/splitByComma.js +5 -0
- package/src/utils/ui/configWizard.js +208 -34
- package/src/utils/ui/createEditorLink.js +17 -1
- package/src/utils/ui/promptDescriptors.js +196 -0
- package/src/utils/ui/promptDescriptors.test.js +162 -0
- package/src/utils/variables/cleanVariable.js +22 -0
- package/src/utils/variables/getVariableType.js +1 -0
package/src/display.js
CHANGED
|
@@ -5,13 +5,64 @@ const chalk = require('./utils/ui/chalk')
|
|
|
5
5
|
const { logHeader } = require('./utils/ui/logs')
|
|
6
6
|
const { makeStackedBoxes } = require('@davidwells/box-logger')
|
|
7
7
|
const { findLineForKey } = require('./utils/paths/findLineForKey')
|
|
8
|
-
const { createEditorLink } = require('./utils/ui/createEditorLink')
|
|
9
|
-
const { isSensitiveVariable } = require('./utils/
|
|
8
|
+
const { createEditorLink, toClickablePath } = require('./utils/ui/createEditorLink')
|
|
9
|
+
const { isSensitiveVariable } = require('./utils/redaction/redact')
|
|
10
10
|
|
|
11
11
|
const SPACING = ' '
|
|
12
12
|
const TITLE_TEXT = `Variable:${SPACING}`
|
|
13
13
|
const VALUE_HEX = '#899499'
|
|
14
14
|
|
|
15
|
+
function uniqueCompact(values) {
|
|
16
|
+
return [...new Set((values || []).filter(value => value !== undefined && value !== null && value !== ''))]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function firstValue(source, field) {
|
|
20
|
+
if (!source) return undefined
|
|
21
|
+
if (source[field] !== undefined && source[field] !== null && source[field] !== '') return source[field]
|
|
22
|
+
const occurrences = Array.isArray(source.occurrences) ? source.occurrences : []
|
|
23
|
+
const occurrence = occurrences.find(occ => occ[field] !== undefined && occ[field] !== null && occ[field] !== '')
|
|
24
|
+
return occurrence ? occurrence[field] : undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAnnotationMetadata(source) {
|
|
28
|
+
const occurrences = Array.isArray(source?.occurrences) ? source.occurrences : []
|
|
29
|
+
return {
|
|
30
|
+
group: firstValue(source, 'group'),
|
|
31
|
+
obtainHint: firstValue(source, 'obtainHint'),
|
|
32
|
+
examples: uniqueCompact([
|
|
33
|
+
...(Array.isArray(source?.examples) ? source.examples : []),
|
|
34
|
+
...occurrences.flatMap(occ => Array.isArray(occ.examples) ? occ.examples : [])
|
|
35
|
+
]),
|
|
36
|
+
defaultHint: firstValue(source, 'defaultHint'),
|
|
37
|
+
deprecationMessage: firstValue(source, 'deprecationMessage'),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function appendAnnotationMetadata(varMsg, source, keyChalk, valueChalk, indent = '') {
|
|
42
|
+
const metadata = getAnnotationMetadata(source)
|
|
43
|
+
const lines = []
|
|
44
|
+
|
|
45
|
+
if (metadata.group) lines.push(['Group:', metadata.group])
|
|
46
|
+
if (metadata.obtainHint) lines.push(['From:', metadata.obtainHint])
|
|
47
|
+
if (metadata.examples.length > 0) {
|
|
48
|
+
lines.push([metadata.examples.length === 1 ? 'Example:' : 'Examples:', metadata.examples.join(', ')])
|
|
49
|
+
}
|
|
50
|
+
if (metadata.defaultHint) lines.push(['Default hint:', metadata.defaultHint])
|
|
51
|
+
if (metadata.deprecationMessage) lines.push(['Deprecated:', metadata.deprecationMessage])
|
|
52
|
+
|
|
53
|
+
for (const [label, value] of lines) {
|
|
54
|
+
varMsg += `${indent}${keyChalk(label.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(value)}\n`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return varMsg
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isSensitiveOccurrence(varName, occurrences = []) {
|
|
61
|
+
const explicitOccurrence = occurrences.find(occ => typeof occ.sensitive === 'boolean')
|
|
62
|
+
if (explicitOccurrence) return explicitOccurrence.sensitive
|
|
63
|
+
return isSensitiveVariable(varName)
|
|
64
|
+
}
|
|
65
|
+
|
|
15
66
|
/**
|
|
16
67
|
* Display "No Variables Found" message
|
|
17
68
|
* @param {string} configFilePath
|
|
@@ -58,9 +109,13 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
|
|
|
58
109
|
|
|
59
110
|
const getBaseVarName = (key) => key.replace(varPrefixPattern, '').replace(varSuffixPattern, '').split(',')[0].trim()
|
|
60
111
|
|
|
61
|
-
|
|
112
|
+
logHeader(`Found ${varKeys.length} Variables`)
|
|
62
113
|
|
|
63
|
-
|
|
114
|
+
// Print the path on its own line (outside the wrapping box) so it stays clickable
|
|
115
|
+
if (configFilePath) {
|
|
116
|
+
console.log()
|
|
117
|
+
console.log(` in ${toClickablePath(configFilePath)}`)
|
|
118
|
+
}
|
|
64
119
|
|
|
65
120
|
// deepLog('variableData', variableData)
|
|
66
121
|
|
|
@@ -96,6 +151,8 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
|
|
|
96
151
|
// Get uniqueVariable data for description and other metadata
|
|
97
152
|
const varName = getBaseVarName(key)
|
|
98
153
|
const uniqueVar = uniqueVariables[varName]
|
|
154
|
+
const occurrenceMetadata = uniqueVar || { occurrences: variableInstances }
|
|
155
|
+
const isSensitive = isSensitiveOccurrence(varName, occurrenceMetadata.occurrences || variableInstances)
|
|
99
156
|
|
|
100
157
|
// Build display message from enriched metadata
|
|
101
158
|
let varMsg = ''
|
|
@@ -128,7 +185,7 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
|
|
|
128
185
|
|
|
129
186
|
// Show default value from metadata
|
|
130
187
|
if (typeof firstInstance.defaultValue !== 'undefined') {
|
|
131
|
-
const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
|
|
188
|
+
const defaultValueRender = isSensitive ? '********' : (firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue)
|
|
132
189
|
const defaultValueText = `${indent}${keyChalk('Default value:'.padEnd(TITLE_TEXT.length, ' '))}`
|
|
133
190
|
varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
|
|
134
191
|
}
|
|
@@ -144,6 +201,8 @@ function displayVariableDetails({ varKeys, variableData, uniqueVariables, varPre
|
|
|
144
201
|
}
|
|
145
202
|
}
|
|
146
203
|
|
|
204
|
+
varMsg = appendAnnotationMetadata(varMsg, occurrenceMetadata, keyChalk, valueChalk, indent)
|
|
205
|
+
|
|
147
206
|
// Show path(s) from metadata
|
|
148
207
|
const configPathLine = findLineForKey(variableInstances[0].path, lines, fileType)
|
|
149
208
|
let locationRender = configPathLine
|
|
@@ -250,7 +309,7 @@ function displayUniqueVariables({ uniqueVarKeys, uniqueVariables, lines, fileTyp
|
|
|
250
309
|
|
|
251
310
|
// Show default value only if it's a true fallback, not a pre-resolved value
|
|
252
311
|
// Redact sensitive values like API keys, secrets, tokens
|
|
253
|
-
const isSensitive =
|
|
312
|
+
const isSensitive = isSensitiveOccurrence(varName, occurrences)
|
|
254
313
|
const hasActualDefault = firstOcc.hasFallback && typeof firstOcc.defaultValue !== 'undefined'
|
|
255
314
|
if (hasActualDefault) {
|
|
256
315
|
const defaultValueRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
|
|
@@ -275,6 +334,8 @@ function displayUniqueVariables({ uniqueVarKeys, uniqueVariables, lines, fileTyp
|
|
|
275
334
|
}
|
|
276
335
|
}
|
|
277
336
|
|
|
337
|
+
varMsg = appendAnnotationMetadata(varMsg, uniqueVar, keyChalk, valueChalk)
|
|
338
|
+
|
|
278
339
|
// Show config path(s) from occurrences
|
|
279
340
|
let locationRender
|
|
280
341
|
let locationLabel
|
|
@@ -403,7 +464,7 @@ function displayConfigurableVariables({ uniqueVarKeys, uniqueVariables, lines, f
|
|
|
403
464
|
}
|
|
404
465
|
|
|
405
466
|
// Show current/default value (redact sensitive values)
|
|
406
|
-
const isSensitive =
|
|
467
|
+
const isSensitive = isSensitiveOccurrence(v.varName, occurrences)
|
|
407
468
|
if (v.resolvedValue !== undefined) {
|
|
408
469
|
const resolvedRender = isSensitive ? '********' : (v.resolvedValue === '' ? '""' : v.resolvedValue)
|
|
409
470
|
varMsg += `${keyChalk('Current value:'.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(resolvedRender)}\n`
|
|
@@ -412,6 +473,8 @@ function displayConfigurableVariables({ uniqueVarKeys, uniqueVariables, lines, f
|
|
|
412
473
|
varMsg += `${keyChalk('Default value:'.padEnd(TITLE_TEXT.length, ' '))} ${valueChalk(defaultRender)}\n`
|
|
413
474
|
}
|
|
414
475
|
|
|
476
|
+
varMsg = appendAnnotationMetadata(varMsg, v, keyChalk, valueChalk)
|
|
477
|
+
|
|
415
478
|
// Show config path(s)
|
|
416
479
|
let locationRender
|
|
417
480
|
let locationLabel
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { displayConfigurableVariables } = require('./display')
|
|
4
|
+
|
|
5
|
+
function captureStdout(fn) {
|
|
6
|
+
const originalLog = console.log
|
|
7
|
+
const output = []
|
|
8
|
+
console.log = (...args) => output.push(args.join(' '))
|
|
9
|
+
try {
|
|
10
|
+
fn()
|
|
11
|
+
} finally {
|
|
12
|
+
console.log = originalLog
|
|
13
|
+
}
|
|
14
|
+
return output.join('\n')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('displayConfigurableVariables renders annotation metadata and sensitive overrides', () => {
|
|
18
|
+
const output = captureStdout(() => {
|
|
19
|
+
displayConfigurableVariables({
|
|
20
|
+
uniqueVarKeys: [
|
|
21
|
+
'env:CONFIGORAMA_DISPLAY_SECRET',
|
|
22
|
+
'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY',
|
|
23
|
+
],
|
|
24
|
+
uniqueVariables: {
|
|
25
|
+
'env:CONFIGORAMA_DISPLAY_SECRET': {
|
|
26
|
+
varName: 'env:CONFIGORAMA_DISPLAY_SECRET',
|
|
27
|
+
variableType: 'env',
|
|
28
|
+
variableSourceType: 'user',
|
|
29
|
+
descriptions: ['Stripe live secret key'],
|
|
30
|
+
resolvedValue: 'secret-value',
|
|
31
|
+
occurrences: [
|
|
32
|
+
{
|
|
33
|
+
path: 'secrets.stripeSecret',
|
|
34
|
+
varMatch: '${env:CONFIGORAMA_DISPLAY_SECRET}',
|
|
35
|
+
isRequired: true,
|
|
36
|
+
sensitive: true,
|
|
37
|
+
group: 'Payments',
|
|
38
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
39
|
+
examples: ['sk_live_...'],
|
|
40
|
+
defaultHint: 'Set in CI',
|
|
41
|
+
deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY': {
|
|
46
|
+
varName: 'env:CONFIGORAMA_DISPLAY_PUBLIC_KEY',
|
|
47
|
+
variableType: 'env',
|
|
48
|
+
variableSourceType: 'user',
|
|
49
|
+
descriptions: ['Public publishable key'],
|
|
50
|
+
resolvedValue: 'public-value',
|
|
51
|
+
occurrences: [
|
|
52
|
+
{
|
|
53
|
+
path: 'secrets.publishableKey',
|
|
54
|
+
varMatch: '${env:CONFIGORAMA_DISPLAY_PUBLIC_KEY}',
|
|
55
|
+
isRequired: true,
|
|
56
|
+
sensitive: false,
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
lines: [],
|
|
62
|
+
fileType: 'yaml',
|
|
63
|
+
configFilePath: '',
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
assert.match(output, /Group:/)
|
|
68
|
+
assert.match(output, /Payments/)
|
|
69
|
+
assert.match(output, /From:/)
|
|
70
|
+
assert.match(output, /Stripe dashboard > Developers > API keys/)
|
|
71
|
+
assert.match(output, /Example:/)
|
|
72
|
+
assert.match(output, /sk_live_\.\.\./)
|
|
73
|
+
assert.match(output, /Default hint:/)
|
|
74
|
+
assert.match(output, /Set in CI/)
|
|
75
|
+
assert.match(output, /Deprecated:/)
|
|
76
|
+
assert.match(output, /Use STRIPE_RESTRICTED_KEY instead/)
|
|
77
|
+
assert.match(output, /\*{8}/)
|
|
78
|
+
assert.not.match(output, /secret-value/)
|
|
79
|
+
assert.match(output, /public-value/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test.run()
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Registry of stable error codes emitted on `error.code` and in `--error-format json`.
|
|
2
|
+
// Single source of truth for the codes referenced by classifyErrorMessage and thrown
|
|
3
|
+
// directly as ConfigoramaError; surfaced by `configorama capabilities`.
|
|
4
|
+
const ERROR_CODES = [
|
|
5
|
+
{ code: 'missing_env', description: 'A referenced environment variable was not set and had no fallback.' },
|
|
6
|
+
{ code: 'missing_file', description: 'A referenced file could not be found on disk.' },
|
|
7
|
+
{ code: 'unresolved_variable', description: 'A variable could not be resolved to a value.' },
|
|
8
|
+
{ code: 'unknown_filter', description: 'A pipe filter name is not registered.' },
|
|
9
|
+
{ code: 'invalid_variable_syntax', description: 'A variable reference is malformed.' },
|
|
10
|
+
{ code: 'circular_dependency', description: 'Variables reference each other in a cycle.' },
|
|
11
|
+
{ code: 'invalid_view', description: 'An unknown --view was passed to the inspect command.' },
|
|
12
|
+
{ code: 'blocked_by_safe_mode', description: 'An executable or mutating reference was blocked by --safe.' },
|
|
13
|
+
{ code: 'blocked_eval_escape', description: 'An eval/if expression attempted a prototype-chain (constructor) escape.' },
|
|
14
|
+
{ code: 'file_root_forbidden', description: 'A file/text reference resolved outside an allowed --safe-root.' },
|
|
15
|
+
{ code: 'unknown_command', description: 'The first argument was not a recognized command.' },
|
|
16
|
+
{ code: 'no_input_file', description: 'No config file was provided on the command line.' },
|
|
17
|
+
{ code: 'file_not_found', description: 'The provided config file path does not exist.' },
|
|
18
|
+
{ code: 'path_not_found', description: 'A jq-style extraction path matched nothing in the resolved config.' },
|
|
19
|
+
{ code: 'configorama_error', description: 'Generic, unclassified configorama error.' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
class ConfigoramaError extends Error {
|
|
23
|
+
constructor(code, message, details = {}) {
|
|
24
|
+
super(message)
|
|
25
|
+
this.name = 'ConfigoramaError'
|
|
26
|
+
this.code = code || 'configorama_error'
|
|
27
|
+
this.details = details
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
toJSON() {
|
|
31
|
+
return {
|
|
32
|
+
error: {
|
|
33
|
+
code: this.code,
|
|
34
|
+
message: this.message,
|
|
35
|
+
details: this.details || {},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isConfigoramaError(error) {
|
|
42
|
+
return !!(error && error.name === 'ConfigoramaError' && error.code)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeError(error, fallbackCode = 'configorama_error') {
|
|
46
|
+
if (isConfigoramaError(error)) return error
|
|
47
|
+
const code = error && error.code ? error.code : classifyErrorMessage(error && error.message, fallbackCode)
|
|
48
|
+
return new ConfigoramaError(
|
|
49
|
+
code,
|
|
50
|
+
error && error.message ? error.message : String(error),
|
|
51
|
+
error && error.details ? error.details : {}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function classifyErrorMessage(message, fallbackCode = 'configorama_error') {
|
|
56
|
+
const text = String(message || '')
|
|
57
|
+
if (/Filter ".+" not found/.test(text)) return 'unknown_filter'
|
|
58
|
+
if (/Unable to resolve config variable/.test(text) && /env:/.test(text)) return 'missing_env'
|
|
59
|
+
if (/Unable to resolve config variable/.test(text)) return 'unresolved_variable'
|
|
60
|
+
if (/Blocked eval expression/.test(text)) return 'blocked_eval_escape'
|
|
61
|
+
if (/File not found|cannot resolve due to missing file/i.test(text)) return 'missing_file'
|
|
62
|
+
if (/Invalid variable reference syntax/.test(text)) return 'invalid_variable_syntax'
|
|
63
|
+
if (/Circular variable dependency/.test(text)) return 'circular_dependency'
|
|
64
|
+
return fallbackCode
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
ERROR_CODES,
|
|
69
|
+
ConfigoramaError,
|
|
70
|
+
classifyErrorMessage,
|
|
71
|
+
isConfigoramaError,
|
|
72
|
+
normalizeError,
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,14 @@ const Configorama = require('./main')
|
|
|
2
2
|
const parsers = require('./parsers')
|
|
3
3
|
const enrichMetadata = require('./utils/parsing/enrichMetadata')
|
|
4
4
|
const { buildVariableSyntax } = require('./utils/variables/variableUtils')
|
|
5
|
+
const { serializeRequirements } = require('./utils/requirements/serializeRequirements')
|
|
6
|
+
const { buildConfigRequirements } = require('./utils/requirements/configRequirements')
|
|
7
|
+
const { buildIntrospection } = require('./utils/introspection/model')
|
|
8
|
+
const { buildAuditReport } = require('./utils/introspection/audit')
|
|
9
|
+
const { formatGraph } = require('./utils/introspection/graph')
|
|
10
|
+
const { ConfigoramaError } = require('./errors')
|
|
11
|
+
|
|
12
|
+
const INSPECT_VIEWS = ['requirements', 'audit', 'graph']
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
15
|
* @typedef {Object} ConfigoramaSettings
|
|
@@ -127,7 +135,89 @@ module.exports.analyze = async (configPathOrObject, settings = {}) => {
|
|
|
127
135
|
returnPreResolvedVariableDetails: true,
|
|
128
136
|
})
|
|
129
137
|
const options = settings.options || {}
|
|
130
|
-
|
|
138
|
+
const analysis = await instance.init(options)
|
|
139
|
+
if (settings.instructions) {
|
|
140
|
+
return serializeRequirements(analysis, { configPathOrObject })
|
|
141
|
+
}
|
|
142
|
+
return analysis
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports.introspect = async (configPathOrObject, settings = {}) => {
|
|
146
|
+
const analysis = await module.exports.analyze(configPathOrObject, {
|
|
147
|
+
...settings,
|
|
148
|
+
blockCustomResolvers: false,
|
|
149
|
+
blockCustomFunctions: false,
|
|
150
|
+
blockDotEnv: false,
|
|
151
|
+
})
|
|
152
|
+
const requirements = buildConfigRequirements(analysis)
|
|
153
|
+
return buildIntrospection(analysis, { requirements })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports.audit = async (configPathOrObject, settings = {}) => {
|
|
157
|
+
const analysis = await module.exports.analyze(configPathOrObject, {
|
|
158
|
+
...settings,
|
|
159
|
+
blockCustomResolvers: false,
|
|
160
|
+
blockCustomFunctions: false,
|
|
161
|
+
blockDotEnv: false,
|
|
162
|
+
})
|
|
163
|
+
const requirements = buildConfigRequirements(analysis)
|
|
164
|
+
const introspection = buildIntrospection(analysis, { requirements })
|
|
165
|
+
const customResolvers = Array.isArray(settings.variableSources)
|
|
166
|
+
? settings.variableSources.map(source => source.type).filter(Boolean)
|
|
167
|
+
: []
|
|
168
|
+
const originalConfig = analysis.originalConfig || {}
|
|
169
|
+
return buildAuditReport(introspection, {
|
|
170
|
+
safeMode: settings.safeMode === true || settings.safe === true,
|
|
171
|
+
dotenv: originalConfig.useDotenv === true || originalConfig.useDotEnv === true || settings.useDotEnvFiles === true,
|
|
172
|
+
customResolvers,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports.graph = async (configPathOrObject, settings = {}) => {
|
|
177
|
+
const graph = await module.exports.introspect(configPathOrObject, settings)
|
|
178
|
+
if (settings.formatGraph === false) return graph
|
|
179
|
+
return formatGraph(graph, settings.format || 'json')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Unified introspection entry point. Without a view it returns the full model
|
|
184
|
+
* (requirements + graph + audit). With `settings.view` it returns a single
|
|
185
|
+
* projection, identical to the focused `requirements` / `graph` / `audit` verbs.
|
|
186
|
+
* @param {string|object} configPathOrObject - Path to config file or raw config object
|
|
187
|
+
* @param {object} [settings] - Same settings as the main API, plus `view` and `format`
|
|
188
|
+
* @return {Promise<object|string>} the requested model (string for mermaid/dot graph views)
|
|
189
|
+
*/
|
|
190
|
+
module.exports.inspect = async (configPathOrObject, settings = {}) => {
|
|
191
|
+
const view = settings.view
|
|
192
|
+
if (view !== undefined && view !== null && view !== '' && !INSPECT_VIEWS.includes(view)) {
|
|
193
|
+
throw new ConfigoramaError('invalid_view', `Unknown inspect view "${view}". Valid views: ${INSPECT_VIEWS.join(', ')}.`)
|
|
194
|
+
}
|
|
195
|
+
if (view === 'requirements') {
|
|
196
|
+
return module.exports.analyze(configPathOrObject, { ...settings, instructions: true })
|
|
197
|
+
}
|
|
198
|
+
if (view === 'audit') {
|
|
199
|
+
return module.exports.audit(configPathOrObject, settings)
|
|
200
|
+
}
|
|
201
|
+
if (view === 'graph') {
|
|
202
|
+
return module.exports.graph(configPathOrObject, settings)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Full model: each projection runs its own verb's code path so the unified
|
|
206
|
+
// output stays identical to `requirements`, `graph`, and `audit` run alone.
|
|
207
|
+
// (Those paths use different analyze settings, so they are not collapsible
|
|
208
|
+
// into a single shared analysis without changing their output.)
|
|
209
|
+
const [requirements, graph, audit] = await Promise.all([
|
|
210
|
+
module.exports.analyze(configPathOrObject, { ...settings, instructions: true }),
|
|
211
|
+
module.exports.graph(configPathOrObject, { ...settings, formatGraph: false }),
|
|
212
|
+
module.exports.audit(configPathOrObject, settings),
|
|
213
|
+
])
|
|
214
|
+
return {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
config: typeof configPathOrObject === 'string' ? configPathOrObject : (settings.configFilePath || null),
|
|
217
|
+
requirements,
|
|
218
|
+
graph,
|
|
219
|
+
audit,
|
|
220
|
+
}
|
|
131
221
|
}
|
|
132
222
|
|
|
133
223
|
/**
|