configorama 0.11.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +429 -123
- package/cli.js +282 -49
- package/index.d.ts +43 -1
- package/package.json +5 -1
- package/src/capabilities.js +59 -0
- package/src/capabilities.test.js +44 -0
- package/src/display.js +70 -7
- package/src/display.test.js +82 -0
- package/src/errors.js +73 -0
- package/src/index.js +91 -1
- package/src/main.js +117 -15
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +5 -0
- package/src/resolvers/valueFromGit.js +43 -17
- package/src/resolvers/valueFromOptions.js +5 -4
- package/src/utils/filters/filterArgs.js +57 -0
- package/src/utils/filters/oneOf.js +77 -0
- package/src/utils/introspection/audit.js +78 -0
- package/src/utils/introspection/graph.js +43 -0
- package/src/utils/introspection/model.js +150 -0
- package/src/utils/introspection/model.test.js +93 -0
- package/src/utils/parsing/commentAnnotations.js +107 -0
- package/src/utils/parsing/commentAnnotations.test.js +123 -0
- package/src/utils/parsing/enrichMetadata.js +64 -1
- package/src/utils/parsing/enrichMetadata.test.js +84 -0
- package/src/utils/parsing/extractComment.js +145 -0
- package/src/utils/parsing/extractComment.test.js +182 -0
- package/src/utils/parsing/preProcess.js +2 -1
- package/src/utils/paths/findLineForKey.js +2 -2
- package/src/utils/paths/ignorePaths.js +21 -9
- package/src/utils/redaction/redact.js +78 -0
- package/src/utils/redaction/redact.test.js +38 -0
- package/src/utils/redaction/setupRedaction.js +47 -0
- package/src/utils/redaction/setupRedaction.test.js +68 -0
- package/src/utils/requirements/configRequirements.js +351 -0
- package/src/utils/requirements/configRequirements.test.js +380 -0
- package/src/utils/requirements/serializeRequirements.js +120 -0
- package/src/utils/requirements/serializeRequirements.test.js +211 -0
- package/src/utils/security/evalSafety.js +86 -0
- package/src/utils/security/evalSafety.test.js +61 -0
- package/src/utils/security/safetyPolicy.js +110 -0
- package/src/utils/security/safetyPolicy.test.js +29 -0
- package/src/utils/strings/didYouMean.js +70 -0
- package/src/utils/strings/didYouMean.test.js +52 -0
- package/src/utils/strings/formatFunctionArgs.js +6 -1
- package/src/utils/strings/splitByComma.js +5 -0
- package/src/utils/ui/configWizard.js +208 -34
- package/src/utils/ui/createEditorLink.js +17 -1
- package/src/utils/ui/promptDescriptors.js +196 -0
- package/src/utils/ui/promptDescriptors.test.js +162 -0
- package/src/utils/variables/cleanVariable.js +22 -0
- package/src/utils/variables/getVariableType.js +1 -0
|
@@ -4,6 +4,10 @@ const chalk = require('./chalk')
|
|
|
4
4
|
const dotProp = require('dot-prop')
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
|
+
const { toClickablePath } = require('./createEditorLink')
|
|
8
|
+
const { buildConfigRequirements } = require('../requirements/configRequirements')
|
|
9
|
+
const { createPromptDescriptors } = require('./promptDescriptors')
|
|
10
|
+
const { isSensitiveVariable } = require('../redaction/redact')
|
|
7
11
|
|
|
8
12
|
const INVISIBLE_SPACE = '\u2800\u2800\u2800'
|
|
9
13
|
|
|
@@ -35,6 +39,32 @@ function formatWizardMultilineText(indentCount, text, addLeadingEmptyLine = true
|
|
|
35
39
|
return leadingLine + formattedLines.join('\n') + `\n${chalk.gray('│')}`
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Removes a single pair of matching surrounding quotes from a string.
|
|
44
|
+
* Mirrors how configorama strips quotes from inline fallback values like `'us-east-1'`.
|
|
45
|
+
* @param {*} val - Value to clean
|
|
46
|
+
* @returns {*} The value without surrounding quotes (non-strings pass through)
|
|
47
|
+
*/
|
|
48
|
+
function stripQuotes(val) {
|
|
49
|
+
if (typeof val !== 'string') return val
|
|
50
|
+
const match = val.match(/^(['"])([\s\S]*)\1$/)
|
|
51
|
+
return match ? match[2] : val
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Shortens a file path for display so long absolute paths don't wrap the prompt box.
|
|
56
|
+
* Prefers a path relative to cwd when shorter, then truncates the front, keeping the filename.
|
|
57
|
+
* @param {string} filePath - Path to display
|
|
58
|
+
* @param {number} maxLen - Maximum display length (default 56)
|
|
59
|
+
* @returns {string} Display-friendly path
|
|
60
|
+
*/
|
|
61
|
+
function formatPathForDisplay(filePath, maxLen = 56) {
|
|
62
|
+
if (!filePath) return filePath
|
|
63
|
+
const display = toClickablePath(filePath)
|
|
64
|
+
if (display.length <= maxLen) return display
|
|
65
|
+
return '…' + display.slice(display.length - (maxLen - 1))
|
|
66
|
+
}
|
|
67
|
+
|
|
38
68
|
/**
|
|
39
69
|
* Groups variables by type for wizard flow
|
|
40
70
|
* @param {object} uniqueVariables - The uniqueVariables from enriched metadata
|
|
@@ -201,21 +231,90 @@ function groupVariablesByType(uniqueVariables, originalConfig = {}) {
|
|
|
201
231
|
return grouped
|
|
202
232
|
}
|
|
203
233
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
234
|
+
function shouldPromptDescriptor(descriptor) {
|
|
235
|
+
if (descriptor.variableType === 'option' || descriptor.variableType === 'env') return true
|
|
236
|
+
if (descriptor.variableType === 'self' || descriptor.variableType === 'dotProp') {
|
|
237
|
+
return descriptor.required && (descriptor.defaultValue === null || descriptor.defaultValue === undefined)
|
|
238
|
+
}
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function descriptorToVarInfo(descriptor) {
|
|
243
|
+
const variableType = descriptor.variableType === 'option'
|
|
244
|
+
? 'options'
|
|
245
|
+
: descriptor.variableType === 'dotProp'
|
|
246
|
+
? 'dot.prop'
|
|
247
|
+
: descriptor.variableType
|
|
248
|
+
const hasDefault = descriptor.defaultValue !== null && descriptor.defaultValue !== undefined && descriptor.defaultValue !== ''
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
key: descriptor.variable,
|
|
252
|
+
variable: descriptor.variable,
|
|
253
|
+
cleanName: descriptor.name,
|
|
254
|
+
variableType,
|
|
255
|
+
type: descriptor.type,
|
|
256
|
+
isRequired: descriptor.required,
|
|
257
|
+
defaultValue: descriptor.defaultValue,
|
|
258
|
+
hasFallback: hasDefault,
|
|
259
|
+
resolvedValue: descriptor.variableType === 'env' && process.env[descriptor.name] !== undefined
|
|
260
|
+
? descriptor.defaultValue
|
|
261
|
+
: undefined,
|
|
262
|
+
occurrences: descriptor.occurrences || [],
|
|
263
|
+
descriptions: descriptor.description ? [descriptor.description] : [],
|
|
264
|
+
allowedValues: descriptor.allowedValues,
|
|
265
|
+
conflictWarning: descriptor.conflictWarning,
|
|
266
|
+
obtainHint: descriptor.obtainHint,
|
|
267
|
+
examples: descriptor.examples,
|
|
268
|
+
defaultHint: descriptor.defaultHint,
|
|
269
|
+
group: descriptor.group,
|
|
270
|
+
deprecationMessage: descriptor.deprecationMessage,
|
|
271
|
+
sensitive: descriptor.sensitive,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function groupPromptDescriptorsForWizard(descriptors) {
|
|
276
|
+
const grouped = {
|
|
277
|
+
options: [],
|
|
278
|
+
env: [],
|
|
279
|
+
self: [],
|
|
280
|
+
dotProp: [],
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const descriptor of descriptors || []) {
|
|
284
|
+
if (!shouldPromptDescriptor(descriptor)) continue
|
|
285
|
+
const varInfo = descriptorToVarInfo(descriptor)
|
|
286
|
+
if (descriptor.variableType === 'option') grouped.options.push(varInfo)
|
|
287
|
+
else if (descriptor.variableType === 'env') grouped.env.push(varInfo)
|
|
288
|
+
else if (descriptor.variableType === 'self') grouped.self.push(varInfo)
|
|
289
|
+
else if (descriptor.variableType === 'dotProp') grouped.dotProp.push(varInfo)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return grouped
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isVarInfoSensitive(varInfo) {
|
|
296
|
+
if (varInfo && typeof varInfo.sensitive === 'boolean') return varInfo.sensitive
|
|
297
|
+
return isSensitiveVariable(varInfo.cleanName)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function uniqueCompact(values) {
|
|
301
|
+
return [...new Set((values || []).filter(value => value !== undefined && value !== null && value !== ''))]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getAnnotationDisplayMetadata(varInfo) {
|
|
305
|
+
const occurrences = varInfo && Array.isArray(varInfo.occurrences) ? varInfo.occurrences : []
|
|
306
|
+
const examples = uniqueCompact([
|
|
307
|
+
...(Array.isArray(varInfo.examples) ? varInfo.examples : []),
|
|
308
|
+
...occurrences.flatMap(occ => Array.isArray(occ.examples) ? occ.examples : [])
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
obtainHint: varInfo.obtainHint || occurrences.find(occ => occ.obtainHint)?.obtainHint,
|
|
313
|
+
examples,
|
|
314
|
+
defaultHint: varInfo.defaultHint || occurrences.find(occ => occ.defaultHint)?.defaultHint,
|
|
315
|
+
group: varInfo.group || occurrences.find(occ => occ.group)?.group,
|
|
316
|
+
deprecationMessage: varInfo.deprecationMessage || occurrences.find(occ => occ.deprecationMessage)?.deprecationMessage,
|
|
317
|
+
}
|
|
219
318
|
}
|
|
220
319
|
|
|
221
320
|
/**
|
|
@@ -226,8 +325,16 @@ function isSensitiveVariable(name) {
|
|
|
226
325
|
*/
|
|
227
326
|
function validateType(value, expectedType) {
|
|
228
327
|
if (!expectedType || !value) return
|
|
229
|
-
|
|
230
|
-
|
|
328
|
+
const normalizedType = {
|
|
329
|
+
boolean: 'Boolean',
|
|
330
|
+
number: 'Number',
|
|
331
|
+
string: 'String',
|
|
332
|
+
json: 'Json',
|
|
333
|
+
object: 'Object',
|
|
334
|
+
array: 'Array',
|
|
335
|
+
}[expectedType] || expectedType
|
|
336
|
+
|
|
337
|
+
switch (normalizedType) {
|
|
231
338
|
case 'Boolean':
|
|
232
339
|
const lowerVal = value.toLowerCase()
|
|
233
340
|
const validBooleans = ['true', 'false', 'yes', 'no', 'on', 'off', '1', '0']
|
|
@@ -250,6 +357,29 @@ function validateType(value, expectedType) {
|
|
|
250
357
|
}
|
|
251
358
|
break
|
|
252
359
|
|
|
360
|
+
case 'Object':
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(value)
|
|
363
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
364
|
+
return `Must be a valid JSON object`
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return `Must be valid JSON`
|
|
368
|
+
}
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
case 'Array':
|
|
372
|
+
if (Array.isArray(value)) break
|
|
373
|
+
if (typeof value !== 'string') return `Must be a comma-separated list or JSON array`
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(value)
|
|
376
|
+
if (Array.isArray(parsed)) break
|
|
377
|
+
return `Must be a JSON array`
|
|
378
|
+
} catch (e) {
|
|
379
|
+
if (!value.includes(',')) return `Must be a comma-separated list or JSON array`
|
|
380
|
+
}
|
|
381
|
+
break
|
|
382
|
+
|
|
253
383
|
case 'String':
|
|
254
384
|
// String is always valid
|
|
255
385
|
break
|
|
@@ -262,6 +392,10 @@ function validateType(value, expectedType) {
|
|
|
262
392
|
* @returns {string|null} Expected type or null
|
|
263
393
|
*/
|
|
264
394
|
function getExpectedType(varData) {
|
|
395
|
+
if (varData && varData.type) {
|
|
396
|
+
return varData.type
|
|
397
|
+
}
|
|
398
|
+
|
|
265
399
|
// Use pre-computed types if available
|
|
266
400
|
if (varData && varData.types && varData.types.length > 0) {
|
|
267
401
|
return varData.types[0]
|
|
@@ -326,6 +460,10 @@ function getHelpText(varData) {
|
|
|
326
460
|
* @returns {string[]|null} Array of allowed values or null if not found
|
|
327
461
|
*/
|
|
328
462
|
function getAllowedValues(varData) {
|
|
463
|
+
if (varData && varData.allowedValues && varData.allowedValues.length > 0) {
|
|
464
|
+
return varData.allowedValues
|
|
465
|
+
}
|
|
466
|
+
|
|
329
467
|
const helpText = getHelpText(varData)
|
|
330
468
|
if (!helpText) return null
|
|
331
469
|
|
|
@@ -387,6 +525,23 @@ function createPromptMessage(varInfo) {
|
|
|
387
525
|
if (descriptions.length > 0) {
|
|
388
526
|
contextHint = ` - ${descriptions.join('. ')}`
|
|
389
527
|
}
|
|
528
|
+
if (varInfo.conflictWarning) {
|
|
529
|
+
contextHint += `${contextHint ? '\n' : ' - '}Warning: ${varInfo.conflictWarning}`
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const annotationMetadata = getAnnotationDisplayMetadata(varInfo)
|
|
533
|
+
const metadataLines = []
|
|
534
|
+
if (annotationMetadata.group) metadataLines.push(`Group: ${annotationMetadata.group}`)
|
|
535
|
+
if (annotationMetadata.obtainHint) metadataLines.push(`From: ${annotationMetadata.obtainHint}`)
|
|
536
|
+
if (annotationMetadata.examples.length > 0) {
|
|
537
|
+
metadataLines.push(`${annotationMetadata.examples.length === 1 ? 'Example' : 'Examples'}: ${annotationMetadata.examples.join(', ')}`)
|
|
538
|
+
}
|
|
539
|
+
if (annotationMetadata.defaultHint) metadataLines.push(`Default hint: ${annotationMetadata.defaultHint}`)
|
|
540
|
+
if (annotationMetadata.deprecationMessage) metadataLines.push(`Deprecated: ${annotationMetadata.deprecationMessage}`)
|
|
541
|
+
|
|
542
|
+
if (metadataLines.length > 0) {
|
|
543
|
+
contextHint += '\n' + formatWizardMultilineText(1, metadataLines.join('\n'), false)
|
|
544
|
+
}
|
|
390
545
|
|
|
391
546
|
// Show usage list if there are occurrences
|
|
392
547
|
if (occurrences && occurrences.length > 0) {
|
|
@@ -466,13 +621,15 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
466
621
|
if (!uniqueVariables || Object.keys(uniqueVariables).length === 0) {
|
|
467
622
|
p.intro(chalk.cyan('Configuration Wizard'))
|
|
468
623
|
if (configFilePath) {
|
|
469
|
-
p.note(
|
|
624
|
+
p.note(formatPathForDisplay(configFilePath), 'File')
|
|
470
625
|
}
|
|
471
626
|
p.outro('No variables found that require setup.')
|
|
472
627
|
return {}
|
|
473
628
|
}
|
|
474
629
|
|
|
475
|
-
const
|
|
630
|
+
const requirements = buildConfigRequirements(metadata)
|
|
631
|
+
const descriptors = createPromptDescriptors(requirements)
|
|
632
|
+
const grouped = groupPromptDescriptorsForWizard(descriptors)
|
|
476
633
|
const totalVars = grouped.options.length + grouped.env.length + grouped.self.length + grouped.dotProp.length
|
|
477
634
|
|
|
478
635
|
if (totalVars === 0) {
|
|
@@ -483,7 +640,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
483
640
|
|
|
484
641
|
p.intro(chalk.cyan('Configuration Wizard'))
|
|
485
642
|
if (configFilePath) {
|
|
486
|
-
p.note(
|
|
643
|
+
p.note(formatPathForDisplay(configFilePath), 'Config file')
|
|
487
644
|
}
|
|
488
645
|
|
|
489
646
|
const userInputs = {
|
|
@@ -505,28 +662,30 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
505
662
|
|
|
506
663
|
for (const varInfo of grouped.options) {
|
|
507
664
|
const message = createPromptMessage(varInfo)
|
|
508
|
-
const isSensitive =
|
|
665
|
+
const isSensitive = isVarInfoSensitive(varInfo)
|
|
509
666
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
510
667
|
const allowedValues = getAllowedValues(varInfo)
|
|
511
668
|
|
|
512
669
|
let value
|
|
670
|
+
const cleanDefault = stripQuotes(varInfo.defaultValue)
|
|
513
671
|
if (allowedValues && !isSensitive) {
|
|
514
672
|
// Use select picker for enumerated values
|
|
515
673
|
const options = allowedValues.map(v => ({ value: v, label: v }))
|
|
516
674
|
value = await p.select({
|
|
517
675
|
message,
|
|
518
676
|
options,
|
|
519
|
-
initialValue:
|
|
677
|
+
initialValue: cleanDefault || allowedValues[0]
|
|
520
678
|
})
|
|
521
679
|
} else {
|
|
522
680
|
const promptFn = isSensitive ? p.password : p.text
|
|
523
681
|
const placeholder = varInfo.hasFallback
|
|
524
|
-
?
|
|
682
|
+
? String(cleanDefault)
|
|
525
683
|
: `Enter value for --${varInfo.cleanName}`
|
|
526
684
|
|
|
527
685
|
value = await promptFn({
|
|
528
686
|
message,
|
|
529
687
|
placeholder,
|
|
688
|
+
defaultValue: varInfo.hasFallback ? String(cleanDefault) : undefined,
|
|
530
689
|
validate: (val) => {
|
|
531
690
|
// Only required if no fallback exists
|
|
532
691
|
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
@@ -544,7 +703,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
544
703
|
throw new Error('Setup cancelled')
|
|
545
704
|
}
|
|
546
705
|
|
|
547
|
-
userInputs.options[varInfo.cleanName] = value ||
|
|
706
|
+
userInputs.options[varInfo.cleanName] = value || cleanDefault
|
|
548
707
|
}
|
|
549
708
|
}
|
|
550
709
|
|
|
@@ -559,11 +718,14 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
559
718
|
|
|
560
719
|
for (const varInfo of grouped.env) {
|
|
561
720
|
let message = createPromptMessage(varInfo)
|
|
562
|
-
const isSensitive =
|
|
721
|
+
const isSensitive = isVarInfoSensitive(varInfo)
|
|
563
722
|
const promptFn = isSensitive ? p.password : p.text
|
|
564
723
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
565
724
|
|
|
725
|
+
const cleanCurrent = stripQuotes(varInfo.resolvedValue)
|
|
726
|
+
const cleanDefault = stripQuotes(varInfo.defaultValue)
|
|
566
727
|
let placeholder
|
|
728
|
+
let defaultValue
|
|
567
729
|
if (varInfo.resolvedValue !== undefined) {
|
|
568
730
|
if (isSensitive) {
|
|
569
731
|
// For sensitive vars, show hint in message since password prompts don't show placeholders
|
|
@@ -571,10 +733,12 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
571
733
|
// placeholder doesn't work with password prompts
|
|
572
734
|
placeholder = ' enter to use current value or input a new value'
|
|
573
735
|
} else {
|
|
574
|
-
placeholder =
|
|
736
|
+
placeholder = String(cleanCurrent)
|
|
737
|
+
defaultValue = String(cleanCurrent)
|
|
575
738
|
}
|
|
576
739
|
} else if (varInfo.hasFallback) {
|
|
577
|
-
placeholder =
|
|
740
|
+
placeholder = String(cleanDefault)
|
|
741
|
+
defaultValue = String(cleanDefault)
|
|
578
742
|
} else {
|
|
579
743
|
placeholder = `Enter environment variable for ${varInfo.cleanName}`
|
|
580
744
|
}
|
|
@@ -582,6 +746,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
582
746
|
const value = await promptFn({
|
|
583
747
|
message,
|
|
584
748
|
placeholder,
|
|
749
|
+
defaultValue,
|
|
585
750
|
validate: (val) => {
|
|
586
751
|
// Only required if no fallback exists
|
|
587
752
|
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
@@ -598,7 +763,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
598
763
|
throw new Error('Setup cancelled')
|
|
599
764
|
}
|
|
600
765
|
|
|
601
|
-
userInputs.env[varInfo.cleanName] = value ||
|
|
766
|
+
userInputs.env[varInfo.cleanName] = value || cleanCurrent || cleanDefault
|
|
602
767
|
}
|
|
603
768
|
}
|
|
604
769
|
|
|
@@ -613,17 +778,19 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
613
778
|
|
|
614
779
|
for (const varInfo of grouped.self) {
|
|
615
780
|
const message = createPromptMessage(varInfo)
|
|
616
|
-
const isSensitive =
|
|
781
|
+
const isSensitive = isVarInfoSensitive(varInfo)
|
|
617
782
|
const promptFn = isSensitive ? p.password : p.text
|
|
618
783
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
619
784
|
|
|
785
|
+
const cleanDefault = stripQuotes(varInfo.defaultValue)
|
|
620
786
|
const placeholder = varInfo.hasFallback
|
|
621
|
-
?
|
|
787
|
+
? String(cleanDefault)
|
|
622
788
|
: `Enter value for ${varInfo.cleanName}`
|
|
623
789
|
|
|
624
790
|
const value = await promptFn({
|
|
625
791
|
message,
|
|
626
792
|
placeholder,
|
|
793
|
+
defaultValue: varInfo.hasFallback ? String(cleanDefault) : undefined,
|
|
627
794
|
validate: (val) => {
|
|
628
795
|
// Only required if no fallback exists
|
|
629
796
|
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
@@ -640,7 +807,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
640
807
|
throw new Error('Setup cancelled')
|
|
641
808
|
}
|
|
642
809
|
|
|
643
|
-
userInputs.self[varInfo.cleanName] = value ||
|
|
810
|
+
userInputs.self[varInfo.cleanName] = value || cleanDefault
|
|
644
811
|
}
|
|
645
812
|
}
|
|
646
813
|
|
|
@@ -655,17 +822,19 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
655
822
|
|
|
656
823
|
for (const varInfo of grouped.dotProp) {
|
|
657
824
|
const message = createPromptMessage(varInfo)
|
|
658
|
-
const isSensitive =
|
|
825
|
+
const isSensitive = isVarInfoSensitive(varInfo)
|
|
659
826
|
const promptFn = isSensitive ? p.password : p.text
|
|
660
827
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
661
828
|
|
|
829
|
+
const cleanDefault = stripQuotes(varInfo.defaultValue)
|
|
662
830
|
const placeholder = varInfo.hasFallback
|
|
663
|
-
?
|
|
831
|
+
? String(cleanDefault)
|
|
664
832
|
: `Enter value for ${varInfo.cleanName}`
|
|
665
833
|
|
|
666
834
|
const value = await promptFn({
|
|
667
835
|
message,
|
|
668
836
|
placeholder,
|
|
837
|
+
defaultValue: varInfo.hasFallback ? String(cleanDefault) : undefined,
|
|
669
838
|
validate: (val) => {
|
|
670
839
|
// Only required if no fallback exists
|
|
671
840
|
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
@@ -682,7 +851,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
682
851
|
throw new Error('Setup cancelled')
|
|
683
852
|
}
|
|
684
853
|
|
|
685
|
-
userInputs.dotProp[varInfo.cleanName] = value ||
|
|
854
|
+
userInputs.dotProp[varInfo.cleanName] = value || cleanDefault
|
|
686
855
|
}
|
|
687
856
|
}
|
|
688
857
|
|
|
@@ -708,7 +877,10 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
708
877
|
module.exports = {
|
|
709
878
|
runConfigWizard,
|
|
710
879
|
groupVariablesByType,
|
|
880
|
+
groupPromptDescriptorsForWizard,
|
|
881
|
+
descriptorToVarInfo,
|
|
711
882
|
isSensitiveVariable,
|
|
883
|
+
isVarInfoSensitive,
|
|
712
884
|
createPromptMessage,
|
|
713
885
|
getExpectedType,
|
|
714
886
|
getHelpText,
|
|
@@ -716,4 +888,6 @@ module.exports = {
|
|
|
716
888
|
validateType,
|
|
717
889
|
prefixMultilineText,
|
|
718
890
|
formatWizardMultilineText,
|
|
891
|
+
formatPathForDisplay,
|
|
892
|
+
stripQuotes,
|
|
719
893
|
}
|
|
@@ -27,6 +27,22 @@ function createEditorLink(filePath, line = 1, column = 1, customDisplay = null,
|
|
|
27
27
|
return `\x1b]8;;${url}\x1b\\${displayText}\x1b]8;;\x1b\\`
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Returns a click-to-open path: relative to cwd with a leading './' when the file
|
|
32
|
+
* lives under cwd, otherwise the absolute path. Editors linkify both forms.
|
|
33
|
+
* @param {string} filePath - The file path
|
|
34
|
+
* @returns {string} Display path that editors can open on click
|
|
35
|
+
*/
|
|
36
|
+
function toClickablePath(filePath) {
|
|
37
|
+
if (!filePath) return filePath
|
|
38
|
+
const rel = path.relative(process.cwd(), filePath)
|
|
39
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
40
|
+
return '.' + path.sep + rel
|
|
41
|
+
}
|
|
42
|
+
return path.resolve(filePath)
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
module.exports = {
|
|
31
|
-
createEditorLink
|
|
46
|
+
createEditorLink,
|
|
47
|
+
toClickablePath
|
|
32
48
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
function stripPromptQuotes(value) {
|
|
2
|
+
if (typeof value !== 'string') return value
|
|
3
|
+
const match = value.match(/^(['"])([\s\S]*)\1$/)
|
|
4
|
+
return match ? match[2] : value
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getSourceGroup(requirement) {
|
|
8
|
+
if (requirement.variableType === 'option') return 'options'
|
|
9
|
+
if (requirement.variableType === 'env') return 'env'
|
|
10
|
+
if (requirement.variableType === 'self') return 'self'
|
|
11
|
+
if (requirement.variableType === 'dotProp') return 'dotProp'
|
|
12
|
+
if (requirement.variableType === 'file' || requirement.variableType === 'text') return 'files'
|
|
13
|
+
return 'other'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getRequirementGroup(requirement) {
|
|
17
|
+
return requirement.group || getSourceGroup(requirement)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function groupRequirementsForWizard(requirements) {
|
|
21
|
+
const grouped = {
|
|
22
|
+
options: [],
|
|
23
|
+
env: [],
|
|
24
|
+
self: [],
|
|
25
|
+
dotProp: [],
|
|
26
|
+
files: [],
|
|
27
|
+
other: [],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const requirement of requirements || []) {
|
|
31
|
+
const group = getRequirementGroup(requirement)
|
|
32
|
+
if (!grouped[group]) grouped[group] = []
|
|
33
|
+
grouped[group].push(requirement)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return grouped
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function selectPromptType(requirement) {
|
|
40
|
+
if (requirement.sensitive) return 'password'
|
|
41
|
+
if (requirement.allowedValues && requirement.allowedValues.length) {
|
|
42
|
+
return requirement.type === 'array' ? 'multiselect' : 'select'
|
|
43
|
+
}
|
|
44
|
+
if (requirement.type === 'boolean') return 'confirm'
|
|
45
|
+
return 'text'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPromptDefault(requirement) {
|
|
49
|
+
if (requirement.variableType === 'env' && process.env[requirement.name] !== undefined) {
|
|
50
|
+
return process.env[requirement.name]
|
|
51
|
+
}
|
|
52
|
+
return stripPromptQuotes(requirement.default)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPromptPlaceholder(requirement) {
|
|
56
|
+
const defaultValue = getPromptDefault(requirement)
|
|
57
|
+
if (defaultValue !== null && defaultValue !== undefined && defaultValue !== '') {
|
|
58
|
+
return String(defaultValue)
|
|
59
|
+
}
|
|
60
|
+
if (requirement.variableType === 'option') return `Enter value for --${requirement.name}`
|
|
61
|
+
if (requirement.variableType === 'env') return `Enter environment variable for ${requirement.name}`
|
|
62
|
+
if (requirement.variableType === 'file' || requirement.variableType === 'text') return requirement.name
|
|
63
|
+
return `Enter value for ${requirement.name}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getConflictWarning(requirement) {
|
|
67
|
+
if (!requirement.conflicts || requirement.conflicts.length === 0) return null
|
|
68
|
+
return requirement.conflicts.map(conflict => {
|
|
69
|
+
const paths = conflict.paths && conflict.paths.length ? conflict.paths.join(', ') : 'unknown path'
|
|
70
|
+
return `${conflict.field} conflict at ${paths}`
|
|
71
|
+
}).join('; ')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validatePromptValue(value, descriptor) {
|
|
75
|
+
if ((value === undefined || value === null || value === '') && descriptor.required) {
|
|
76
|
+
return 'This value is required'
|
|
77
|
+
}
|
|
78
|
+
if (value === undefined || value === null || value === '') return undefined
|
|
79
|
+
|
|
80
|
+
if (descriptor.allowedValues && descriptor.allowedValues.length && descriptor.promptType !== 'multiselect') {
|
|
81
|
+
if (!descriptor.allowedValues.map(String).includes(String(value))) {
|
|
82
|
+
return `Must be one of: ${descriptor.allowedValues.join(', ')}`
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
switch (descriptor.type) {
|
|
87
|
+
case 'number':
|
|
88
|
+
if (Number.isNaN(Number(value))) return 'Must be a valid number'
|
|
89
|
+
return undefined
|
|
90
|
+
case 'boolean':
|
|
91
|
+
if (typeof value === 'boolean') return undefined
|
|
92
|
+
if (!['true', 'false', 'yes', 'no', 'on', 'off', '1', '0'].includes(String(value).toLowerCase())) {
|
|
93
|
+
return 'Must be a boolean value'
|
|
94
|
+
}
|
|
95
|
+
return undefined
|
|
96
|
+
case 'json':
|
|
97
|
+
case 'object':
|
|
98
|
+
if (typeof value === 'object') return undefined
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(value)
|
|
101
|
+
if (descriptor.type === 'object' && (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))) {
|
|
102
|
+
return 'Must be a valid JSON object'
|
|
103
|
+
}
|
|
104
|
+
return undefined
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return 'Must be valid JSON'
|
|
107
|
+
}
|
|
108
|
+
case 'array':
|
|
109
|
+
if (Array.isArray(value)) return undefined
|
|
110
|
+
if (typeof value !== 'string') return 'Must be a comma-separated list or JSON array'
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(value)
|
|
113
|
+
if (Array.isArray(parsed)) return undefined
|
|
114
|
+
return 'Must be a JSON array'
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return value.includes(',') ? undefined : 'Must be a comma-separated list or JSON array'
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePromptValue(value, descriptor) {
|
|
124
|
+
const input = value === undefined || value === '' ? descriptor.defaultValue : value
|
|
125
|
+
const cleaned = stripPromptQuotes(input)
|
|
126
|
+
if (cleaned === undefined || cleaned === null) return cleaned
|
|
127
|
+
|
|
128
|
+
switch (descriptor.type) {
|
|
129
|
+
case 'number':
|
|
130
|
+
return Number(cleaned)
|
|
131
|
+
case 'boolean':
|
|
132
|
+
if (typeof cleaned === 'boolean') return cleaned
|
|
133
|
+
return ['true', 'yes', 'on', '1'].includes(String(cleaned).toLowerCase())
|
|
134
|
+
case 'array':
|
|
135
|
+
if (Array.isArray(cleaned)) return cleaned
|
|
136
|
+
return String(cleaned).split(',').map(item => item.trim()).filter(Boolean)
|
|
137
|
+
case 'json':
|
|
138
|
+
case 'object':
|
|
139
|
+
return typeof cleaned === 'object' ? cleaned : JSON.parse(cleaned)
|
|
140
|
+
default:
|
|
141
|
+
return cleaned
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createPromptDescriptor(requirement) {
|
|
146
|
+
const defaultValue = getPromptDefault(requirement)
|
|
147
|
+
const promptType = selectPromptType(requirement)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
name: requirement.name,
|
|
151
|
+
variable: requirement.variable,
|
|
152
|
+
variableType: requirement.variableType,
|
|
153
|
+
group: getRequirementGroup(requirement),
|
|
154
|
+
promptType,
|
|
155
|
+
type: requirement.type || 'string',
|
|
156
|
+
required: Boolean(requirement.required),
|
|
157
|
+
sensitive: Boolean(requirement.sensitive),
|
|
158
|
+
description: requirement.description || null,
|
|
159
|
+
obtainHint: requirement.obtainHint || null,
|
|
160
|
+
examples: requirement.examples || null,
|
|
161
|
+
defaultHint: requirement.defaultHint || null,
|
|
162
|
+
deprecationMessage: requirement.deprecationMessage || null,
|
|
163
|
+
defaultValue,
|
|
164
|
+
placeholder: getPromptPlaceholder(requirement),
|
|
165
|
+
allowedValues: requirement.allowedValues || null,
|
|
166
|
+
paths: requirement.paths || [],
|
|
167
|
+
occurrences: requirement.occurrences || [],
|
|
168
|
+
conflicts: requirement.conflicts || [],
|
|
169
|
+
conflictWarning: getConflictWarning(requirement),
|
|
170
|
+
normalize: value => normalizePromptValue(value, {
|
|
171
|
+
type: requirement.type || 'string',
|
|
172
|
+
defaultValue,
|
|
173
|
+
}),
|
|
174
|
+
validate: value => validatePromptValue(value, {
|
|
175
|
+
promptType,
|
|
176
|
+
type: requirement.type || 'string',
|
|
177
|
+
required: Boolean(requirement.required),
|
|
178
|
+
allowedValues: requirement.allowedValues || null,
|
|
179
|
+
}),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createPromptDescriptors(requirements) {
|
|
184
|
+
return (requirements || []).map(createPromptDescriptor)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
createPromptDescriptor,
|
|
189
|
+
createPromptDescriptors,
|
|
190
|
+
getRequirementGroup,
|
|
191
|
+
groupRequirementsForWizard,
|
|
192
|
+
normalizePromptValue,
|
|
193
|
+
selectPromptType,
|
|
194
|
+
stripPromptQuotes,
|
|
195
|
+
validatePromptValue,
|
|
196
|
+
}
|