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
@@ -0,0 +1,380 @@
1
+ /* eslint-disable no-template-curly-in-string */
2
+ const { test } = require('uvu')
3
+ const assert = require('uvu/assert')
4
+ const configorama = require('../../index')
5
+ const {
6
+ buildConfigRequirements,
7
+ cleanDefaultValue,
8
+ normalizeType,
9
+ normalizeVariableType,
10
+ } = require('./configRequirements')
11
+
12
+ test('normalizeVariableType maps option resolver variants to option', () => {
13
+ assert.is(normalizeVariableType('options'), 'option')
14
+ assert.is(normalizeVariableType('opt'), 'option')
15
+ assert.is(normalizeVariableType('option'), 'option')
16
+ assert.is(normalizeVariableType('dot.prop'), 'dotProp')
17
+ })
18
+
19
+ test('normalizeType defaults to string and lowers known type filters', () => {
20
+ assert.is(normalizeType(), 'string')
21
+ assert.is(normalizeType('String'), 'string')
22
+ assert.is(normalizeType('Number'), 'number')
23
+ assert.is(normalizeType('Boolean'), 'boolean')
24
+ assert.is(normalizeType('Json'), 'json')
25
+ assert.is(normalizeType('Array'), 'array')
26
+ assert.is(normalizeType('Object'), 'object')
27
+ })
28
+
29
+ test('cleanDefaultValue strips only matching surrounding quotes', () => {
30
+ assert.is(cleanDefaultValue('"fallback-value"'), 'fallback-value')
31
+ assert.is(cleanDefaultValue("'fallback-value'"), 'fallback-value')
32
+ assert.is(cleanDefaultValue('5432'), '5432')
33
+ assert.is(cleanDefaultValue(5432), 5432)
34
+ assert.is(cleanDefaultValue(undefined), null)
35
+ })
36
+
37
+ test('buildConfigRequirements normalizes analyze uniqueVariables', async () => {
38
+ const analysis = await configorama.analyze({
39
+ apiKey: '${env:API_KEY | String | help("API key secret")}',
40
+ port: '${env:DB_PORT, 5432 | Number | help("Database port")}',
41
+ stage: '${opt:stage | help("Deployment stage")}',
42
+ otherStage: '${opt:stage, "dev"}',
43
+ selfRef: '${self:serviceName}',
44
+ serviceName: 'demo',
45
+ dotRef: '${missing.path | Boolean}',
46
+ }, {
47
+ options: {}
48
+ })
49
+
50
+ const requirements = buildConfigRequirements(analysis)
51
+ const byVariable = Object.fromEntries(requirements.map(req => [req.variable, req]))
52
+
53
+ assert.is(byVariable['env:API_KEY'].name, 'API_KEY')
54
+ assert.is(byVariable['env:API_KEY'].variableType, 'env')
55
+ assert.is(byVariable['env:API_KEY'].sourceClass, 'user')
56
+ assert.is(byVariable['env:API_KEY'].type, 'string')
57
+ assert.is(byVariable['env:API_KEY'].description, 'API key secret')
58
+ assert.is(byVariable['env:API_KEY'].descriptionSource, 'help')
59
+ assert.is(byVariable['env:API_KEY'].sensitive, true)
60
+ assert.is(byVariable['env:API_KEY'].required, true)
61
+ assert.equal(byVariable['env:API_KEY'].paths, ['apiKey'])
62
+
63
+ assert.is(byVariable['env:DB_PORT'].type, 'number')
64
+ assert.is(byVariable['env:DB_PORT'].default, '5432')
65
+ assert.is(byVariable['env:DB_PORT'].required, false)
66
+
67
+ assert.is(byVariable['opt:stage'].name, 'stage')
68
+ assert.is(byVariable['opt:stage'].variableType, 'option')
69
+ assert.is(byVariable['opt:stage'].sourceClass, 'user')
70
+ assert.equal(byVariable['opt:stage'].paths, ['stage', 'otherStage'])
71
+ assert.is(byVariable['opt:stage'].default, 'dev')
72
+
73
+ assert.is(byVariable['self:serviceName'].name, 'serviceName')
74
+ assert.is(byVariable['self:serviceName'].variableType, 'self')
75
+ assert.is(byVariable['self:serviceName'].sourceClass, 'config')
76
+ assert.is(byVariable['self:serviceName'].default, 'demo')
77
+
78
+ assert.is(byVariable['missing.path'].variableType, 'dotProp')
79
+ assert.is(byVariable['missing.path'].type, 'boolean')
80
+ })
81
+
82
+ test('buildConfigRequirements carries file variables and paths', () => {
83
+ const requirements = buildConfigRequirements({
84
+ uniqueVariables: {
85
+ 'file(./missing.yml)': {
86
+ variable: 'file(./missing.yml)',
87
+ variableType: 'file',
88
+ variableSourceType: 'config',
89
+ fileExists: false,
90
+ occurrences: [
91
+ {
92
+ path: 'config',
93
+ isRequired: true,
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ })
99
+
100
+ assert.is(requirements.length, 1)
101
+ assert.is(requirements[0].name, './missing.yml')
102
+ assert.is(requirements[0].variableType, 'file')
103
+ assert.is(requirements[0].sourceClass, 'config')
104
+ assert.equal(requirements[0].paths, ['config'])
105
+ assert.is(requirements[0].type, 'string')
106
+ assert.is(requirements[0].required, true)
107
+ })
108
+
109
+ test('buildConfigRequirements leaves identical annotations conflict-free', () => {
110
+ const [requirement] = buildConfigRequirements({
111
+ uniqueVariables: {
112
+ 'opt:stage': {
113
+ variable: 'opt:stage',
114
+ variableType: 'options',
115
+ variableSourceType: 'user',
116
+ occurrences: [
117
+ {
118
+ path: 'stage',
119
+ type: 'String',
120
+ defaultValue: '"dev"',
121
+ allowedValues: ['dev', 'prod'],
122
+ isRequired: false,
123
+ },
124
+ {
125
+ path: 'provider.stage',
126
+ type: 'String',
127
+ defaultValue: 'dev',
128
+ allowedValues: ['prod', 'dev'],
129
+ isRequired: false,
130
+ }
131
+ ]
132
+ }
133
+ }
134
+ })
135
+
136
+ assert.equal(requirement.conflicts, [])
137
+ assert.is(requirement.type, 'string')
138
+ assert.is(requirement.default, 'dev')
139
+ assert.equal(requirement.allowedValues, ['dev', 'prod'])
140
+ })
141
+
142
+ test('buildConfigRequirements records type/default/allowedValues conflicts', () => {
143
+ const [requirement] = buildConfigRequirements({
144
+ uniqueVariables: {
145
+ 'opt:stage': {
146
+ variable: 'opt:stage',
147
+ variableType: 'options',
148
+ variableSourceType: 'user',
149
+ occurrences: [
150
+ {
151
+ path: 'stage',
152
+ type: 'String',
153
+ defaultValue: 'dev',
154
+ allowedValues: ['dev', 'prod'],
155
+ isRequired: false,
156
+ },
157
+ {
158
+ path: 'provider.stage',
159
+ type: 'Number',
160
+ defaultValue: '1',
161
+ allowedValues: ['1', '2'],
162
+ isRequired: false,
163
+ }
164
+ ]
165
+ }
166
+ }
167
+ })
168
+
169
+ const byField = Object.fromEntries(requirement.conflicts.map(conflict => [conflict.field, conflict]))
170
+ assert.ok(byField.type)
171
+ assert.equal(byField.type.paths, ['stage', 'provider.stage'])
172
+ assert.equal(byField.type.values.map(item => item.value), ['string', 'number'])
173
+
174
+ assert.ok(byField.default)
175
+ assert.equal(byField.default.paths, ['stage', 'provider.stage'])
176
+ assert.equal(byField.default.values.map(item => item.value), ['dev', '1'])
177
+
178
+ assert.ok(byField.allowedValues)
179
+ assert.equal(byField.allowedValues.paths, ['stage', 'provider.stage'])
180
+ assert.equal(byField.allowedValues.values.map(item => item.value), [['dev', 'prod'], ['1', '2']])
181
+ })
182
+
183
+ test('buildConfigRequirements selects descriptions by precedence without conflicts', () => {
184
+ const [requirement] = buildConfigRequirements({
185
+ uniqueVariables: {
186
+ 'env:API_KEY': {
187
+ variable: 'env:API_KEY',
188
+ variableType: 'env',
189
+ variableSourceType: 'user',
190
+ occurrences: [
191
+ {
192
+ path: 'apiKey',
193
+ description: 'Comment description',
194
+ descriptionSource: 'comment',
195
+ isRequired: true,
196
+ },
197
+ {
198
+ path: 'apiKey',
199
+ description: 'Help description',
200
+ descriptionSource: 'help',
201
+ isRequired: true,
202
+ }
203
+ ]
204
+ }
205
+ }
206
+ })
207
+
208
+ assert.is(requirement.description, 'Help description')
209
+ assert.is(requirement.descriptionSource, 'help')
210
+ assert.equal(requirement.conflicts, [])
211
+ assert.is(requirement.sensitive, true)
212
+ })
213
+
214
+ test('buildConfigRequirements gives @description commentTag priority over help', () => {
215
+ const [requirement] = buildConfigRequirements({
216
+ uniqueVariables: {
217
+ 'env:API_KEY': {
218
+ variable: 'env:API_KEY',
219
+ variableType: 'env',
220
+ variableSourceType: 'user',
221
+ occurrences: [
222
+ {
223
+ path: 'apiKey',
224
+ description: 'Help description',
225
+ descriptionSource: 'help',
226
+ isRequired: true,
227
+ },
228
+ {
229
+ path: 'apiKey',
230
+ description: 'Comment tag description',
231
+ descriptionSource: 'commentTag',
232
+ isRequired: true,
233
+ }
234
+ ]
235
+ }
236
+ }
237
+ })
238
+
239
+ assert.is(requirement.description, 'Comment tag description')
240
+ assert.is(requirement.descriptionSource, 'commentTag')
241
+ })
242
+
243
+ test('buildConfigRequirements merges annotation fields and unique examples', () => {
244
+ const [requirement] = buildConfigRequirements({
245
+ uniqueVariables: {
246
+ 'env:STRIPE_SECRET_KEY': {
247
+ variable: 'env:STRIPE_SECRET_KEY',
248
+ variableType: 'env',
249
+ variableSourceType: 'user',
250
+ occurrences: [
251
+ {
252
+ path: 'secrets.stripeSecret',
253
+ obtainHint: 'Stripe dashboard > Developers > API keys',
254
+ examples: ['sk_live_...', 'stripe-key'],
255
+ defaultHint: 'Set in CI',
256
+ group: 'Payments',
257
+ deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
258
+ sensitive: false,
259
+ sensitiveSource: 'commentTag',
260
+ isRequired: true,
261
+ },
262
+ {
263
+ path: 'env.STRIPE_SECRET_KEY',
264
+ obtainHint: 'Stripe dashboard > Developers > API keys',
265
+ examples: ['sk_live_...'],
266
+ defaultHint: 'Set in CI',
267
+ group: 'Payments',
268
+ deprecationMessage: 'Use STRIPE_RESTRICTED_KEY instead',
269
+ sensitive: false,
270
+ sensitiveSource: 'commentTag',
271
+ isRequired: true,
272
+ }
273
+ ]
274
+ }
275
+ }
276
+ })
277
+
278
+ assert.is(requirement.obtainHint, 'Stripe dashboard > Developers > API keys')
279
+ assert.equal(requirement.examples, ['sk_live_...', 'stripe-key'])
280
+ assert.is(requirement.defaultHint, 'Set in CI')
281
+ assert.is(requirement.group, 'Payments')
282
+ assert.is(requirement.deprecationMessage, 'Use STRIPE_RESTRICTED_KEY instead')
283
+ assert.is(requirement.sensitive, false)
284
+ assert.is(requirement.sensitiveSource, 'commentTag')
285
+ assert.equal(requirement.conflicts, [])
286
+ })
287
+
288
+ test('buildConfigRequirements records scalar annotation conflicts', () => {
289
+ const [requirement] = buildConfigRequirements({
290
+ uniqueVariables: {
291
+ 'env:PAYMENT_TOKEN': {
292
+ variable: 'env:PAYMENT_TOKEN',
293
+ variableType: 'env',
294
+ variableSourceType: 'user',
295
+ occurrences: [
296
+ {
297
+ path: 'stripe.token',
298
+ obtainHint: 'Stripe dashboard',
299
+ defaultHint: 'Set in CI',
300
+ group: 'Payments',
301
+ deprecationMessage: 'Use restricted key',
302
+ sensitive: true,
303
+ isRequired: true,
304
+ },
305
+ {
306
+ path: 'github.token',
307
+ obtainHint: 'GitHub settings',
308
+ defaultHint: 'Set locally',
309
+ group: 'Source Control',
310
+ deprecationMessage: 'Use fine-grained token',
311
+ sensitive: false,
312
+ isRequired: true,
313
+ }
314
+ ]
315
+ }
316
+ }
317
+ })
318
+
319
+ const byField = Object.fromEntries(requirement.conflicts.map(conflict => [conflict.field, conflict]))
320
+ assert.equal(byField.obtainHint.paths, ['stripe.token', 'github.token'])
321
+ assert.equal(byField.obtainHint.values.map(item => item.value), ['Stripe dashboard', 'GitHub settings'])
322
+ assert.equal(byField.defaultHint.values.map(item => item.value), ['Set in CI', 'Set locally'])
323
+ assert.equal(byField.group.values.map(item => item.value), ['Payments', 'Source Control'])
324
+ assert.equal(byField.deprecationMessage.values.map(item => item.value), ['Use restricted key', 'Use fine-grained token'])
325
+ assert.equal(byField.sensitive.values.map(item => item.value), [true, false])
326
+ })
327
+
328
+ test('buildConfigRequirements applies sensitive overrides and falls back to name heuristic when absent', () => {
329
+ const requirements = buildConfigRequirements({
330
+ uniqueVariables: {
331
+ 'env:API_KEY': {
332
+ variable: 'env:API_KEY',
333
+ variableType: 'env',
334
+ variableSourceType: 'user',
335
+ occurrences: [
336
+ {
337
+ path: 'apiKey',
338
+ sensitive: false,
339
+ sensitiveSource: 'commentTag',
340
+ isRequired: true,
341
+ }
342
+ ]
343
+ },
344
+ 'env:PUBLIC_VALUE': {
345
+ variable: 'env:PUBLIC_VALUE',
346
+ variableType: 'env',
347
+ variableSourceType: 'user',
348
+ occurrences: [
349
+ {
350
+ path: 'publicValue',
351
+ sensitive: true,
352
+ sensitiveSource: 'commentTag',
353
+ isRequired: true,
354
+ }
355
+ ]
356
+ },
357
+ 'env:AUTH_TOKEN': {
358
+ variable: 'env:AUTH_TOKEN',
359
+ variableType: 'env',
360
+ variableSourceType: 'user',
361
+ occurrences: [
362
+ {
363
+ path: 'authToken',
364
+ isRequired: true,
365
+ }
366
+ ]
367
+ }
368
+ }
369
+ })
370
+ const byVariable = Object.fromEntries(requirements.map(req => [req.variable, req]))
371
+
372
+ assert.is(byVariable['env:API_KEY'].sensitive, false)
373
+ assert.is(byVariable['env:API_KEY'].sensitiveSource, 'commentTag')
374
+ assert.is(byVariable['env:PUBLIC_VALUE'].sensitive, true)
375
+ assert.is(byVariable['env:PUBLIC_VALUE'].sensitiveSource, 'commentTag')
376
+ assert.is(byVariable['env:AUTH_TOKEN'].sensitive, true)
377
+ assert.is(byVariable['env:AUTH_TOKEN'].sensitiveSource, null)
378
+ })
379
+
380
+ test.run()
@@ -0,0 +1,120 @@
1
+ const { buildConfigRequirements } = require('./configRequirements')
2
+
3
+ const READONLY_VARIABLE_TYPES = new Set([
4
+ 'cron',
5
+ 'eval',
6
+ 'git',
7
+ 'self',
8
+ 'dotProp',
9
+ ])
10
+
11
+ function getConfigIdentity(analysis, configPathOrObject) {
12
+ if (typeof configPathOrObject === 'string') return configPathOrObject
13
+ if (analysis && analysis.configFilePath) return analysis.configFilePath
14
+ return null
15
+ }
16
+
17
+ function getSummary(requirements) {
18
+ return {
19
+ total: requirements.length,
20
+ required: requirements.filter(req => req.required).length,
21
+ optional: requirements.filter(req => !req.required).length,
22
+ sensitive: requirements.filter(req => req.sensitive).length,
23
+ }
24
+ }
25
+
26
+ function isDynamicFileRequirement(requirement) {
27
+ const target = requirement.name || requirement.variable || ''
28
+ if (target.includes('${')) return true
29
+ return (requirement.innerVariables || []).length > 0
30
+ }
31
+
32
+ function isMissingConcreteFileRequirement(requirement) {
33
+ if (requirement.variableType !== 'file' && requirement.variableType !== 'text') return false
34
+ if (isDynamicFileRequirement(requirement)) return false
35
+ return requirement.fileExists !== true
36
+ }
37
+
38
+ function getHow(requirement) {
39
+ switch (requirement.variableType) {
40
+ case 'env':
41
+ return `Set environment variable ${requirement.name}`
42
+ case 'option':
43
+ return `Pass --${requirement.name} on the CLI`
44
+ case 'param':
45
+ return `Pass --param ${requirement.name}=<value>`
46
+ case 'file':
47
+ return `Provide file at path ${requirement.name}`
48
+ case 'text':
49
+ return `Provide text file at path ${requirement.name}`
50
+ default:
51
+ return null
52
+ }
53
+ }
54
+
55
+ function shouldAsk(requirement) {
56
+ if (isMissingConcreteFileRequirement(requirement)) return true
57
+ if (READONLY_VARIABLE_TYPES.has(requirement.variableType)) return false
58
+ return Boolean(
59
+ requirement.required &&
60
+ requirement.default === null &&
61
+ requirement.sourceClass === 'user'
62
+ )
63
+ }
64
+
65
+ function toAskItem(requirement) {
66
+ return {
67
+ name: requirement.name,
68
+ variable: requirement.variable,
69
+ variableType: requirement.variableType,
70
+ type: requirement.type,
71
+ sensitive: requirement.sensitive,
72
+ description: requirement.description,
73
+ obtainHint: requirement.obtainHint,
74
+ examples: requirement.examples,
75
+ defaultHint: requirement.defaultHint,
76
+ group: requirement.group,
77
+ deprecationMessage: requirement.deprecationMessage,
78
+ paths: requirement.paths,
79
+ how: getHow(requirement),
80
+ }
81
+ }
82
+
83
+ function formatConflictError(requirements) {
84
+ const conflicted = requirements.filter(req => req.conflicts && req.conflicts.length)
85
+ if (!conflicted.length) return null
86
+
87
+ const details = conflicted.flatMap(req => {
88
+ return req.conflicts.map(conflict => {
89
+ const paths = conflict.paths && conflict.paths.length ? conflict.paths.join(', ') : 'unknown path'
90
+ const values = conflict.values.map(value => JSON.stringify(value.value)).join(', ')
91
+ return `${req.variable} ${conflict.field} conflict at ${paths}: ${values}`
92
+ })
93
+ })
94
+
95
+ return new Error(`Config requirements contain conflicting annotations:\n${details.join('\n')}`)
96
+ }
97
+
98
+ function serializeRequirements(analysis, options = {}) {
99
+ const requirements = buildConfigRequirements(analysis)
100
+ const conflictError = formatConflictError(requirements)
101
+ if (conflictError) throw conflictError
102
+
103
+ return {
104
+ schemaVersion: 1,
105
+ config: getConfigIdentity(analysis, options.configPathOrObject),
106
+ summary: getSummary(requirements),
107
+ requirements,
108
+ ask: requirements
109
+ .filter(shouldAsk)
110
+ .map(toAskItem),
111
+ }
112
+ }
113
+
114
+ module.exports = {
115
+ getHow,
116
+ getSummary,
117
+ isMissingConcreteFileRequirement,
118
+ serializeRequirements,
119
+ shouldAsk,
120
+ }