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.
Files changed (78) 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 +159 -19
  12. package/src/parsers/esm.js +1 -16
  13. package/src/parsers/typescript.js +1 -48
  14. package/src/resolvers/valueFromCron.js +4 -25
  15. package/src/resolvers/valueFromEval.js +11 -1
  16. package/src/resolvers/valueFromFile.js +8 -1
  17. package/src/resolvers/valueFromGit.js +43 -17
  18. package/src/resolvers/valueFromOptions.js +5 -4
  19. package/src/utils/filters/filterArgs.js +57 -0
  20. package/src/utils/filters/oneOf.js +77 -0
  21. package/src/utils/introspection/audit.js +78 -0
  22. package/src/utils/introspection/graph.js +43 -0
  23. package/src/utils/introspection/model.js +150 -0
  24. package/src/utils/introspection/model.test.js +93 -0
  25. package/src/utils/parsing/commentAnnotations.js +107 -0
  26. package/src/utils/parsing/commentAnnotations.test.js +123 -0
  27. package/src/utils/parsing/enrichMetadata.js +64 -1
  28. package/src/utils/parsing/enrichMetadata.test.js +84 -0
  29. package/src/utils/parsing/extractComment.js +145 -0
  30. package/src/utils/parsing/extractComment.test.js +182 -0
  31. package/src/utils/parsing/preProcess.js +2 -1
  32. package/src/utils/paths/findLineForKey.js +2 -2
  33. package/src/utils/paths/ignorePaths.js +22 -9
  34. package/src/utils/redaction/redact.js +78 -0
  35. package/src/utils/redaction/redact.test.js +38 -0
  36. package/src/utils/redaction/setupRedaction.js +47 -0
  37. package/src/utils/redaction/setupRedaction.test.js +68 -0
  38. package/src/utils/requirements/configRequirements.js +351 -0
  39. package/src/utils/requirements/configRequirements.test.js +380 -0
  40. package/src/utils/requirements/serializeRequirements.js +120 -0
  41. package/src/utils/requirements/serializeRequirements.test.js +211 -0
  42. package/src/utils/security/evalSafety.js +86 -0
  43. package/src/utils/security/evalSafety.test.js +61 -0
  44. package/src/utils/security/safetyPolicy.js +110 -0
  45. package/src/utils/security/safetyPolicy.test.js +29 -0
  46. package/src/utils/strings/didYouMean.js +70 -0
  47. package/src/utils/strings/didYouMean.test.js +52 -0
  48. package/src/utils/strings/formatFunctionArgs.js +6 -1
  49. package/src/utils/strings/splitByComma.js +5 -0
  50. package/src/utils/ui/configWizard.js +208 -34
  51. package/src/utils/ui/createEditorLink.js +17 -1
  52. package/src/utils/ui/promptDescriptors.js +196 -0
  53. package/src/utils/ui/promptDescriptors.test.js +162 -0
  54. package/src/utils/variables/cleanVariable.js +22 -0
  55. package/src/utils/variables/getVariableType.js +1 -0
  56. package/types/src/index.d.ts +0 -24
  57. package/types/src/index.d.ts.map +1 -1
  58. package/types/src/main.d.ts +16 -8
  59. package/types/src/main.d.ts.map +1 -1
  60. package/types/src/resolvers/valueFromFile.d.ts +0 -2
  61. package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
  62. package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
  63. package/types/src/resolvers/valueFromSelf.d.ts +1 -0
  64. package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
  65. package/types/src/utils/parsing/parse.d.ts.map +1 -1
  66. package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
  67. package/types/src/utils/paths/findLineForKey.d.ts +0 -9
  68. package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
  69. package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
  70. package/types/src/utils/variables/variableUtils.d.ts +1 -1
  71. package/types/src/display.d.ts +0 -62
  72. package/types/src/display.d.ts.map +0 -1
  73. package/types/src/metadata.d.ts +0 -28
  74. package/types/src/metadata.d.ts.map +0 -1
  75. package/types/src/utils/BoundedMap.d.ts +0 -10
  76. package/types/src/utils/BoundedMap.d.ts.map +0 -1
  77. package/types/src/utils/paths/ignorePaths.d.ts +0 -5
  78. 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, or js (default: json)
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
- for (const arg of argv._) {
136
- if (arg === 'setup') continue
137
-
138
- // jq-style paths start with '.' or '['
139
- if (!inputFile && isFileArg(arg)) {
140
- inputFile = arg
141
- } else if (arg.startsWith('.') || arg.startsWith('[')) {
142
- extractPath = arg
143
- } else if (!inputFile) {
144
- inputFile = arg
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
- console.error('Error: No input file specified')
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
- console.error(`Error: File not found: ${inputFile}`)
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
- 'allow-unknown': allowUnknown,
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
- configorama(inputFile, options)
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
- config = getValueAtPath(config, extractPath)
226
- if (config === undefined) {
227
- console.error(`Error: Path not found: ${extractPath}`)
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 && (config === null || ['string', 'number', 'boolean'].includes(typeof config))) {
236
- output = config === null ? 'null' : String(config)
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(config)
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(config, null, 2)}`
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(config, null, 2)}`
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
- config.variableSyntax = config.variableSyntax ? config.variableSyntax.source : undefined
503
+ outputConfig.variableSyntax = outputConfig.variableSyntax ? outputConfig.variableSyntax.source : undefined
260
504
  }
261
- output = JSON.stringify(config, null, 2)
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((error) => {
287
- const errorMsg = makeBox({
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
- /** Analyze config variables without resolving them */
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.11.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()