configorama 0.11.0 → 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 +159 -19
- package/src/parsers/esm.js +1 -16
- package/src/parsers/typescript.js +1 -48
- package/src/resolvers/valueFromCron.js +4 -25
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +8 -1
- 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 +22 -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/types/src/index.d.ts +0 -24
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +16 -8
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +0 -2
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromSelf.d.ts +1 -0
- package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/findLineForKey.d.ts +0 -9
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +1 -1
- package/types/src/display.d.ts +0 -62
- package/types/src/display.d.ts.map +0 -1
- package/types/src/metadata.d.ts +0 -28
- package/types/src/metadata.d.ts.map +0 -1
- package/types/src/utils/BoundedMap.d.ts +0 -10
- package/types/src/utils/BoundedMap.d.ts.map +0 -1
- package/types/src/utils/paths/ignorePaths.d.ts +0 -5
- package/types/src/utils/paths/ignorePaths.d.ts.map +0 -1
package/cli.js
CHANGED
|
@@ -9,11 +9,28 @@ const { logHeader } = require('./src/utils/ui/logs')
|
|
|
9
9
|
const configorama = require('./src')
|
|
10
10
|
const { makeBox } = require('@davidwells/box-logger')
|
|
11
11
|
const getValueAtPath = require('./src/utils/parsing/getValueAtPath')
|
|
12
|
+
const { redactConfigByRequirements } = require('./src/utils/redaction/setupRedaction')
|
|
13
|
+
const { normalizeError } = require('./src/errors')
|
|
14
|
+
const { didYouMean } = require('./src/utils/strings/didYouMean')
|
|
15
|
+
const { buildCapabilities } = require('./src/capabilities')
|
|
16
|
+
|
|
17
|
+
// Subcommands an agent might type; used for "did you mean" command suggestions.
|
|
18
|
+
const KNOWN_COMMANDS = ['inspect', 'requirements', 'audit', 'graph', 'setup', 'capabilities']
|
|
19
|
+
const INSPECT_VIEWS = ['requirements', 'audit', 'graph']
|
|
20
|
+
// Long flag names recognized by the parser; used for "did you mean" flag suggestions.
|
|
21
|
+
// Unknown flags that are NOT near one of these are left alone so arbitrary
|
|
22
|
+
// ${opt:...} passthrough flags (e.g. --stage, --domain) keep working.
|
|
23
|
+
const KNOWN_FLAGS = [
|
|
24
|
+
'help', 'version', 'output', 'format', 'param', 'error-format', 'safe-root', 'file-root',
|
|
25
|
+
'debug', 'verbose', 'allow-unknown', 'allow-undefined', 'allow-unknown-file-refs',
|
|
26
|
+
'return-metadata', 'list', 'info', 'verify', 'raw', 'copy', 'setup', 'requirements',
|
|
27
|
+
'capabilities', 'view', 'safe', 'unsafe',
|
|
28
|
+
]
|
|
12
29
|
|
|
13
30
|
// Parse command line arguments
|
|
14
31
|
const argv = minimist(process.argv.slice(2), {
|
|
15
|
-
string: ['output', 'o', 'format', 'f', 'param'],
|
|
16
|
-
boolean: ['help', 'h', 'version', 'v', 'V', 'debug', 'allow-unknown', 'allow-undefined', 'list', 'info', 'verify', 'raw', 'r', 'copy', 'c'],
|
|
32
|
+
string: ['output', 'o', 'format', 'f', 'param', 'error-format', 'safe-root', 'file-root', 'view'],
|
|
33
|
+
boolean: ['help', 'h', 'version', 'v', 'V', 'debug', 'allow-unknown', 'allow-undefined', 'list', 'info', 'verify', 'raw', 'r', 'copy', 'c', 'setup', 'requirements', 'capabilities', 'safe', 'unsafe'],
|
|
17
34
|
alias: {
|
|
18
35
|
h: 'help',
|
|
19
36
|
v: 'version',
|
|
@@ -38,21 +55,37 @@ Configorama - Variable resolution for configuration files
|
|
|
38
55
|
|
|
39
56
|
Usage:
|
|
40
57
|
configorama [options] <file> [path]
|
|
58
|
+
configorama <command> <file> [options]
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
(default) Resolve <file> and print the result
|
|
62
|
+
inspect <file> Introspect a config without resolving it (full model)
|
|
63
|
+
--view requirements|audit|graph for a single slice
|
|
64
|
+
setup <file> Run the interactive config wizard (experimental)
|
|
65
|
+
capabilities Print the machine-readable CLI contract (JSON)
|
|
41
66
|
|
|
42
67
|
Options:
|
|
43
68
|
-h, --help Show this help message
|
|
44
69
|
-v, --version Show version number
|
|
45
70
|
-o, --output <file> Write output to file instead of stdout
|
|
46
|
-
-f, --format <format> Output format: json, yaml,
|
|
71
|
+
-f, --format <format> Output format: json, yaml, js (resolve); json, mermaid, dot (graph)
|
|
72
|
+
--view <view> inspect view: requirements, audit, or graph
|
|
47
73
|
-r, --raw Print extracted scalar values without JSON quoting
|
|
48
74
|
-c, --copy Copy the formatted output to the clipboard
|
|
49
75
|
-d, --debug Enable debug mode
|
|
50
76
|
-i, --info Show info about the config
|
|
51
77
|
-V, --verify Verify the config
|
|
78
|
+
--safe Block executable/config-mutating surfaces during resolution
|
|
79
|
+
--unsafe Disable inspect/audit/graph default safe inspection
|
|
80
|
+
--safe-root <dir> Restrict file/text references to an allowed root
|
|
81
|
+
--error-format <fmt> Error format on stderr: json or human. json is the
|
|
82
|
+
default for inspect/requirements/audit/graph/capabilities
|
|
52
83
|
--param <key=value> Pass parameter values (can be used multiple times)
|
|
53
84
|
--allow-unknown Allow unknown variables to pass through
|
|
54
85
|
--allow-undefined Allow undefined values in the final output
|
|
55
86
|
|
|
87
|
+
Aliases: \`setup\` is also available as \`--setup\`, and \`capabilities\` as \`--capabilities\`.
|
|
88
|
+
|
|
56
89
|
Path Extraction:
|
|
57
90
|
Use jq-style paths to extract specific values from the resolved config.
|
|
58
91
|
Paths can appear before or after options.
|
|
@@ -72,6 +105,13 @@ Examples:
|
|
|
72
105
|
configorama -r --copy config.yml .database.host
|
|
73
106
|
configorama '.servers[0].port' config.yml
|
|
74
107
|
configorama --info config.yml
|
|
108
|
+
configorama --setup config.yml
|
|
109
|
+
configorama setup config.yml
|
|
110
|
+
configorama inspect config.yml
|
|
111
|
+
configorama inspect config.yml --view requirements
|
|
112
|
+
configorama inspect config.yml --view audit
|
|
113
|
+
configorama inspect config.yml --view graph --format mermaid
|
|
114
|
+
configorama capabilities
|
|
75
115
|
configorama --format yaml config.json
|
|
76
116
|
configorama --output resolved.json config.yml
|
|
77
117
|
configorama --param="domain=myapp.com" --param="key=value" config.yml
|
|
@@ -87,16 +127,89 @@ if (argv.version) {
|
|
|
87
127
|
process.exit(0)
|
|
88
128
|
}
|
|
89
129
|
|
|
130
|
+
// Capabilities is an informational command and needs no input file.
|
|
131
|
+
if (argv._[0] === 'capabilities' || argv.capabilities) {
|
|
132
|
+
const capsOutput = JSON.stringify(buildCapabilities(), null, 2)
|
|
133
|
+
if (argv.output) {
|
|
134
|
+
fs.writeFileSync(argv.output, capsOutput)
|
|
135
|
+
console.log(`Capabilities written to ${argv.output}`)
|
|
136
|
+
} else {
|
|
137
|
+
console.log(capsOutput)
|
|
138
|
+
}
|
|
139
|
+
process.exit(0)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Warn (don't fail) on a misspelled flag that is one edit from a known flag.
|
|
143
|
+
// Genuine ${opt:...} passthrough flags are far from any known flag, so they
|
|
144
|
+
// are left untouched and keep working.
|
|
145
|
+
function warnUnknownFlags() {
|
|
146
|
+
for (const token of process.argv.slice(2)) {
|
|
147
|
+
if (!token.startsWith('--') || token === '--') continue
|
|
148
|
+
const name = token.slice(2).split('=')[0]
|
|
149
|
+
if (!name || KNOWN_FLAGS.includes(name)) continue
|
|
150
|
+
const suggestion = didYouMean(name, KNOWN_FLAGS)
|
|
151
|
+
if (suggestion) {
|
|
152
|
+
console.error(`Warning: unknown flag "--${name}". Did you mean "--${suggestion}"?`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
warnUnknownFlags()
|
|
157
|
+
|
|
90
158
|
// Parse positional args: find file path and jq-style extraction path
|
|
91
159
|
// File is first arg that exists as a file, jq path starts with '.' or '['
|
|
92
160
|
let inputFile = null
|
|
93
161
|
let extractPath = null
|
|
162
|
+
const requirementsSubcommand = argv._[0] === 'requirements'
|
|
163
|
+
const auditSubcommand = argv._[0] === 'audit'
|
|
164
|
+
const graphSubcommand = argv._[0] === 'graph'
|
|
165
|
+
const inspectSubcommand = argv._[0] === 'inspect'
|
|
166
|
+
const requirementsMode = requirementsSubcommand || Boolean(argv.requirements)
|
|
167
|
+
// Commands that emit JSON to stdout; their errors default to JSON on stderr too.
|
|
168
|
+
const structuredCommand = requirementsMode || auditSubcommand || graphSubcommand || inspectSubcommand
|
|
94
169
|
|
|
95
170
|
function isFileArg(arg) {
|
|
96
171
|
if (fs.existsSync(arg) && fs.statSync(arg).isFile()) return true
|
|
97
172
|
return arg.startsWith('./') || arg.startsWith('../')
|
|
98
173
|
}
|
|
99
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Print a CLI-level error and exit non-zero, honoring --error-format.
|
|
177
|
+
* Structured commands default to JSON; resolve mode defaults to a plain message.
|
|
178
|
+
* @param {string} code - stable error code (see ERROR_CODES)
|
|
179
|
+
* @param {string} message - human-readable message
|
|
180
|
+
* @param {object} [details] - extra machine-readable context
|
|
181
|
+
*/
|
|
182
|
+
function emitCliError(code, message, details = {}) {
|
|
183
|
+
const wantsJson = argv['error-format'] === 'json' || (structuredCommand && argv['error-format'] !== 'human')
|
|
184
|
+
if (wantsJson) {
|
|
185
|
+
console.error(JSON.stringify({ error: { code, message, details } }, null, 2))
|
|
186
|
+
} else {
|
|
187
|
+
console.error(`Error: ${message}`)
|
|
188
|
+
}
|
|
189
|
+
process.exit(1)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If the first arg looks like a misspelled command (not a file or jq-path),
|
|
193
|
+
// suggest the closest command instead of silently treating it as a filename.
|
|
194
|
+
const firstArg = argv._[0]
|
|
195
|
+
if (
|
|
196
|
+
firstArg &&
|
|
197
|
+
!KNOWN_COMMANDS.includes(firstArg) &&
|
|
198
|
+
!firstArg.startsWith('.') &&
|
|
199
|
+
!firstArg.startsWith('[') &&
|
|
200
|
+
!isFileArg(firstArg) &&
|
|
201
|
+
!fs.existsSync(firstArg)
|
|
202
|
+
) {
|
|
203
|
+
const suggestion = didYouMean(firstArg, KNOWN_COMMANDS)
|
|
204
|
+
if (suggestion) {
|
|
205
|
+
emitCliError(
|
|
206
|
+
'unknown_command',
|
|
207
|
+
`Unknown command "${firstArg}". Did you mean "${suggestion}"? Run --help for usage.`,
|
|
208
|
+
{ provided: firstArg, suggestion, commands: KNOWN_COMMANDS }
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
100
213
|
function getClipboardCommands() {
|
|
101
214
|
if (process.env.CONFIGORAMA_CLIPBOARD_COMMAND) {
|
|
102
215
|
return [{ command: process.env.CONFIGORAMA_CLIPBOARD_COMMAND, shell: true }]
|
|
@@ -132,29 +245,32 @@ function copyToClipboard(value) {
|
|
|
132
245
|
}
|
|
133
246
|
}
|
|
134
247
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
inputFile
|
|
248
|
+
if (requirementsSubcommand) {
|
|
249
|
+
inputFile = argv._[1] || null
|
|
250
|
+
} else if (auditSubcommand || graphSubcommand || inspectSubcommand) {
|
|
251
|
+
inputFile = argv._[1] || null
|
|
252
|
+
} else {
|
|
253
|
+
for (const arg of argv._) {
|
|
254
|
+
if (arg === 'setup') continue
|
|
255
|
+
|
|
256
|
+
// jq-style paths start with '.' or '['
|
|
257
|
+
if (!inputFile && isFileArg(arg)) {
|
|
258
|
+
inputFile = arg
|
|
259
|
+
} else if (arg.startsWith('.') || arg.startsWith('[')) {
|
|
260
|
+
extractPath = arg
|
|
261
|
+
} else if (!inputFile) {
|
|
262
|
+
inputFile = arg
|
|
263
|
+
}
|
|
145
264
|
}
|
|
146
265
|
}
|
|
147
266
|
|
|
148
267
|
if (!inputFile) {
|
|
149
|
-
|
|
150
|
-
console.error('Run with --help for usage information')
|
|
151
|
-
process.exit(1)
|
|
268
|
+
emitCliError('no_input_file', 'No input file specified. Run with --help for usage information.', {})
|
|
152
269
|
}
|
|
153
270
|
|
|
154
271
|
// Check if file exists
|
|
155
272
|
if (!fs.existsSync(inputFile)) {
|
|
156
|
-
|
|
157
|
-
process.exit(1)
|
|
273
|
+
emitCliError('file_not_found', `File not found: ${inputFile}`, { path: inputFile })
|
|
158
274
|
}
|
|
159
275
|
|
|
160
276
|
// Set options for Configorama
|
|
@@ -164,9 +280,21 @@ const options = {
|
|
|
164
280
|
allowUnknownFileRefs: argv['allow-unknown-file-refs'] || false,
|
|
165
281
|
returnMetadata: argv['return-metadata'] || false,
|
|
166
282
|
returnPreResolvedVariableDetails: false,
|
|
283
|
+
// Setup wizard via --setup flag or `setup` subcommand
|
|
284
|
+
setup: Boolean(argv.setup) || argv._.includes('setup'),
|
|
285
|
+
safeMode: Boolean(argv.safe) || ((auditSubcommand || graphSubcommand || inspectSubcommand) && !argv.unsafe),
|
|
286
|
+
safeRoots: [],
|
|
167
287
|
dynamicArgs: argv
|
|
168
288
|
}
|
|
169
289
|
|
|
290
|
+
options.safeRoots = []
|
|
291
|
+
if (argv['safe-root']) options.safeRoots = options.safeRoots.concat(argv['safe-root'])
|
|
292
|
+
if (argv['file-root']) options.safeRoots = options.safeRoots.concat(argv['file-root'])
|
|
293
|
+
if (options.safeRoots.length > 0) {
|
|
294
|
+
options.allowedFileRoots = options.safeRoots
|
|
295
|
+
options.restrictFileRoots = true
|
|
296
|
+
}
|
|
297
|
+
|
|
170
298
|
const dynamicArgs = options.dynamicArgs || {}
|
|
171
299
|
const {
|
|
172
300
|
_,
|
|
@@ -188,7 +316,14 @@ const {
|
|
|
188
316
|
raw,
|
|
189
317
|
c,
|
|
190
318
|
copy,
|
|
191
|
-
|
|
319
|
+
setup,
|
|
320
|
+
requirements,
|
|
321
|
+
safe,
|
|
322
|
+
unsafe,
|
|
323
|
+
'safe-root': safeRoot,
|
|
324
|
+
'file-root': fileRoot,
|
|
325
|
+
'error-format': errorFormat,
|
|
326
|
+
'allow-unknown': allowUnknown,
|
|
192
327
|
'allow-undefined': allowUndefined,
|
|
193
328
|
'allow-unknown-file-refs': allowUnknownFileRefs,
|
|
194
329
|
'return-metadata': returnMetadata,
|
|
@@ -208,57 +343,166 @@ if (options.dynamicArgs.verbose) {
|
|
|
208
343
|
console.log()
|
|
209
344
|
}
|
|
210
345
|
|
|
211
|
-
let isSetupMode = false
|
|
212
|
-
if (argv._.length) {
|
|
213
|
-
isSetupMode = argv._.includes('setup')
|
|
214
|
-
}
|
|
215
|
-
|
|
216
346
|
// Set -- flags as options
|
|
217
347
|
options.options = rest
|
|
218
348
|
options.handleSignalEvents = true
|
|
219
349
|
|
|
350
|
+
function handleCliError(error) {
|
|
351
|
+
const wantsJson = argv['error-format'] === 'json' || (structuredCommand && argv['error-format'] !== 'human')
|
|
352
|
+
if (wantsJson) {
|
|
353
|
+
console.error(JSON.stringify(normalizeError(error).toJSON(), null, 2))
|
|
354
|
+
process.exit(1)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const errorMsg = makeBox({
|
|
358
|
+
title: `Error Processing Configuration: ${inputFile}`,
|
|
359
|
+
minWidth: '100%',
|
|
360
|
+
content: error.message,
|
|
361
|
+
type: 'error',
|
|
362
|
+
})
|
|
363
|
+
console.error(errorMsg)
|
|
364
|
+
if (argv.debug) {
|
|
365
|
+
console.error('error', error)
|
|
366
|
+
}
|
|
367
|
+
process.exit(1)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (requirementsMode) {
|
|
371
|
+
configorama.analyze(inputFile, {
|
|
372
|
+
...options,
|
|
373
|
+
instructions: true,
|
|
374
|
+
})
|
|
375
|
+
.then((requirementsJson) => {
|
|
376
|
+
const output = JSON.stringify(requirementsJson, null, 2)
|
|
377
|
+
|
|
378
|
+
if (argv.copy) {
|
|
379
|
+
const copyResult = copyToClipboard(output)
|
|
380
|
+
if (!copyResult.ok) {
|
|
381
|
+
console.error(`Error: Unable to copy to clipboard: ${copyResult.error}`)
|
|
382
|
+
process.exit(1)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (argv.output) {
|
|
387
|
+
fs.writeFileSync(argv.output, output)
|
|
388
|
+
console.log(`Configuration written to ${argv.output}`)
|
|
389
|
+
} else if (!argv.verbose) {
|
|
390
|
+
console.log(output)
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
.catch(handleCliError)
|
|
394
|
+
} else if (auditSubcommand) {
|
|
395
|
+
configorama.audit(inputFile, options)
|
|
396
|
+
.then((report) => {
|
|
397
|
+
const output = JSON.stringify(report, null, 2)
|
|
398
|
+
|
|
399
|
+
if (argv.output) {
|
|
400
|
+
fs.writeFileSync(argv.output, output)
|
|
401
|
+
console.log(`Audit written to ${argv.output}`)
|
|
402
|
+
} else {
|
|
403
|
+
console.log(output)
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
.catch(handleCliError)
|
|
407
|
+
} else if (graphSubcommand) {
|
|
408
|
+
configorama.graph(inputFile, {
|
|
409
|
+
...options,
|
|
410
|
+
format: argv.format || 'json',
|
|
411
|
+
})
|
|
412
|
+
.then((graphOutput) => {
|
|
413
|
+
const output = typeof graphOutput === 'string' ? graphOutput : JSON.stringify(graphOutput, null, 2)
|
|
414
|
+
if (argv.output) {
|
|
415
|
+
fs.writeFileSync(argv.output, output)
|
|
416
|
+
console.log(`Graph written to ${argv.output}`)
|
|
417
|
+
} else {
|
|
418
|
+
console.log(output)
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
.catch(handleCliError)
|
|
422
|
+
} else if (inspectSubcommand) {
|
|
423
|
+
const view = argv.view
|
|
424
|
+
if (view && !INSPECT_VIEWS.includes(view)) {
|
|
425
|
+
const suggestion = didYouMean(String(view), INSPECT_VIEWS)
|
|
426
|
+
emitCliError(
|
|
427
|
+
'invalid_view',
|
|
428
|
+
`Unknown view "${view}".${suggestion ? ` Did you mean "${suggestion}"?` : ''} Valid views: ${INSPECT_VIEWS.join(', ')}.`,
|
|
429
|
+
{ provided: view, suggestion: suggestion || null, views: INSPECT_VIEWS }
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
configorama.inspect(inputFile, {
|
|
433
|
+
...options,
|
|
434
|
+
view: view || undefined,
|
|
435
|
+
format: argv.format || 'json',
|
|
436
|
+
})
|
|
437
|
+
.then((result) => {
|
|
438
|
+
const output = typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
439
|
+
if (argv.output) {
|
|
440
|
+
fs.writeFileSync(argv.output, output)
|
|
441
|
+
console.log(`Inspection written to ${argv.output}`)
|
|
442
|
+
} else {
|
|
443
|
+
console.log(output)
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
.catch(handleCliError)
|
|
447
|
+
} else {
|
|
448
|
+
|
|
220
449
|
// Process the configuration
|
|
221
|
-
|
|
450
|
+
const shouldRedactSetupStdout = options.setup && !argv.output && !argv.copy
|
|
451
|
+
let setupRequirementsForRedaction = []
|
|
452
|
+
const configPromise = shouldRedactSetupStdout
|
|
453
|
+
? (() => {
|
|
454
|
+
const instance = new Configorama(inputFile, options)
|
|
455
|
+
return instance.init(options.options || {}).then((config) => {
|
|
456
|
+
setupRequirementsForRedaction = instance.setupRequirements || []
|
|
457
|
+
return config
|
|
458
|
+
})
|
|
459
|
+
})()
|
|
460
|
+
: configorama(inputFile, options)
|
|
461
|
+
|
|
462
|
+
configPromise
|
|
222
463
|
.then((config) => {
|
|
464
|
+
let outputConfig = shouldRedactSetupStdout
|
|
465
|
+
? redactConfigByRequirements(config, setupRequirementsForRedaction)
|
|
466
|
+
: config
|
|
467
|
+
|
|
223
468
|
// Apply path extraction if specified
|
|
224
469
|
if (extractPath) {
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
process.exit(1)
|
|
470
|
+
outputConfig = getValueAtPath(outputConfig, extractPath)
|
|
471
|
+
if (outputConfig === undefined) {
|
|
472
|
+
emitCliError('path_not_found', `Path not found: ${extractPath}`, { path: extractPath })
|
|
229
473
|
}
|
|
230
474
|
}
|
|
231
475
|
|
|
232
476
|
let output
|
|
233
477
|
|
|
234
478
|
// Format the output
|
|
235
|
-
if (argv.raw && extractPath && (
|
|
236
|
-
output =
|
|
479
|
+
if (argv.raw && extractPath && (outputConfig === null || ['string', 'number', 'boolean'].includes(typeof outputConfig))) {
|
|
480
|
+
output = outputConfig === null ? 'null' : String(outputConfig)
|
|
237
481
|
} else switch (argv.format.toLowerCase()) {
|
|
238
482
|
case 'yaml':
|
|
239
483
|
case 'yml':
|
|
240
484
|
const YAML = require('./src/parsers/yaml')
|
|
241
|
-
output = YAML.dump(
|
|
485
|
+
output = YAML.dump(outputConfig)
|
|
242
486
|
break
|
|
243
487
|
case 'esm':
|
|
244
488
|
case 'mjs':
|
|
245
489
|
case 'module':
|
|
246
|
-
output = `export default ${JSON.stringify(
|
|
490
|
+
output = `export default ${JSON.stringify(outputConfig, null, 2)}`
|
|
247
491
|
break
|
|
248
492
|
case 'js':
|
|
249
493
|
case 'cjs':
|
|
250
494
|
case 'commonjs':
|
|
251
495
|
case 'javascript':
|
|
252
|
-
output = `module.exports = ${JSON.stringify(
|
|
496
|
+
output = `module.exports = ${JSON.stringify(outputConfig, null, 2)}`
|
|
253
497
|
break
|
|
254
498
|
case 'json':
|
|
255
499
|
case 'json5':
|
|
256
500
|
default:
|
|
257
501
|
if (returnMetadata) {
|
|
258
502
|
// turn regex into string
|
|
259
|
-
|
|
503
|
+
outputConfig.variableSyntax = outputConfig.variableSyntax ? outputConfig.variableSyntax.source : undefined
|
|
260
504
|
}
|
|
261
|
-
output = JSON.stringify(
|
|
505
|
+
output = JSON.stringify(outputConfig, null, 2)
|
|
262
506
|
}
|
|
263
507
|
|
|
264
508
|
if (argv.copy) {
|
|
@@ -283,16 +527,5 @@ configorama(inputFile, options)
|
|
|
283
527
|
}
|
|
284
528
|
}
|
|
285
529
|
})
|
|
286
|
-
.catch(
|
|
287
|
-
|
|
288
|
-
title: `Error Processing Configuration: ${inputFile}`,
|
|
289
|
-
minWidth: '100%',
|
|
290
|
-
content: error.message,
|
|
291
|
-
type: 'error',
|
|
292
|
-
})
|
|
293
|
-
console.error(errorMsg)
|
|
294
|
-
if (argv.debug) {
|
|
295
|
-
console.error('error', error)
|
|
296
|
-
}
|
|
297
|
-
process.exit(1)
|
|
298
|
-
})
|
|
530
|
+
.catch(handleCliError)
|
|
531
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -77,6 +77,24 @@ interface ConfigoramaSettings {
|
|
|
77
77
|
filePathOverrides?: Record<string, string>
|
|
78
78
|
/** Install Configorama CLI signal handlers. Defaults to false for library calls. */
|
|
79
79
|
handleSignalEvents?: boolean
|
|
80
|
+
/** Block executable and mutating surfaces such as JS/TS file refs, custom resolvers/functions, and dotenv. */
|
|
81
|
+
safeMode?: boolean
|
|
82
|
+
/** Alias for safeMode */
|
|
83
|
+
safe?: boolean
|
|
84
|
+
/** Restrict file/text references to allowed roots. Enabled by default in safeMode. */
|
|
85
|
+
restrictFileRoots?: boolean
|
|
86
|
+
/** Allowed roots for file/text references */
|
|
87
|
+
allowedFileRoots?: string[]
|
|
88
|
+
/** Alias for allowedFileRoots */
|
|
89
|
+
safeRoots?: string[]
|
|
90
|
+
/** Allow executable file refs even when safeMode is enabled */
|
|
91
|
+
blockExecutableFiles?: boolean
|
|
92
|
+
/** Allow custom resolvers even when safeMode is enabled */
|
|
93
|
+
blockCustomResolvers?: boolean
|
|
94
|
+
/** Allow custom functions even when safeMode is enabled */
|
|
95
|
+
blockCustomFunctions?: boolean
|
|
96
|
+
/** Allow dotenv loading even when safeMode is enabled */
|
|
97
|
+
blockDotEnv?: boolean
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
interface ConfigoramaResult<T = any> {
|
|
@@ -129,12 +147,36 @@ declare namespace configorama {
|
|
|
129
147
|
settings?: ConfigoramaSettings
|
|
130
148
|
): T
|
|
131
149
|
|
|
132
|
-
/**
|
|
150
|
+
/** Unified introspection entry point. Returns requirements, graph, and audit, or one view. */
|
|
151
|
+
export function inspect(
|
|
152
|
+
configPathOrObject: string | object,
|
|
153
|
+
settings?: ConfigoramaSettings & { view?: 'requirements' | 'audit' | 'graph', format?: 'json' | 'mermaid' | 'mmd' | 'dot' | 'graphviz' }
|
|
154
|
+
): Promise<any>
|
|
155
|
+
|
|
156
|
+
/** @deprecated Use inspect(config, { view: 'requirements' }) for requirements, or returnMetadata for resolved metadata. */
|
|
133
157
|
export function analyze(
|
|
134
158
|
configPathOrObject: string | object,
|
|
135
159
|
settings?: ConfigoramaSettings
|
|
136
160
|
): Promise<any>
|
|
137
161
|
|
|
162
|
+
/** @deprecated Use inspect(config) for the unified inspection model. */
|
|
163
|
+
export function introspect(
|
|
164
|
+
configPathOrObject: string | object,
|
|
165
|
+
settings?: ConfigoramaSettings
|
|
166
|
+
): Promise<any>
|
|
167
|
+
|
|
168
|
+
/** @deprecated Use inspect(config, { view: 'audit' }). */
|
|
169
|
+
export function audit(
|
|
170
|
+
configPathOrObject: string | object,
|
|
171
|
+
settings?: ConfigoramaSettings
|
|
172
|
+
): Promise<any>
|
|
173
|
+
|
|
174
|
+
/** @deprecated Use inspect(config, { view: 'graph', format }). */
|
|
175
|
+
export function graph(
|
|
176
|
+
configPathOrObject: string | object,
|
|
177
|
+
settings?: ConfigoramaSettings & { format?: 'json' | 'mermaid' | 'mmd' | 'dot' | 'graphviz', formatGraph?: boolean }
|
|
178
|
+
): Promise<any>
|
|
179
|
+
|
|
138
180
|
/** Format utilities for parsing various config formats */
|
|
139
181
|
export const format: {
|
|
140
182
|
yaml: any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Variable support for configuration files",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"info": "repomix --include \"cli.js,src/**/*.js\" --ignore \"**/*.test.js\" --copy",
|
|
26
26
|
"docs": "node ./scripts/docs.js",
|
|
27
|
+
"site:dev": "cd site && npm run dev",
|
|
28
|
+
"site:build": "cd site && npm run build",
|
|
29
|
+
"site:validate": "cd site && npm run validate",
|
|
30
|
+
"site:deploy": "cd site && npm run deploy",
|
|
27
31
|
"t": "./scripts/run-tests.sh",
|
|
28
32
|
"test": "npm run test:slow && npm run test:lib && uvu tests \".*\\.test.js$\"",
|
|
29
33
|
"test:tests": "uvu tests \".*\\.test.js$\" ",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Machine-readable description of the configorama CLI contract for agents.
|
|
2
|
+
// Lists commands, views, output formats, error codes, exit codes, and flags so
|
|
3
|
+
// an agent can read the contract from the tool instead of guessing or reading docs.
|
|
4
|
+
|
|
5
|
+
const { ERROR_CODES } = require('./errors')
|
|
6
|
+
const pkg = require('../package.json')
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1
|
|
9
|
+
|
|
10
|
+
const VIEWS = ['requirements', 'audit', 'graph']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the capabilities contract object.
|
|
14
|
+
* @returns {object} stable, deterministic description of the CLI surface
|
|
15
|
+
*/
|
|
16
|
+
function buildCapabilities() {
|
|
17
|
+
return {
|
|
18
|
+
name: 'configorama',
|
|
19
|
+
version: pkg.version,
|
|
20
|
+
schemaVersion: SCHEMA_VERSION,
|
|
21
|
+
commands: [
|
|
22
|
+
{ name: 'resolve', usage: 'configorama <file> [path]', summary: 'Resolve a config file and print the result (default command).' },
|
|
23
|
+
{ name: 'inspect', usage: 'configorama inspect <file> [--view requirements|audit|graph]', summary: 'Introspect a config without resolving it; returns the full model or one view.' },
|
|
24
|
+
{ name: 'setup', usage: 'configorama setup <file>', summary: 'Run the interactive setup wizard (experimental).' },
|
|
25
|
+
{ name: 'capabilities', usage: 'configorama capabilities', summary: 'Print this machine-readable contract.' },
|
|
26
|
+
],
|
|
27
|
+
// Hidden back-compat commands that map to an inspect view. They still run if
|
|
28
|
+
// typed, but inspect is the documented surface.
|
|
29
|
+
aliases: [
|
|
30
|
+
{ name: 'requirements', mapsTo: 'inspect --view requirements', summary: 'Inputs a config needs.' },
|
|
31
|
+
{ name: 'audit', mapsTo: 'inspect --view audit', summary: 'Risky references.' },
|
|
32
|
+
{ name: 'graph', mapsTo: 'inspect --view graph', summary: 'Dependency graph of variables and files.' },
|
|
33
|
+
],
|
|
34
|
+
views: VIEWS,
|
|
35
|
+
formats: {
|
|
36
|
+
resolve: ['json', 'yaml', 'js', 'esm'],
|
|
37
|
+
graph: ['json', 'mermaid', 'dot'],
|
|
38
|
+
},
|
|
39
|
+
errorCodes: ERROR_CODES,
|
|
40
|
+
exitCodes: [
|
|
41
|
+
{ code: 0, meaning: 'Success.' },
|
|
42
|
+
{ code: 1, meaning: 'Error. The category is in the JSON `error.code` field (use --error-format json).' },
|
|
43
|
+
],
|
|
44
|
+
flags: [
|
|
45
|
+
{ flag: '--output, -o <file>', summary: 'Write output to a file instead of stdout.' },
|
|
46
|
+
{ flag: '--format, -f <format>', summary: 'Output format (see formats).' },
|
|
47
|
+
{ flag: '--view <view>', summary: 'inspect view: requirements, audit, or graph.' },
|
|
48
|
+
{ flag: '--raw, -r', summary: 'Print extracted scalar values without JSON quoting.' },
|
|
49
|
+
{ flag: '--error-format <json|human>', summary: 'Error output format on stderr (json is machine-readable).' },
|
|
50
|
+
{ flag: '--param <key=value>', summary: 'Pass parameter values (repeatable).' },
|
|
51
|
+
{ flag: '--safe', summary: 'Block executable/config-mutating references during resolution.' },
|
|
52
|
+
{ flag: '--safe-root <dir>', summary: 'Restrict file/text references to an allowed root.' },
|
|
53
|
+
{ flag: '--allow-unknown', summary: 'Allow unknown variables to pass through.' },
|
|
54
|
+
{ flag: '--allow-undefined', summary: 'Allow undefined values in the final output.' },
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { SCHEMA_VERSION, VIEWS, buildCapabilities }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { buildCapabilities } = require('./capabilities')
|
|
4
|
+
const { ERROR_CODES } = require('./errors')
|
|
5
|
+
const pkg = require('../package.json')
|
|
6
|
+
|
|
7
|
+
test('buildCapabilities - reports name, version and schemaVersion', () => {
|
|
8
|
+
const caps = buildCapabilities()
|
|
9
|
+
assert.is(caps.name, 'configorama')
|
|
10
|
+
assert.is(caps.version, pkg.version)
|
|
11
|
+
assert.is(caps.schemaVersion, 1)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('buildCapabilities - commands list is the documented surface', () => {
|
|
15
|
+
const names = buildCapabilities().commands.map(c => c.name)
|
|
16
|
+
assert.equal(names, ['resolve', 'inspect', 'setup', 'capabilities'])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('buildCapabilities - hidden verbs are discoverable under aliases', () => {
|
|
20
|
+
const aliases = buildCapabilities().aliases
|
|
21
|
+
assert.equal(aliases.map(a => a.name), ['requirements', 'audit', 'graph'])
|
|
22
|
+
assert.is(aliases.find(a => a.name === 'audit').mapsTo, 'inspect --view audit')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('buildCapabilities - exposes the full error-code registry', () => {
|
|
26
|
+
const caps = buildCapabilities()
|
|
27
|
+
assert.equal(caps.errorCodes, ERROR_CODES)
|
|
28
|
+
const codes = caps.errorCodes.map(e => e.code)
|
|
29
|
+
assert.ok(codes.includes('missing_env'))
|
|
30
|
+
assert.ok(codes.includes('blocked_by_safe_mode'))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('buildCapabilities - documents exit codes and graph formats', () => {
|
|
34
|
+
const caps = buildCapabilities()
|
|
35
|
+
assert.equal(caps.exitCodes.map(e => e.code), [0, 1])
|
|
36
|
+
assert.equal(caps.formats.graph, ['json', 'mermaid', 'dot'])
|
|
37
|
+
assert.equal(caps.views, ['requirements', 'audit', 'graph'])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('buildCapabilities - output is deterministic', () => {
|
|
41
|
+
assert.is(JSON.stringify(buildCapabilities()), JSON.stringify(buildCapabilities()))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test.run()
|