configorama 0.6.12 → 0.6.13
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 +196 -24
- package/cli.js +3 -3
- package/package.json +1 -1
- package/src/index.js +22 -32
- package/src/main.js +690 -778
- package/src/parsers/yaml.js +3 -47
- package/src/resolvers/valueFromCron.js +3 -1
- package/src/resolvers/valueFromEnv.js +1 -0
- package/src/resolvers/valueFromEval.js +1 -0
- package/src/resolvers/valueFromFile.js +394 -0
- package/src/resolvers/valueFromGit.js +3 -2
- package/src/resolvers/valueFromOptions.js +1 -0
- package/src/resolvers/valueFromString.js +2 -1
- package/src/sync.js +12 -5
- package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
- package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +177 -15
- package/src/utils/{parse.js → parsing/parse.js} +13 -13
- package/src/utils/parsing/preProcess.js +165 -0
- package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
- package/src/utils/paths/findLineForKey.js +47 -0
- package/src/utils/paths/findLineForKey.test.js +126 -0
- package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
- package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
- package/src/utils/regex/index.js +23 -1
- package/src/utils/resolution/preResolveVariable.js +260 -0
- package/src/utils/resolution/preResolveVariable.test.js +98 -0
- package/src/utils/strings/bracketMatcher.js +86 -0
- package/src/utils/strings/bracketMatcher.test.js +135 -0
- package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
- package/src/utils/strings/formatFunctionArgs.test.js +77 -0
- package/src/utils/strings/quoteUtils.js +89 -0
- package/src/utils/strings/quoteUtils.test.js +217 -0
- package/src/utils/strings/replaceAll.test.js +82 -0
- package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
- package/src/utils/strings/splitCsv.js +38 -0
- package/src/utils/strings/splitCsv.test.js +96 -0
- package/src/utils/strings/textUtils.test.js +86 -0
- package/src/utils/{configWizard.js → ui/configWizard.js} +177 -38
- package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
- package/src/utils/{logs.js → ui/logs.js} +3 -3
- package/src/utils/validation/isValidValue.test.js +64 -0
- package/src/utils/validation/warnIfNotFound.js +52 -0
- package/src/utils/variables/appendDeepVariable.test.js +41 -0
- package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
- package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
- package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
- package/src/utils/variables/getVariableType.test.js +109 -0
- package/src/utils/variables/variableUtils.test.js +117 -0
- package/src/utils/isValidValue.js +0 -8
- package/src/utils/splitCsv.js +0 -29
- package/src/utils/trimSurroundingQuotes.js +0 -5
- /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
- /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
- /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
- /package/src/utils/{filePathUtils.test.js → paths/filePathUtils.test.js} +0 -0
- /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
- /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
- /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
- /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
- /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
- /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
- /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
- /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
- /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
- /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
- /package/src/utils/{variableUtils.js → variables/variableUtils.js} +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { splitCsv } = require('./splitCsv')
|
|
4
|
+
|
|
5
|
+
// Tests for default comma splitting (uses splitByComma internally)
|
|
6
|
+
test('splitCsv - should split simple comma-separated values', () => {
|
|
7
|
+
const result = splitCsv('first,second,third')
|
|
8
|
+
assert.equal(result, ['first', 'second', 'third'])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('splitCsv - should handle spaces around commas', () => {
|
|
12
|
+
const result = splitCsv('first, second, third')
|
|
13
|
+
assert.equal(result, ['first', 'second', 'third'])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('splitCsv - should preserve quoted strings with commas', () => {
|
|
17
|
+
const result = splitCsv("'string, with comma', normal")
|
|
18
|
+
assert.equal(result, ["'string, with comma'", 'normal'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('splitCsv - should handle double quotes', () => {
|
|
22
|
+
const result = splitCsv('"quoted, value", normal')
|
|
23
|
+
assert.equal(result, ['"quoted, value"', 'normal'])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('splitCsv - should handle parentheses (function calls)', () => {
|
|
27
|
+
const result = splitCsv('func(arg1, arg2), other')
|
|
28
|
+
assert.equal(result, ['func(arg1, arg2)', 'other'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('splitCsv - should handle square brackets (arrays)', () => {
|
|
32
|
+
const result = splitCsv('[item1, item2], other')
|
|
33
|
+
assert.equal(result, ['[item1, item2]', 'other'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('splitCsv - should return array with single element when no commas', () => {
|
|
37
|
+
const result = splitCsv('singleValue')
|
|
38
|
+
assert.equal(result, ['singleValue'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('splitCsv - should handle empty string', () => {
|
|
42
|
+
const result = splitCsv('')
|
|
43
|
+
assert.equal(result, [''])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Tests for custom splitter (uses original simple implementation)
|
|
47
|
+
test('splitCsv - should use custom splitter', () => {
|
|
48
|
+
const result = splitCsv('first|second|third', '|')
|
|
49
|
+
assert.equal(result, ['first', 'second', 'third'])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('splitCsv - should preserve quoted content with custom splitter', () => {
|
|
53
|
+
const result = splitCsv('first|"quoted|content"|third', '|')
|
|
54
|
+
assert.equal(result, ['first', '"quoted|content"', 'third'])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('splitCsv - should handle semicolon splitter', () => {
|
|
58
|
+
const result = splitCsv('a;b;c', ';')
|
|
59
|
+
assert.equal(result, ['a', 'b', 'c'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('splitCsv - custom splitter with quotes preserving internal splitters', () => {
|
|
63
|
+
const result = splitCsv('"has;semicolon";normal', ';')
|
|
64
|
+
assert.equal(result, ['"has;semicolon"', 'normal'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Edge cases
|
|
68
|
+
test('splitCsv - should handle mixed quotes and brackets', () => {
|
|
69
|
+
const result = splitCsv('[array], "string", func(a, b)')
|
|
70
|
+
assert.equal(result, ['[array]', '"string"', 'func(a, b)'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('splitCsv - should handle deeply nested structures', () => {
|
|
74
|
+
const result = splitCsv('func(obj[0, 1], "str, comma"), other')
|
|
75
|
+
assert.equal(result, ['func(obj[0, 1], "str, comma")', 'other'])
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('splitCsv - should handle serverless variable syntax', () => {
|
|
79
|
+
const result = splitCsv('opt:stage, ${opt:stageOne}')
|
|
80
|
+
// Default behavior splits inside ${}, but splitByComma handles this better
|
|
81
|
+
// This test just verifies it doesn't crash
|
|
82
|
+
assert.ok(Array.isArray(result))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('splitCsv - should preserve whitespace in quoted strings', () => {
|
|
86
|
+
const result = splitCsv('" spaces ", normal')
|
|
87
|
+
assert.equal(result, ['" spaces "', 'normal'])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('splitCsv - should handle consecutive commas with custom splitter', () => {
|
|
91
|
+
const result = splitCsv('a||c', '|')
|
|
92
|
+
assert.equal(result, ['a', '', 'c'])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Run all tests
|
|
96
|
+
test.run()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { getTextAfterOccurrence, findNestedVariable } = require('./textUtils')
|
|
4
|
+
|
|
5
|
+
// Tests for getTextAfterOccurrence
|
|
6
|
+
test('getTextAfterOccurrence - should return text after first occurrence', () => {
|
|
7
|
+
const result = getTextAfterOccurrence('hello world, hello again', 'world')
|
|
8
|
+
assert.equal(result, 'world, hello again')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('getTextAfterOccurrence - should return empty string when search not found', () => {
|
|
12
|
+
const result = getTextAfterOccurrence('hello world', 'xyz')
|
|
13
|
+
assert.equal(result, '')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('getTextAfterOccurrence - should handle search at start of string', () => {
|
|
17
|
+
const result = getTextAfterOccurrence('start of text', 'start')
|
|
18
|
+
assert.equal(result, 'start of text')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('getTextAfterOccurrence - should handle search at end of string', () => {
|
|
22
|
+
const result = getTextAfterOccurrence('end of text', 'text')
|
|
23
|
+
assert.equal(result, 'text')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('getTextAfterOccurrence - should handle empty search string', () => {
|
|
27
|
+
const result = getTextAfterOccurrence('hello', '')
|
|
28
|
+
assert.equal(result, 'hello')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('getTextAfterOccurrence - should return empty for empty source string', () => {
|
|
32
|
+
const result = getTextAfterOccurrence('', 'search')
|
|
33
|
+
assert.equal(result, '')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Tests for findNestedVariable
|
|
37
|
+
test('findNestedVariable - should find variable in original source', () => {
|
|
38
|
+
const split = ['env:VAR', 'default']
|
|
39
|
+
const originalSource = 'value is ${env:VAR}'
|
|
40
|
+
const result = findNestedVariable(split, originalSource)
|
|
41
|
+
assert.equal(result, 'env:VAR')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('findNestedVariable - should return undefined when not found', () => {
|
|
45
|
+
const split = ['env:VAR', 'default']
|
|
46
|
+
const originalSource = 'no variables here'
|
|
47
|
+
const result = findNestedVariable(split, originalSource)
|
|
48
|
+
assert.equal(result, undefined)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('findNestedVariable - should find first matching variable', () => {
|
|
52
|
+
const split = ['env:ONE', 'env:TWO']
|
|
53
|
+
const originalSource = '${env:TWO} and ${env:ONE}'
|
|
54
|
+
const result = findNestedVariable(split, originalSource)
|
|
55
|
+
assert.equal(result, 'env:ONE')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('findNestedVariable - should handle non-string source', () => {
|
|
59
|
+
const split = ['env:VAR']
|
|
60
|
+
const originalSource = null
|
|
61
|
+
const result = findNestedVariable(split, originalSource)
|
|
62
|
+
assert.equal(result, undefined)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('findNestedVariable - should handle undefined source', () => {
|
|
66
|
+
const split = ['env:VAR']
|
|
67
|
+
const result = findNestedVariable(split, undefined)
|
|
68
|
+
assert.equal(result, undefined)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('findNestedVariable - should handle empty split array', () => {
|
|
72
|
+
const split = []
|
|
73
|
+
const originalSource = '${env:VAR}'
|
|
74
|
+
const result = findNestedVariable(split, originalSource)
|
|
75
|
+
assert.equal(result, undefined)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('findNestedVariable - should match exact variable syntax', () => {
|
|
79
|
+
const split = ['env:VAR']
|
|
80
|
+
const originalSource = 'env:VAR without braces'
|
|
81
|
+
const result = findNestedVariable(split, originalSource)
|
|
82
|
+
assert.equal(result, undefined)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Run all tests
|
|
86
|
+
test.run()
|
|
@@ -5,6 +5,36 @@ const dotProp = require('dot-prop')
|
|
|
5
5
|
const fs = require('fs')
|
|
6
6
|
const path = require('path')
|
|
7
7
|
|
|
8
|
+
const INVISIBLE_SPACE = '\u2800\u2800\u2800'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Prefixes each line of multiline text with INVISIBLE_SPACE repeated a specified number of times
|
|
12
|
+
* @param {number} count - Number of times to repeat INVISIBLE_SPACE for prefix
|
|
13
|
+
* @param {string} text - Multiline text to prefix
|
|
14
|
+
* @returns {string} Text with each line prefixed with INVISIBLE_SPACE
|
|
15
|
+
*/
|
|
16
|
+
function prefixMultilineText(count, text) {
|
|
17
|
+
if (!text) return text
|
|
18
|
+
const prefix = INVISIBLE_SPACE.repeat(count)
|
|
19
|
+
return text.split('\n').map(line => `${prefix}${line}`).join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formats multiline text for wizard display with leading pipe and invisible space indentation
|
|
24
|
+
* @param {number} indentCount - Number of times to repeat INVISIBLE_SPACE for indentation
|
|
25
|
+
* @param {string} text - Multiline text to format
|
|
26
|
+
* @param {boolean} addLeadingEmptyLine - Whether to add empty line with pipe before first line (default: true)
|
|
27
|
+
* @returns {string} Formatted text with pipe prefix and indentation
|
|
28
|
+
*/
|
|
29
|
+
function formatWizardMultilineText(indentCount, text, addLeadingEmptyLine = true) {
|
|
30
|
+
if (!text) return text
|
|
31
|
+
const indent = INVISIBLE_SPACE.repeat(indentCount)
|
|
32
|
+
const lines = text.split('\n')
|
|
33
|
+
const formattedLines = lines.map(line => `${chalk.gray('│')}${indent}${line}`)
|
|
34
|
+
const leadingLine = addLeadingEmptyLine ? `${chalk.gray('│')}\n` : '\n'
|
|
35
|
+
return leadingLine + formattedLines.join('\n') + `\n${chalk.gray('│')}`
|
|
36
|
+
}
|
|
37
|
+
|
|
8
38
|
/**
|
|
9
39
|
* Groups variables by type for wizard flow
|
|
10
40
|
* @param {object} uniqueVariables - The uniqueVariables from enriched metadata
|
|
@@ -16,13 +46,14 @@ function groupVariablesByType(uniqueVariables, originalConfig = {}) {
|
|
|
16
46
|
options: [],
|
|
17
47
|
env: [],
|
|
18
48
|
self: [],
|
|
49
|
+
dotProp: [],
|
|
19
50
|
}
|
|
20
51
|
|
|
21
52
|
// Track variables we've already added to avoid duplicates
|
|
22
53
|
const addedVars = new Set()
|
|
23
54
|
|
|
24
55
|
for (const [varKey, varData] of Object.entries(uniqueVariables)) {
|
|
25
|
-
const { variable, variableType, isRequired, defaultValue, defaultValueSrc, occurrences, innerVariables, hasValue } = varData
|
|
56
|
+
const { variable, variableType, isRequired, defaultValue, defaultValueSrc, occurrences, innerVariables, hasValue, resolvedValue } = varData
|
|
26
57
|
|
|
27
58
|
// Handle top-level variables (not file/text types)
|
|
28
59
|
if (variableType !== 'file' && variableType !== 'text') {
|
|
@@ -92,6 +123,7 @@ function groupVariablesByType(uniqueVariables, originalConfig = {}) {
|
|
|
92
123
|
isRequired: hasRequiredOccurrence,
|
|
93
124
|
defaultValue: availableDefault,
|
|
94
125
|
hasFallback: !!availableDefault,
|
|
126
|
+
resolvedValue,
|
|
95
127
|
occurrences: occurrences || [],
|
|
96
128
|
}
|
|
97
129
|
|
|
@@ -101,6 +133,8 @@ function groupVariablesByType(uniqueVariables, originalConfig = {}) {
|
|
|
101
133
|
grouped.env.push(varInfo)
|
|
102
134
|
} else if (variableType === 'self') {
|
|
103
135
|
grouped.self.push(varInfo)
|
|
136
|
+
} else if (variableType === 'dot.prop') {
|
|
137
|
+
grouped.dotProp.push(varInfo)
|
|
104
138
|
}
|
|
105
139
|
}
|
|
106
140
|
}
|
|
@@ -286,6 +320,29 @@ function getHelpText(varData) {
|
|
|
286
320
|
return null
|
|
287
321
|
}
|
|
288
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Extracts allowed values from description text like "Deployment stage (dev, staging, production)"
|
|
325
|
+
* @param {object} varData - Variable data with descriptions array or occurrences
|
|
326
|
+
* @returns {string[]|null} Array of allowed values or null if not found
|
|
327
|
+
*/
|
|
328
|
+
function getAllowedValues(varData) {
|
|
329
|
+
const helpText = getHelpText(varData)
|
|
330
|
+
if (!helpText) return null
|
|
331
|
+
|
|
332
|
+
// Match pattern like (value1, value2, value3) at end of description
|
|
333
|
+
const match = helpText.match(/\(([^)]+)\)\s*$/)
|
|
334
|
+
if (!match) return null
|
|
335
|
+
|
|
336
|
+
const valuesStr = match[1]
|
|
337
|
+
const values = valuesStr.split(',').map(v => v.trim()).filter(Boolean)
|
|
338
|
+
|
|
339
|
+
// Only treat as allowed values if we have 2+ options and they look like simple values
|
|
340
|
+
if (values.length < 2) return null
|
|
341
|
+
if (values.some(v => v.includes(' ') && !v.match(/^['"].*['"]$/))) return null
|
|
342
|
+
|
|
343
|
+
return values
|
|
344
|
+
}
|
|
345
|
+
|
|
289
346
|
/**
|
|
290
347
|
* Creates a human-readable prompt message
|
|
291
348
|
* @param {object} varInfo - Variable info
|
|
@@ -301,6 +358,8 @@ function createPromptMessage(varInfo) {
|
|
|
301
358
|
typeLabel = 'Env'
|
|
302
359
|
} else if (variableType === 'self') {
|
|
303
360
|
typeLabel = 'Config'
|
|
361
|
+
} else if (variableType === 'dot.prop') {
|
|
362
|
+
typeLabel = 'Config'
|
|
304
363
|
} else {
|
|
305
364
|
typeLabel = 'Value'
|
|
306
365
|
}
|
|
@@ -338,8 +397,8 @@ function createPromptMessage(varInfo) {
|
|
|
338
397
|
|
|
339
398
|
// Strip help() filter from the displayed value
|
|
340
399
|
if (originalValue && typeof originalValue === 'string') {
|
|
341
|
-
// Remove | help('...')
|
|
342
|
-
originalValue = originalValue.replace(/\s*\|\s*help\([^
|
|
400
|
+
// Remove | help('...') or | help("...") - match quoted string inside help()
|
|
401
|
+
originalValue = originalValue.replace(/\s*\|\s*help\(('[^']*'|"[^"]*")\)/g, '')
|
|
343
402
|
}
|
|
344
403
|
|
|
345
404
|
if (keyPath && originalValue) {
|
|
@@ -360,8 +419,13 @@ function createPromptMessage(varInfo) {
|
|
|
360
419
|
|
|
361
420
|
if (parsedOccurrences.length > 0) {
|
|
362
421
|
// Get the variable reference syntax
|
|
363
|
-
|
|
364
|
-
|
|
422
|
+
let varSyntax
|
|
423
|
+
if (variableType === 'dot.prop') {
|
|
424
|
+
varSyntax = `\${${cleanName}}`
|
|
425
|
+
} else {
|
|
426
|
+
const varPrefix = variableType === 'options' ? 'opt' : variableType === 'env' ? 'env' : 'self'
|
|
427
|
+
varSyntax = `\${${varPrefix}:${cleanName}}`
|
|
428
|
+
}
|
|
365
429
|
|
|
366
430
|
// Show variable syntax and count (only if no descriptions, otherwise it's redundant)
|
|
367
431
|
if (descriptions.length === 0) {
|
|
@@ -371,16 +435,17 @@ function createPromptMessage(varInfo) {
|
|
|
371
435
|
// Find longest key for alignment
|
|
372
436
|
const maxKeyLength = Math.max(...parsedOccurrences.map(o => o.key.length))
|
|
373
437
|
|
|
438
|
+
// Count unique descriptions
|
|
439
|
+
const uniqueDescriptions = new Set(parsedOccurrences.map(o => o.description).filter(Boolean))
|
|
440
|
+
|
|
374
441
|
// List all occurrences with bullets and aligned values (using invisible unicode for indentation)
|
|
375
|
-
const
|
|
376
|
-
const usageList = parsedOccurrences.map(({ key, value, description }, index) => {
|
|
442
|
+
const usageLines = parsedOccurrences.map(({ key, value, description }) => {
|
|
377
443
|
const padding = ' '.repeat(maxKeyLength - key.length)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return value ? `${leadingEmptyLine}│${indent}- ${key}:${padding} ${value}${descComment}` : `${leadingEmptyLine}│${indent}• ${key}${descComment}`
|
|
444
|
+
// Only show inline description if there are multiple unique descriptions
|
|
445
|
+
const descComment = description && uniqueDescriptions.size > 1 ? ` - # ${description}` : ''
|
|
446
|
+
return value ? `- ${key}:${padding} ${value}${descComment}` : `• ${key}${descComment}`
|
|
382
447
|
})
|
|
383
|
-
contextHint += '\n' +
|
|
448
|
+
contextHint += '\n' + formatWizardMultilineText(1, usageLines.join('\n'))
|
|
384
449
|
}
|
|
385
450
|
}
|
|
386
451
|
|
|
@@ -408,7 +473,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
408
473
|
}
|
|
409
474
|
|
|
410
475
|
const grouped = groupVariablesByType(uniqueVariables, originalConfig)
|
|
411
|
-
const totalVars = grouped.options.length + grouped.env.length + grouped.self.length
|
|
476
|
+
const totalVars = grouped.options.length + grouped.env.length + grouped.self.length + grouped.dotProp.length
|
|
412
477
|
|
|
413
478
|
if (totalVars === 0) {
|
|
414
479
|
p.intro(chalk.cyan('Configuration Wizard'))
|
|
@@ -425,26 +490,94 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
425
490
|
options: {},
|
|
426
491
|
env: {},
|
|
427
492
|
self: {},
|
|
493
|
+
dotProp: {},
|
|
428
494
|
}
|
|
429
495
|
|
|
430
496
|
// Prompt for options (CLI flags)
|
|
431
497
|
if (grouped.options.length > 0) {
|
|
432
|
-
const flagsList = grouped.options.map(v => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
498
|
+
const flagsList = grouped.options.map(v => `\${opt:${v.cleanName}}`)
|
|
499
|
+
const flagsDisplay = flagsList.length < 5
|
|
500
|
+
? flagsList.join(', ')
|
|
501
|
+
: flagsList.map(f => ` - ${f}`).join('\n')
|
|
502
|
+
const addNewLine = flagsList.length > 5 ? '\n' : ' - '
|
|
503
|
+
const noteContent = `Found ${grouped.options.length} CLI flag(s)${addNewLine}${flagsDisplay}`
|
|
437
504
|
p.note(noteContent, 'CLI Flags')
|
|
438
505
|
|
|
439
506
|
for (const varInfo of grouped.options) {
|
|
440
507
|
const message = createPromptMessage(varInfo)
|
|
441
508
|
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
509
|
+
const expectedType = getExpectedType(varInfo.occurrences)
|
|
510
|
+
const allowedValues = getAllowedValues(varInfo)
|
|
511
|
+
|
|
512
|
+
let value
|
|
513
|
+
if (allowedValues && !isSensitive) {
|
|
514
|
+
// Use select picker for enumerated values
|
|
515
|
+
const options = allowedValues.map(v => ({ value: v, label: v }))
|
|
516
|
+
value = await p.select({
|
|
517
|
+
message,
|
|
518
|
+
options,
|
|
519
|
+
initialValue: varInfo.defaultValue || allowedValues[0]
|
|
520
|
+
})
|
|
521
|
+
} else {
|
|
522
|
+
const promptFn = isSensitive ? p.password : p.text
|
|
523
|
+
const placeholder = varInfo.hasFallback
|
|
524
|
+
? ` ${varInfo.defaultValue} `
|
|
525
|
+
: `Enter value for --${varInfo.cleanName}`
|
|
526
|
+
|
|
527
|
+
value = await promptFn({
|
|
528
|
+
message,
|
|
529
|
+
placeholder,
|
|
530
|
+
validate: (val) => {
|
|
531
|
+
// Only required if no fallback exists
|
|
532
|
+
if (!val && varInfo.isRequired && !varInfo.hasFallback) {
|
|
533
|
+
return 'This value is required'
|
|
534
|
+
}
|
|
535
|
+
// Type validation
|
|
536
|
+
const typeError = validateType(val, expectedType)
|
|
537
|
+
if (typeError) return typeError
|
|
538
|
+
}
|
|
539
|
+
})
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (p.isCancel(value)) {
|
|
543
|
+
p.cancel('Setup cancelled')
|
|
544
|
+
process.exit(0)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
userInputs.options[varInfo.cleanName] = value || varInfo.defaultValue
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Prompt for environment variables
|
|
552
|
+
if (grouped.env.length > 0) {
|
|
553
|
+
const envList = grouped.env.map(v => {
|
|
554
|
+
const varSyntax = `\${env:${v.cleanName}}`
|
|
555
|
+
return ` - ${varSyntax}`
|
|
556
|
+
}).join('\n')
|
|
557
|
+
const noteContent = `Found ${grouped.env.length} environment variable(s)\n${envList}`
|
|
558
|
+
p.note(noteContent, 'Environment Variables')
|
|
559
|
+
|
|
560
|
+
for (const varInfo of grouped.env) {
|
|
561
|
+
let message = createPromptMessage(varInfo)
|
|
562
|
+
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
442
563
|
const promptFn = isSensitive ? p.password : p.text
|
|
443
564
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
444
565
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
566
|
+
let placeholder
|
|
567
|
+
if (varInfo.resolvedValue !== undefined) {
|
|
568
|
+
if (isSensitive) {
|
|
569
|
+
// For sensitive vars, show hint in message since password prompts don't show placeholders
|
|
570
|
+
message += formatWizardMultilineText(1, chalk.green(`Notice: process.env.${varInfo.cleanName} set\nPress enter to use current value OR input a new value below`), false)
|
|
571
|
+
// placeholder doesn't work with password prompts
|
|
572
|
+
placeholder = ' enter to use current value or input a new value'
|
|
573
|
+
} else {
|
|
574
|
+
placeholder = `${varInfo.resolvedValue} (current env value)`
|
|
575
|
+
}
|
|
576
|
+
} else if (varInfo.hasFallback) {
|
|
577
|
+
placeholder = `${varInfo.defaultValue} (default)`
|
|
578
|
+
} else {
|
|
579
|
+
placeholder = `Enter environment variable for ${varInfo.cleanName}`
|
|
580
|
+
}
|
|
448
581
|
|
|
449
582
|
const value = await promptFn({
|
|
450
583
|
message,
|
|
@@ -465,20 +598,20 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
465
598
|
process.exit(0)
|
|
466
599
|
}
|
|
467
600
|
|
|
468
|
-
userInputs.
|
|
601
|
+
userInputs.env[varInfo.cleanName] = value || varInfo.resolvedValue || varInfo.defaultValue
|
|
469
602
|
}
|
|
470
603
|
}
|
|
471
604
|
|
|
472
|
-
// Prompt for
|
|
473
|
-
if (grouped.
|
|
474
|
-
const
|
|
475
|
-
const varSyntax = `\${
|
|
605
|
+
// Prompt for self references (if any need values)
|
|
606
|
+
if (grouped.self.length > 0) {
|
|
607
|
+
const selfList = grouped.self.map(v => {
|
|
608
|
+
const varSyntax = `\${self:${v.cleanName}}`
|
|
476
609
|
return ` - ${varSyntax}`
|
|
477
610
|
}).join('\n')
|
|
478
|
-
const noteContent = `Found ${grouped.
|
|
479
|
-
p.note(noteContent, '
|
|
611
|
+
const noteContent = `Found ${grouped.self.length} config reference(s)\n${selfList}`
|
|
612
|
+
p.note(noteContent, 'Config References')
|
|
480
613
|
|
|
481
|
-
for (const varInfo of grouped.
|
|
614
|
+
for (const varInfo of grouped.self) {
|
|
482
615
|
const message = createPromptMessage(varInfo)
|
|
483
616
|
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
484
617
|
const promptFn = isSensitive ? p.password : p.text
|
|
@@ -486,7 +619,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
486
619
|
|
|
487
620
|
const placeholder = varInfo.hasFallback
|
|
488
621
|
? `${varInfo.defaultValue} (default)`
|
|
489
|
-
: `Enter
|
|
622
|
+
: `Enter value for ${varInfo.cleanName}`
|
|
490
623
|
|
|
491
624
|
const value = await promptFn({
|
|
492
625
|
message,
|
|
@@ -507,20 +640,20 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
507
640
|
process.exit(0)
|
|
508
641
|
}
|
|
509
642
|
|
|
510
|
-
userInputs.
|
|
643
|
+
userInputs.self[varInfo.cleanName] = value || varInfo.defaultValue
|
|
511
644
|
}
|
|
512
645
|
}
|
|
513
646
|
|
|
514
|
-
// Prompt for
|
|
515
|
-
if (grouped.
|
|
516
|
-
const
|
|
517
|
-
const varSyntax = `\${
|
|
647
|
+
// Prompt for config dot.prop references
|
|
648
|
+
if (grouped.dotProp.length > 0) {
|
|
649
|
+
const configList = grouped.dotProp.map(v => {
|
|
650
|
+
const varSyntax = `\${${v.cleanName}}`
|
|
518
651
|
return ` - ${varSyntax}`
|
|
519
652
|
}).join('\n')
|
|
520
|
-
const noteContent = `Found ${grouped.
|
|
653
|
+
const noteContent = `Found ${grouped.dotProp.length} config reference(s)\n${configList}`
|
|
521
654
|
p.note(noteContent, 'Config References')
|
|
522
655
|
|
|
523
|
-
for (const varInfo of grouped.
|
|
656
|
+
for (const varInfo of grouped.dotProp) {
|
|
524
657
|
const message = createPromptMessage(varInfo)
|
|
525
658
|
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
526
659
|
const promptFn = isSensitive ? p.password : p.text
|
|
@@ -549,7 +682,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
549
682
|
process.exit(0)
|
|
550
683
|
}
|
|
551
684
|
|
|
552
|
-
userInputs.
|
|
685
|
+
userInputs.dotProp[varInfo.cleanName] = value || varInfo.defaultValue
|
|
553
686
|
}
|
|
554
687
|
}
|
|
555
688
|
|
|
@@ -565,6 +698,9 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
565
698
|
if (Object.keys(userInputs.self).length === 0) {
|
|
566
699
|
delete userInputs.self
|
|
567
700
|
}
|
|
701
|
+
if (Object.keys(userInputs.dotProp).length === 0) {
|
|
702
|
+
delete userInputs.dotProp
|
|
703
|
+
}
|
|
568
704
|
|
|
569
705
|
return userInputs
|
|
570
706
|
}
|
|
@@ -576,5 +712,8 @@ module.exports = {
|
|
|
576
712
|
createPromptMessage,
|
|
577
713
|
getExpectedType,
|
|
578
714
|
getHelpText,
|
|
715
|
+
getAllowedValues,
|
|
579
716
|
validateType,
|
|
717
|
+
prefixMultilineText,
|
|
718
|
+
formatWizardMultilineText,
|
|
580
719
|
}
|
|
@@ -7,7 +7,7 @@ const chalk = require('./chalk')
|
|
|
7
7
|
* @param {number} line - Line number (default: 1)
|
|
8
8
|
* @param {number} column - Column number (default: 1)
|
|
9
9
|
* @param {string} customDisplay - Custom display text (default: filename:line)
|
|
10
|
-
* @param {string} color - Chalk color for the link (default: 'cyanBright')
|
|
10
|
+
* @param {string|false} color - Chalk color for the link (default: 'cyanBright'), or false to skip coloring
|
|
11
11
|
* @returns {string} The hyperlink string
|
|
12
12
|
*/
|
|
13
13
|
function createEditorLink(filePath, line = 1, column = 1, customDisplay = null, color = 'cyanBright') {
|
|
@@ -15,7 +15,16 @@ function createEditorLink(filePath, line = 1, column = 1, customDisplay = null,
|
|
|
15
15
|
const url = `cursor://file${absolutePath}:${line}:${column}`
|
|
16
16
|
const display = customDisplay ? customDisplay: `${path.basename(filePath)}:${line}`
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
let displayText = display
|
|
19
|
+
if (color !== false) {
|
|
20
|
+
if (typeof color === 'string' && color.startsWith('#')) {
|
|
21
|
+
displayText = chalk.hex(color)(display)
|
|
22
|
+
} else {
|
|
23
|
+
displayText = chalk[color](display)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return `\x1b]8;;${url}\x1b\\${displayText}\x1b]8;;\x1b\\`
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
module.exports = {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
const { makeHeader } = require('@davidwells/box-logger')
|
|
1
|
+
const { makeHeader, logHeader : logHeaderBox } = require('@davidwells/box-logger')
|
|
2
2
|
|
|
3
3
|
function logHeader(message) {
|
|
4
|
-
|
|
4
|
+
logHeaderBox({
|
|
5
5
|
content: message,
|
|
6
6
|
rightBorder: true,
|
|
7
7
|
minWidth: 80,
|
|
8
8
|
textStyle: 'bold',
|
|
9
9
|
borderStyle: 'bold',
|
|
10
10
|
borderColor: 'cyanBright',
|
|
11
|
-
})
|
|
11
|
+
})
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
module.exports = {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const { isValidValue } = require('./warnIfNotFound')
|
|
4
|
+
|
|
5
|
+
test('isValidValue - should return true for non-empty string', () => {
|
|
6
|
+
assert.is(isValidValue('hello'), true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('isValidValue - should return true for number', () => {
|
|
10
|
+
assert.is(isValidValue(42), true)
|
|
11
|
+
assert.is(isValidValue(0), true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('isValidValue - should return true for boolean', () => {
|
|
15
|
+
assert.is(isValidValue(true), true)
|
|
16
|
+
assert.is(isValidValue(false), true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('isValidValue - should return true for non-empty object', () => {
|
|
20
|
+
assert.is(isValidValue({ key: 'value' }), true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('isValidValue - should return true for non-empty array', () => {
|
|
24
|
+
assert.is(isValidValue([1, 2, 3]), true)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test.skip('isValidValue - should return false for null', () => {
|
|
28
|
+
assert.is(isValidValue(null), false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('isValidValue - should return false for undefined', () => {
|
|
32
|
+
assert.is(isValidValue(undefined), false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('isValidValue - should return false for empty object', () => {
|
|
36
|
+
assert.is(isValidValue({}), false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('isValidValue - should return false for empty array', () => {
|
|
40
|
+
assert.is(isValidValue([]), false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('isValidValue - should return false for object with __internal_only_flag', () => {
|
|
44
|
+
assert.is(isValidValue({ __internal_only_flag: true, data: 'value' }), false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('isValidValue - should return false for object with __internal_metadata', () => {
|
|
48
|
+
assert.is(isValidValue({ __internal_metadata: {}, data: 'value' }), false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('isValidValue - should return true for empty string', () => {
|
|
52
|
+
assert.is(isValidValue(''), true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test.skip('isValidValue - should return true for date object', () => {
|
|
56
|
+
assert.is(isValidValue(new Date()), true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('isValidValue - should return true for function', () => {
|
|
60
|
+
assert.is(isValidValue(() => {}), true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Run all tests
|
|
64
|
+
test.run()
|