configorama 0.6.9 → 0.6.10
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/cli.js +57 -28
- package/package.json +4 -2
- package/src/index.js +39 -2
- package/src/main.js +611 -269
- package/src/resolvers/valueFromCron.js +2 -0
- package/src/resolvers/valueFromEnv.js +2 -0
- package/src/resolvers/valueFromEnv.test.js +78 -0
- package/src/resolvers/valueFromEval.js +1 -0
- package/src/resolvers/valueFromGit.js +24 -9
- package/src/resolvers/valueFromNumber.js +1 -0
- package/src/resolvers/valueFromOptions.js +2 -0
- package/src/resolvers/valueFromString.js +1 -0
- package/src/sync.js +13 -4
- package/src/utils/cleanVariable.js +3 -3
- package/src/utils/configWizard.js +567 -0
- package/src/utils/encoders/index.js +15 -0
- package/src/utils/encoders/js-fixes.js +22 -0
- package/src/utils/{unknownValues.js → encoders/unknown-values.js} +10 -1
- package/src/utils/enrichMetadata.js +439 -82
- package/src/utils/find-nested-variables.js +41 -38
- package/src/utils/find-nested-variables.test.js +119 -35
- package/src/utils/getFullFilePath.js +38 -0
- package/src/utils/getVariableType.js +55 -0
- package/src/utils/logs.js +1 -1
- package/src/utils/parse.js +6 -4
- package/src/utils/resolveAlias.js +3 -2
- package/src/utils/splitByComma.js +2 -1
- package/src/utils/splitCsv.js +6 -6
- package/src/utils/resolveAliasOld.js +0 -65
- package/src/utils/x.js +0 -173
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
// Wizard to prompt user through config setup
|
|
2
|
+
const p = require('@clack/prompts')
|
|
3
|
+
const chalk = require('./chalk')
|
|
4
|
+
const dotProp = require('dot-prop')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Groups variables by type for wizard flow
|
|
10
|
+
* @param {object} uniqueVariables - The uniqueVariables from enriched metadata
|
|
11
|
+
* @param {object} originalConfig - The original config before resolution
|
|
12
|
+
* @returns {object} Grouped variables by type
|
|
13
|
+
*/
|
|
14
|
+
function groupVariablesByType(uniqueVariables, originalConfig = {}) {
|
|
15
|
+
const grouped = {
|
|
16
|
+
options: [],
|
|
17
|
+
env: [],
|
|
18
|
+
self: [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Track variables we've already added to avoid duplicates
|
|
22
|
+
const addedVars = new Set()
|
|
23
|
+
|
|
24
|
+
for (const [varKey, varData] of Object.entries(uniqueVariables)) {
|
|
25
|
+
const { variable, variableType, isRequired, defaultValue, defaultValueSrc, occurrences, innerVariables, hasValue } = varData
|
|
26
|
+
|
|
27
|
+
// Handle top-level variables (not file/text types)
|
|
28
|
+
if (variableType !== 'file' && variableType !== 'text') {
|
|
29
|
+
// Skip if has a value (for self refs and dot.prop)
|
|
30
|
+
if (hasValue === true) {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// For self/dot.prop refs, skip if they have a defaultValueSrc (means they have a static value in config)
|
|
35
|
+
if ((variableType === 'self' || variableType === 'dot.prop') && defaultValueSrc) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// For self/dot.prop refs, skip if ANY occurrence has a fallback
|
|
40
|
+
// This means the variable has a fallback chain and will resolve naturally
|
|
41
|
+
if (variableType === 'self' || variableType === 'dot.prop') {
|
|
42
|
+
const hasAnyFallback = occurrences.some(occ => occ.hasFallback)
|
|
43
|
+
if (hasAnyFallback) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Skip if this variable only appears as part of another variable's fallback chain
|
|
48
|
+
// (i.e., appears in 'value' field but not as 'originalString')
|
|
49
|
+
const isOnlyFallback = occurrences.every(occ => {
|
|
50
|
+
// If it has originalString, it's a top-level variable
|
|
51
|
+
if (occ.originalString) return false
|
|
52
|
+
// If it appears in a 'value' field, it's part of a fallback chain
|
|
53
|
+
if (occ.value && occ.value.includes(',')) return true
|
|
54
|
+
return false
|
|
55
|
+
})
|
|
56
|
+
if (isOnlyFallback) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Skip if the variable path itself contains unresolved variables
|
|
61
|
+
// e.g., self:stages.${opt:stage}.SECRET - can't set this in config because path is dynamic
|
|
62
|
+
const cleanPath = variable.replace(/^self:/, '')
|
|
63
|
+
if (cleanPath.includes('${')) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Also check if value exists in original config
|
|
68
|
+
const configValue = dotProp.get(originalConfig, cleanPath)
|
|
69
|
+
if (configValue !== undefined) {
|
|
70
|
+
// Value exists in config, skip prompting
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cleanName = variable.replace(/^(opt|env|self):/, '')
|
|
76
|
+
|
|
77
|
+
if (!addedVars.has(variable)) {
|
|
78
|
+
addedVars.add(variable)
|
|
79
|
+
|
|
80
|
+
// Check if ANY occurrence is required without fallback
|
|
81
|
+
const hasRequiredOccurrence = occurrences.some(occ => occ.isRequired && !occ.hasFallback)
|
|
82
|
+
|
|
83
|
+
// Check if ANY occurrence has a default/fallback value
|
|
84
|
+
const fallbackOccurrence = occurrences.find(occ => occ.hasFallback && occ.defaultValue)
|
|
85
|
+
const availableDefault = fallbackOccurrence?.defaultValue || defaultValue
|
|
86
|
+
|
|
87
|
+
const varInfo = {
|
|
88
|
+
key: varKey,
|
|
89
|
+
variable,
|
|
90
|
+
cleanName,
|
|
91
|
+
variableType,
|
|
92
|
+
isRequired: hasRequiredOccurrence,
|
|
93
|
+
defaultValue: availableDefault,
|
|
94
|
+
hasFallback: !!availableDefault,
|
|
95
|
+
occurrences: occurrences || [],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (variableType === 'options') {
|
|
99
|
+
grouped.options.push(varInfo)
|
|
100
|
+
} else if (variableType === 'env') {
|
|
101
|
+
grouped.env.push(varInfo)
|
|
102
|
+
} else if (variableType === 'self') {
|
|
103
|
+
grouped.self.push(varInfo)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Extract inner variables from file/text references
|
|
109
|
+
if (innerVariables && Array.isArray(innerVariables)) {
|
|
110
|
+
for (const innerVar of innerVariables) {
|
|
111
|
+
// Skip if already has a value
|
|
112
|
+
if (innerVar.hasValue) {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Skip if not required
|
|
117
|
+
if (!innerVar.isRequired) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cleanName = innerVar.variable.replace(/^(opt|env|self):/, '')
|
|
122
|
+
|
|
123
|
+
// If already added, append this occurrence to existing variable
|
|
124
|
+
const existingVarIndex = grouped[innerVar.variableType === 'options' ? 'options' : innerVar.variableType === 'env' ? 'env' : 'self']
|
|
125
|
+
.findIndex(v => v.variable === innerVar.variable)
|
|
126
|
+
|
|
127
|
+
if (existingVarIndex >= 0) {
|
|
128
|
+
// Add this occurrence to the existing variable
|
|
129
|
+
const varList = innerVar.variableType === 'options' ? grouped.options : innerVar.variableType === 'env' ? grouped.env : grouped.self
|
|
130
|
+
const existingVar = varList[existingVarIndex]
|
|
131
|
+
|
|
132
|
+
// Add occurrence from parent file variable
|
|
133
|
+
if (occurrences && occurrences.length > 0) {
|
|
134
|
+
existingVar.occurrences.push(...occurrences)
|
|
135
|
+
}
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Skip if already added to the set
|
|
140
|
+
if (addedVars.has(innerVar.variable)) {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
addedVars.add(innerVar.variable)
|
|
145
|
+
|
|
146
|
+
const varInfo = {
|
|
147
|
+
key: innerVar.variable,
|
|
148
|
+
variable: innerVar.variable,
|
|
149
|
+
cleanName,
|
|
150
|
+
variableType: innerVar.variableType,
|
|
151
|
+
isRequired: innerVar.isRequired,
|
|
152
|
+
defaultValue: innerVar.defaultValue,
|
|
153
|
+
occurrences: occurrences ? [...occurrences] : [], // Use parent file variable occurrences
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (innerVar.variableType === 'options') {
|
|
157
|
+
grouped.options.push(varInfo)
|
|
158
|
+
} else if (innerVar.variableType === 'env') {
|
|
159
|
+
grouped.env.push(varInfo)
|
|
160
|
+
} else if (innerVar.variableType === 'self') {
|
|
161
|
+
grouped.self.push(varInfo)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return grouped
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detects if a variable name suggests sensitive data
|
|
172
|
+
* @param {string} name - Variable name
|
|
173
|
+
* @returns {boolean} True if likely sensitive
|
|
174
|
+
*/
|
|
175
|
+
function isSensitiveVariable(name) {
|
|
176
|
+
const sensitivePatterns = [
|
|
177
|
+
/secret/i,
|
|
178
|
+
/password/i,
|
|
179
|
+
/token/i,
|
|
180
|
+
/key/i,
|
|
181
|
+
/credential/i,
|
|
182
|
+
/auth/i,
|
|
183
|
+
]
|
|
184
|
+
return sensitivePatterns.some(pattern => pattern.test(name))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validates input value based on expected type
|
|
189
|
+
* @param {string} value - Input value
|
|
190
|
+
* @param {string} expectedType - Expected type (String, Number, Boolean, Json)
|
|
191
|
+
* @returns {string|undefined} Error message if invalid, undefined if valid
|
|
192
|
+
*/
|
|
193
|
+
function validateType(value, expectedType) {
|
|
194
|
+
if (!expectedType || !value) return
|
|
195
|
+
|
|
196
|
+
switch (expectedType) {
|
|
197
|
+
case 'Boolean':
|
|
198
|
+
const lowerVal = value.toLowerCase()
|
|
199
|
+
const validBooleans = ['true', 'false', 'yes', 'no', 'on', 'off', '1', '0']
|
|
200
|
+
if (!validBooleans.includes(lowerVal)) {
|
|
201
|
+
return `Must be a boolean value (true/false, yes/no, on/off, 1/0)`
|
|
202
|
+
}
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
case 'Number':
|
|
206
|
+
if (isNaN(Number(value))) {
|
|
207
|
+
return `Must be a valid number`
|
|
208
|
+
}
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
case 'Json':
|
|
212
|
+
try {
|
|
213
|
+
JSON.parse(value)
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return `Must be valid JSON`
|
|
216
|
+
}
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
case 'String':
|
|
220
|
+
// String is always valid
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extracts type from variable occurrences
|
|
227
|
+
* @param {Array} occurrences - Variable occurrences
|
|
228
|
+
* @returns {string|null} Expected type or null
|
|
229
|
+
*/
|
|
230
|
+
function getExpectedType(occurrences) {
|
|
231
|
+
if (!occurrences || occurrences.length === 0) return null
|
|
232
|
+
|
|
233
|
+
for (const occ of occurrences) {
|
|
234
|
+
if (occ.filters && Array.isArray(occ.filters)) {
|
|
235
|
+
for (const filter of occ.filters) {
|
|
236
|
+
// Check if filter starts with uppercase letter
|
|
237
|
+
if (filter && typeof filter === 'string' && /^[A-Z]/.test(filter)) {
|
|
238
|
+
return filter
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extracts help text from variable occurrences
|
|
248
|
+
* @param {Array} occurrences - Variable occurrences
|
|
249
|
+
* @returns {string|null} Help text or null
|
|
250
|
+
*/
|
|
251
|
+
function getHelpText(occurrences) {
|
|
252
|
+
if (!occurrences || occurrences.length === 0) return null
|
|
253
|
+
|
|
254
|
+
for (const occ of occurrences) {
|
|
255
|
+
// Check for description field first (preferred)
|
|
256
|
+
if (occ.description) {
|
|
257
|
+
return occ.description
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Fallback to checking filters array (for backwards compatibility)
|
|
261
|
+
if (occ.filters && Array.isArray(occ.filters)) {
|
|
262
|
+
for (const filter of occ.filters) {
|
|
263
|
+
// Check if filter has help() syntax
|
|
264
|
+
const helpMatch = filter.match(/^help\(['"](.+)['"]\)$/)
|
|
265
|
+
if (helpMatch) {
|
|
266
|
+
return helpMatch[1]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return null
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Creates a human-readable prompt message
|
|
276
|
+
* @param {object} varInfo - Variable info
|
|
277
|
+
* @returns {string} Prompt message
|
|
278
|
+
*/
|
|
279
|
+
function createPromptMessage(varInfo) {
|
|
280
|
+
const { cleanName, variableType, occurrences } = varInfo
|
|
281
|
+
|
|
282
|
+
let typeLabel
|
|
283
|
+
if (variableType === 'options') {
|
|
284
|
+
typeLabel = 'Flag'
|
|
285
|
+
} else if (variableType === 'env') {
|
|
286
|
+
typeLabel = 'Env'
|
|
287
|
+
} else if (variableType === 'self') {
|
|
288
|
+
typeLabel = 'Config'
|
|
289
|
+
} else {
|
|
290
|
+
typeLabel = 'Value'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check for type from filters
|
|
294
|
+
const expectedType = getExpectedType(occurrences)
|
|
295
|
+
|
|
296
|
+
// Append type to label if found
|
|
297
|
+
if (expectedType) {
|
|
298
|
+
typeLabel = `${typeLabel}:${expectedType}`
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Collect all unique descriptions from occurrences
|
|
302
|
+
const descriptions = []
|
|
303
|
+
if (occurrences && occurrences.length > 0) {
|
|
304
|
+
occurrences.forEach(occ => {
|
|
305
|
+
if (occ.description && !descriptions.includes(occ.description)) {
|
|
306
|
+
descriptions.push(occ.description)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Build context from all occurrences
|
|
312
|
+
let contextHint = ''
|
|
313
|
+
|
|
314
|
+
// Show combined descriptions if available
|
|
315
|
+
if (descriptions.length > 0) {
|
|
316
|
+
contextHint = ` - ${descriptions.join('. ')}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Show usage list if there are occurrences
|
|
320
|
+
if (occurrences && occurrences.length > 0) {
|
|
321
|
+
// Parse occurrences into key-value pairs with descriptions
|
|
322
|
+
const parsedOccurrences = occurrences.map(occ => {
|
|
323
|
+
const keyPath = occ.path
|
|
324
|
+
let originalValue = occ.value || occ.originalString || occ.varMatch
|
|
325
|
+
|
|
326
|
+
// Strip help() filter from the displayed value
|
|
327
|
+
if (originalValue && typeof originalValue === 'string') {
|
|
328
|
+
// Remove | help('...') including nested parens
|
|
329
|
+
originalValue = originalValue.replace(/\s*\|\s*help\([^)]*(?:\([^)]*\))?[^)]*\)/g, '')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (keyPath && originalValue) {
|
|
333
|
+
return {
|
|
334
|
+
key: keyPath,
|
|
335
|
+
value: originalValue,
|
|
336
|
+
description: occ.description
|
|
337
|
+
}
|
|
338
|
+
} else if (keyPath) {
|
|
339
|
+
return {
|
|
340
|
+
key: keyPath,
|
|
341
|
+
value: '',
|
|
342
|
+
description: occ.description
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}).filter(Boolean)
|
|
347
|
+
|
|
348
|
+
if (parsedOccurrences.length > 0) {
|
|
349
|
+
// Get the variable reference syntax
|
|
350
|
+
const varPrefix = variableType === 'options' ? 'opt' : variableType === 'env' ? 'env' : 'self'
|
|
351
|
+
const varSyntax = `\${${varPrefix}:${cleanName}}`
|
|
352
|
+
|
|
353
|
+
// Show variable syntax and count (only if no descriptions, otherwise it's redundant)
|
|
354
|
+
if (descriptions.length === 0) {
|
|
355
|
+
contextHint = ` - ${varSyntax} - Used in ${parsedOccurrences.length} ${parsedOccurrences.length === 1 ? 'place' : 'places'}`
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Find longest key for alignment
|
|
359
|
+
const maxKeyLength = Math.max(...parsedOccurrences.map(o => o.key.length))
|
|
360
|
+
|
|
361
|
+
// List all occurrences with bullets and aligned values (using invisible unicode for indentation)
|
|
362
|
+
const indent = '\u2800\u2800\u2800' // Braille blank pattern (invisible but not stripped)
|
|
363
|
+
const usageList = parsedOccurrences.map(({ key, value, description }, index) => {
|
|
364
|
+
const padding = ' '.repeat(maxKeyLength - key.length)
|
|
365
|
+
const leadingEmptyLine = index === 0 ? '│\n' : ''
|
|
366
|
+
// Only show inline description if there are multiple occurrences (otherwise it's redundant with header)
|
|
367
|
+
const descComment = description && parsedOccurrences.length > 1 ? ` - # ${description}` : ''
|
|
368
|
+
return value ? `${leadingEmptyLine}│${indent}- ${key}:${padding} ${value}${descComment}` : `${leadingEmptyLine}│${indent}• ${key}${descComment}`
|
|
369
|
+
})
|
|
370
|
+
contextHint += '\n' + usageList.join('\n') + '\n│'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const message = `[${typeLabel}] ${cleanName}${chalk.gray(contextHint)}`
|
|
375
|
+
|
|
376
|
+
return message
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Runs config setup wizard
|
|
381
|
+
* @param {object} metadata - Enriched metadata from configorama
|
|
382
|
+
* @param {object} originalConfig - The original config before resolution
|
|
383
|
+
* @returns {Promise<object>} User inputs by variable type
|
|
384
|
+
*/
|
|
385
|
+
async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '') {
|
|
386
|
+
const { uniqueVariables } = metadata
|
|
387
|
+
|
|
388
|
+
if (!uniqueVariables || Object.keys(uniqueVariables).length === 0) {
|
|
389
|
+
p.intro(chalk.cyan('Configuration Wizard'))
|
|
390
|
+
if (configFilePath) {
|
|
391
|
+
p.note(`File: ${configFilePath}`, 'File')
|
|
392
|
+
}
|
|
393
|
+
p.outro('No variables found that require setup.')
|
|
394
|
+
return {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const grouped = groupVariablesByType(uniqueVariables, originalConfig)
|
|
398
|
+
const totalVars = grouped.options.length + grouped.env.length + grouped.self.length
|
|
399
|
+
|
|
400
|
+
if (totalVars === 0) {
|
|
401
|
+
p.intro(chalk.cyan('Configuration Wizard'))
|
|
402
|
+
p.outro('No variables found that require setup.')
|
|
403
|
+
return {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
p.intro(chalk.cyan('Configuration Wizard'))
|
|
407
|
+
if (configFilePath) {
|
|
408
|
+
p.note(`Processing config file: ${configFilePath}`)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const userInputs = {
|
|
412
|
+
options: {},
|
|
413
|
+
env: {},
|
|
414
|
+
self: {},
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Prompt for options (CLI flags)
|
|
418
|
+
if (grouped.options.length > 0) {
|
|
419
|
+
const flagsList = grouped.options.map(v => {
|
|
420
|
+
const varSyntax = `\${opt:${v.cleanName}}`
|
|
421
|
+
return ` - ${varSyntax}`
|
|
422
|
+
}).join('\n')
|
|
423
|
+
const noteContent = `Found ${grouped.options.length} CLI flag(s)\n${flagsList}`
|
|
424
|
+
p.note(noteContent, 'CLI Flags')
|
|
425
|
+
|
|
426
|
+
for (const varInfo of grouped.options) {
|
|
427
|
+
const message = createPromptMessage(varInfo)
|
|
428
|
+
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
429
|
+
const promptFn = isSensitive ? p.password : p.text
|
|
430
|
+
const expectedType = getExpectedType(varInfo.occurrences)
|
|
431
|
+
|
|
432
|
+
const placeholder = varInfo.hasFallback
|
|
433
|
+
? `${varInfo.defaultValue} (default)`
|
|
434
|
+
: `Enter value for --${varInfo.cleanName}`
|
|
435
|
+
|
|
436
|
+
const value = await promptFn({
|
|
437
|
+
message,
|
|
438
|
+
placeholder,
|
|
439
|
+
validate: (val) => {
|
|
440
|
+
// Only required if no fallback exists
|
|
441
|
+
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
442
|
+
return 'This value is required'
|
|
443
|
+
}
|
|
444
|
+
// Type validation
|
|
445
|
+
const typeError = validateType(val, expectedType)
|
|
446
|
+
if (typeError) return typeError
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
if (p.isCancel(value)) {
|
|
451
|
+
p.cancel('Setup cancelled')
|
|
452
|
+
process.exit(0)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
userInputs.options[varInfo.cleanName] = value || varInfo.defaultValue
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Prompt for environment variables
|
|
460
|
+
if (grouped.env.length > 0) {
|
|
461
|
+
const envList = grouped.env.map(v => {
|
|
462
|
+
const varSyntax = `\${env:${v.cleanName}}`
|
|
463
|
+
return ` - ${varSyntax}`
|
|
464
|
+
}).join('\n')
|
|
465
|
+
const noteContent = `Found ${grouped.env.length} environment variable(s)\n${envList}`
|
|
466
|
+
p.note(noteContent, 'Environment Variables')
|
|
467
|
+
|
|
468
|
+
for (const varInfo of grouped.env) {
|
|
469
|
+
const message = createPromptMessage(varInfo)
|
|
470
|
+
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
471
|
+
const promptFn = isSensitive ? p.password : p.text
|
|
472
|
+
const expectedType = getExpectedType(varInfo.occurrences)
|
|
473
|
+
|
|
474
|
+
const placeholder = varInfo.hasFallback
|
|
475
|
+
? `${varInfo.defaultValue} (default)`
|
|
476
|
+
: `Enter environment variable for ${varInfo.cleanName}`
|
|
477
|
+
|
|
478
|
+
const value = await promptFn({
|
|
479
|
+
message,
|
|
480
|
+
placeholder,
|
|
481
|
+
validate: (val) => {
|
|
482
|
+
// Only required if no fallback exists
|
|
483
|
+
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
484
|
+
return 'This value is required'
|
|
485
|
+
}
|
|
486
|
+
// Type validation
|
|
487
|
+
const typeError = validateType(val, expectedType)
|
|
488
|
+
if (typeError) return typeError
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
if (p.isCancel(value)) {
|
|
493
|
+
p.cancel('Setup cancelled')
|
|
494
|
+
process.exit(0)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
userInputs.env[varInfo.cleanName] = value || varInfo.defaultValue
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Prompt for self references (if any need values)
|
|
502
|
+
if (grouped.self.length > 0) {
|
|
503
|
+
const selfList = grouped.self.map(v => {
|
|
504
|
+
const varSyntax = `\${self:${v.cleanName}}`
|
|
505
|
+
return ` - ${varSyntax}`
|
|
506
|
+
}).join('\n')
|
|
507
|
+
const noteContent = `Found ${grouped.self.length} config reference(s)\n${selfList}`
|
|
508
|
+
p.note(noteContent, 'Config References')
|
|
509
|
+
|
|
510
|
+
for (const varInfo of grouped.self) {
|
|
511
|
+
const message = createPromptMessage(varInfo)
|
|
512
|
+
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
513
|
+
const promptFn = isSensitive ? p.password : p.text
|
|
514
|
+
const expectedType = getExpectedType(varInfo.occurrences)
|
|
515
|
+
|
|
516
|
+
const placeholder = varInfo.hasFallback
|
|
517
|
+
? `${varInfo.defaultValue} (default)`
|
|
518
|
+
: `Enter value for ${varInfo.cleanName}`
|
|
519
|
+
|
|
520
|
+
const value = await promptFn({
|
|
521
|
+
message,
|
|
522
|
+
placeholder,
|
|
523
|
+
validate: (val) => {
|
|
524
|
+
// Only required if no fallback exists
|
|
525
|
+
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
526
|
+
return 'This value is required'
|
|
527
|
+
}
|
|
528
|
+
// Type validation
|
|
529
|
+
const typeError = validateType(val, expectedType)
|
|
530
|
+
if (typeError) return typeError
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
if (p.isCancel(value)) {
|
|
535
|
+
p.cancel('Setup cancelled')
|
|
536
|
+
process.exit(0)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
userInputs.self[varInfo.cleanName] = value || varInfo.defaultValue
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
p.outro(chalk.green('Setup complete!'))
|
|
544
|
+
|
|
545
|
+
// Remove empty sections
|
|
546
|
+
if (Object.keys(userInputs.options).length === 0) {
|
|
547
|
+
delete userInputs.options
|
|
548
|
+
}
|
|
549
|
+
if (Object.keys(userInputs.env).length === 0) {
|
|
550
|
+
delete userInputs.env
|
|
551
|
+
}
|
|
552
|
+
if (Object.keys(userInputs.self).length === 0) {
|
|
553
|
+
delete userInputs.self
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return userInputs
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
module.exports = {
|
|
560
|
+
runConfigWizard,
|
|
561
|
+
groupVariablesByType,
|
|
562
|
+
isSensitiveVariable,
|
|
563
|
+
createPromptMessage,
|
|
564
|
+
getExpectedType,
|
|
565
|
+
getHelpText,
|
|
566
|
+
validateType,
|
|
567
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { decodeJsSyntax, hasParenthesesPlaceholder } = require('./js-fixes')
|
|
2
|
+
const { decodeUnknown, hasEncodedUnknown } = require('./unknown-values')
|
|
3
|
+
|
|
4
|
+
// Generic decoder for all encoded values
|
|
5
|
+
function decodeEncodedValue(value) {
|
|
6
|
+
if (hasParenthesesPlaceholder(value)) {
|
|
7
|
+
return decodeJsSyntax(value)
|
|
8
|
+
}
|
|
9
|
+
if (hasEncodedUnknown(value)) {
|
|
10
|
+
return decodeUnknown(value)
|
|
11
|
+
}
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { decodeEncodedValue }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const PAREN_OPEN_PLACEHOLDER = '__PH_PAREN_OPEN__'
|
|
2
|
+
const OPEN_PAREN_PLACEHOLDER_PATTERN = /__PH_PAREN_OPEN__/g
|
|
3
|
+
|
|
4
|
+
function encodeJsSyntax(value = '') {
|
|
5
|
+
return value.replace(/\(/g, PAREN_OPEN_PLACEHOLDER)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function decodeJsSyntax(value) {
|
|
9
|
+
if (!value) return value
|
|
10
|
+
return value.replace(OPEN_PAREN_PLACEHOLDER_PATTERN, '(')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasParenthesesPlaceholder(value = '') {
|
|
14
|
+
return OPEN_PAREN_PLACEHOLDER_PATTERN.test(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
OPEN_PAREN_PLACEHOLDER_PATTERN,
|
|
19
|
+
hasParenthesesPlaceholder,
|
|
20
|
+
encodeJsSyntax,
|
|
21
|
+
decodeJsSyntax,
|
|
22
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const PASSTHROUGH_PREFIX = '>passthrough'
|
|
2
|
+
const PASSTHROUGH_PATTERN = />passthrough/g
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Encode unknown variable for passthrough
|
|
3
6
|
*/
|
|
@@ -5,12 +8,16 @@ function encodeUnknown(v) {
|
|
|
5
8
|
return `>passthrough[_[${Buffer.from(v).toString('base64')}]_]`
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
function hasEncodedUnknown(value) {
|
|
12
|
+
return PASSTHROUGH_PATTERN.test(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Decode unknown variable from passthrough
|
|
10
17
|
*/
|
|
11
18
|
function decodeUnknown(rawValue) {
|
|
12
19
|
const x = findUnknownValues(rawValue)
|
|
13
|
-
let val = rawValue.replace(
|
|
20
|
+
let val = rawValue.replace(PASSTHROUGH_PATTERN, '')
|
|
14
21
|
if (x.length) {
|
|
15
22
|
x.forEach(({ match, value }) => {
|
|
16
23
|
const decodedValue = Buffer.from(value, 'base64').toString('ascii')
|
|
@@ -40,6 +47,8 @@ function findUnknownValues(text) {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
module.exports = {
|
|
50
|
+
PASSTHROUGH_PATTERN,
|
|
51
|
+
hasEncodedUnknown,
|
|
43
52
|
encodeUnknown,
|
|
44
53
|
decodeUnknown,
|
|
45
54
|
findUnknownValues
|