configorama 0.6.11 → 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 +775 -857
- 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} +244 -94
- package/src/utils/{parse.js → parsing/parse.js} +13 -13
- package/src/utils/parsing/preProcess.js +165 -0
- package/src/utils/paths/filePathUtils.js +136 -0
- package/src/utils/paths/filePathUtils.test.js +214 -0
- 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} +212 -60
- 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/{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,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
|
}
|
|
@@ -223,17 +257,27 @@ function validateType(value, expectedType) {
|
|
|
223
257
|
}
|
|
224
258
|
|
|
225
259
|
/**
|
|
226
|
-
* Extracts type from variable occurrences
|
|
227
|
-
* @param {
|
|
260
|
+
* Extracts type from variable data or occurrences
|
|
261
|
+
* @param {object} varData - Variable data with types array or occurrences
|
|
228
262
|
* @returns {string|null} Expected type or null
|
|
229
263
|
*/
|
|
230
|
-
function getExpectedType(
|
|
231
|
-
|
|
264
|
+
function getExpectedType(varData) {
|
|
265
|
+
// Use pre-computed types if available
|
|
266
|
+
if (varData && varData.types && varData.types.length > 0) {
|
|
267
|
+
return varData.types[0]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Fallback to checking occurrences
|
|
271
|
+
const occurrences = varData && varData.occurrences ? varData.occurrences : varData
|
|
272
|
+
if (!occurrences || !Array.isArray(occurrences) || occurrences.length === 0) return null
|
|
232
273
|
|
|
233
274
|
for (const occ of occurrences) {
|
|
275
|
+
// Check pre-computed type on occurrence
|
|
276
|
+
if (occ.type) return occ.type
|
|
277
|
+
|
|
278
|
+
// Fallback to filters
|
|
234
279
|
if (occ.filters && Array.isArray(occ.filters)) {
|
|
235
280
|
for (const filter of occ.filters) {
|
|
236
|
-
// Check if filter starts with uppercase letter
|
|
237
281
|
if (filter && typeof filter === 'string' && /^[A-Z]/.test(filter)) {
|
|
238
282
|
return filter
|
|
239
283
|
}
|
|
@@ -244,23 +288,28 @@ function getExpectedType(occurrences) {
|
|
|
244
288
|
}
|
|
245
289
|
|
|
246
290
|
/**
|
|
247
|
-
* Extracts help text from variable occurrences
|
|
248
|
-
* @param {
|
|
291
|
+
* Extracts help text from variable data or occurrences
|
|
292
|
+
* @param {object} varData - Variable data with descriptions array or occurrences
|
|
249
293
|
* @returns {string|null} Help text or null
|
|
250
294
|
*/
|
|
251
|
-
function getHelpText(
|
|
252
|
-
|
|
295
|
+
function getHelpText(varData) {
|
|
296
|
+
// Use pre-computed descriptions if available
|
|
297
|
+
if (varData && varData.descriptions && varData.descriptions.length > 0) {
|
|
298
|
+
return varData.descriptions.join('. ')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Fallback to checking occurrences
|
|
302
|
+
const occurrences = varData && varData.occurrences ? varData.occurrences : varData
|
|
303
|
+
if (!occurrences || !Array.isArray(occurrences) || occurrences.length === 0) return null
|
|
253
304
|
|
|
254
305
|
for (const occ of occurrences) {
|
|
255
|
-
// Check for description field first (preferred)
|
|
256
306
|
if (occ.description) {
|
|
257
307
|
return occ.description
|
|
258
308
|
}
|
|
259
309
|
|
|
260
|
-
// Fallback to checking filters array
|
|
310
|
+
// Fallback to checking filters array
|
|
261
311
|
if (occ.filters && Array.isArray(occ.filters)) {
|
|
262
312
|
for (const filter of occ.filters) {
|
|
263
|
-
// Check if filter has help() syntax
|
|
264
313
|
const helpMatch = filter.match(/^help\(['"](.+)['"]\)$/)
|
|
265
314
|
if (helpMatch) {
|
|
266
315
|
return helpMatch[1]
|
|
@@ -271,6 +320,29 @@ function getHelpText(occurrences) {
|
|
|
271
320
|
return null
|
|
272
321
|
}
|
|
273
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
|
+
|
|
274
346
|
/**
|
|
275
347
|
* Creates a human-readable prompt message
|
|
276
348
|
* @param {object} varInfo - Variable info
|
|
@@ -286,26 +358,26 @@ function createPromptMessage(varInfo) {
|
|
|
286
358
|
typeLabel = 'Env'
|
|
287
359
|
} else if (variableType === 'self') {
|
|
288
360
|
typeLabel = 'Config'
|
|
361
|
+
} else if (variableType === 'dot.prop') {
|
|
362
|
+
typeLabel = 'Config'
|
|
289
363
|
} else {
|
|
290
364
|
typeLabel = 'Value'
|
|
291
365
|
}
|
|
292
366
|
|
|
293
|
-
// Check for type
|
|
294
|
-
const expectedType = getExpectedType(
|
|
367
|
+
// Check for type - use pre-computed if available
|
|
368
|
+
const expectedType = getExpectedType(varInfo)
|
|
295
369
|
|
|
296
370
|
// Append type to label if found
|
|
297
371
|
if (expectedType) {
|
|
298
372
|
typeLabel = `${typeLabel}:${expectedType}`
|
|
299
373
|
}
|
|
300
374
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
if (occurrences && occurrences.length > 0) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
})
|
|
375
|
+
// Use pre-computed descriptions if available, otherwise collect from occurrences
|
|
376
|
+
let descriptions = varInfo.descriptions || []
|
|
377
|
+
if (descriptions.length === 0 && occurrences && occurrences.length > 0) {
|
|
378
|
+
descriptions = occurrences
|
|
379
|
+
.map(occ => occ.description)
|
|
380
|
+
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
309
381
|
}
|
|
310
382
|
|
|
311
383
|
// Build context from all occurrences
|
|
@@ -325,8 +397,8 @@ function createPromptMessage(varInfo) {
|
|
|
325
397
|
|
|
326
398
|
// Strip help() filter from the displayed value
|
|
327
399
|
if (originalValue && typeof originalValue === 'string') {
|
|
328
|
-
// Remove | help('...')
|
|
329
|
-
originalValue = originalValue.replace(/\s*\|\s*help\([^
|
|
400
|
+
// Remove | help('...') or | help("...") - match quoted string inside help()
|
|
401
|
+
originalValue = originalValue.replace(/\s*\|\s*help\(('[^']*'|"[^"]*")\)/g, '')
|
|
330
402
|
}
|
|
331
403
|
|
|
332
404
|
if (keyPath && originalValue) {
|
|
@@ -347,8 +419,13 @@ function createPromptMessage(varInfo) {
|
|
|
347
419
|
|
|
348
420
|
if (parsedOccurrences.length > 0) {
|
|
349
421
|
// Get the variable reference syntax
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
}
|
|
352
429
|
|
|
353
430
|
// Show variable syntax and count (only if no descriptions, otherwise it's redundant)
|
|
354
431
|
if (descriptions.length === 0) {
|
|
@@ -358,16 +435,17 @@ function createPromptMessage(varInfo) {
|
|
|
358
435
|
// Find longest key for alignment
|
|
359
436
|
const maxKeyLength = Math.max(...parsedOccurrences.map(o => o.key.length))
|
|
360
437
|
|
|
438
|
+
// Count unique descriptions
|
|
439
|
+
const uniqueDescriptions = new Set(parsedOccurrences.map(o => o.description).filter(Boolean))
|
|
440
|
+
|
|
361
441
|
// List all occurrences with bullets and aligned values (using invisible unicode for indentation)
|
|
362
|
-
const
|
|
363
|
-
const usageList = parsedOccurrences.map(({ key, value, description }, index) => {
|
|
442
|
+
const usageLines = parsedOccurrences.map(({ key, value, description }) => {
|
|
364
443
|
const padding = ' '.repeat(maxKeyLength - key.length)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
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}`
|
|
369
447
|
})
|
|
370
|
-
contextHint += '\n' +
|
|
448
|
+
contextHint += '\n' + formatWizardMultilineText(1, usageLines.join('\n'))
|
|
371
449
|
}
|
|
372
450
|
}
|
|
373
451
|
|
|
@@ -395,7 +473,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
395
473
|
}
|
|
396
474
|
|
|
397
475
|
const grouped = groupVariablesByType(uniqueVariables, originalConfig)
|
|
398
|
-
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
|
|
399
477
|
|
|
400
478
|
if (totalVars === 0) {
|
|
401
479
|
p.intro(chalk.cyan('Configuration Wizard'))
|
|
@@ -412,26 +490,94 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
412
490
|
options: {},
|
|
413
491
|
env: {},
|
|
414
492
|
self: {},
|
|
493
|
+
dotProp: {},
|
|
415
494
|
}
|
|
416
495
|
|
|
417
496
|
// Prompt for options (CLI flags)
|
|
418
497
|
if (grouped.options.length > 0) {
|
|
419
|
-
const flagsList = grouped.options.map(v => {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
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}`
|
|
424
504
|
p.note(noteContent, 'CLI Flags')
|
|
425
505
|
|
|
426
506
|
for (const varInfo of grouped.options) {
|
|
427
507
|
const message = createPromptMessage(varInfo)
|
|
428
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)
|
|
429
563
|
const promptFn = isSensitive ? p.password : p.text
|
|
430
564
|
const expectedType = getExpectedType(varInfo.occurrences)
|
|
431
565
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
}
|
|
435
581
|
|
|
436
582
|
const value = await promptFn({
|
|
437
583
|
message,
|
|
@@ -452,20 +598,20 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
452
598
|
process.exit(0)
|
|
453
599
|
}
|
|
454
600
|
|
|
455
|
-
userInputs.
|
|
601
|
+
userInputs.env[varInfo.cleanName] = value || varInfo.resolvedValue || varInfo.defaultValue
|
|
456
602
|
}
|
|
457
603
|
}
|
|
458
604
|
|
|
459
|
-
// Prompt for
|
|
460
|
-
if (grouped.
|
|
461
|
-
const
|
|
462
|
-
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}}`
|
|
463
609
|
return ` - ${varSyntax}`
|
|
464
610
|
}).join('\n')
|
|
465
|
-
const noteContent = `Found ${grouped.
|
|
466
|
-
p.note(noteContent, '
|
|
611
|
+
const noteContent = `Found ${grouped.self.length} config reference(s)\n${selfList}`
|
|
612
|
+
p.note(noteContent, 'Config References')
|
|
467
613
|
|
|
468
|
-
for (const varInfo of grouped.
|
|
614
|
+
for (const varInfo of grouped.self) {
|
|
469
615
|
const message = createPromptMessage(varInfo)
|
|
470
616
|
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
471
617
|
const promptFn = isSensitive ? p.password : p.text
|
|
@@ -473,7 +619,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
473
619
|
|
|
474
620
|
const placeholder = varInfo.hasFallback
|
|
475
621
|
? `${varInfo.defaultValue} (default)`
|
|
476
|
-
: `Enter
|
|
622
|
+
: `Enter value for ${varInfo.cleanName}`
|
|
477
623
|
|
|
478
624
|
const value = await promptFn({
|
|
479
625
|
message,
|
|
@@ -494,20 +640,20 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
494
640
|
process.exit(0)
|
|
495
641
|
}
|
|
496
642
|
|
|
497
|
-
userInputs.
|
|
643
|
+
userInputs.self[varInfo.cleanName] = value || varInfo.defaultValue
|
|
498
644
|
}
|
|
499
645
|
}
|
|
500
646
|
|
|
501
|
-
// Prompt for
|
|
502
|
-
if (grouped.
|
|
503
|
-
const
|
|
504
|
-
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}}`
|
|
505
651
|
return ` - ${varSyntax}`
|
|
506
652
|
}).join('\n')
|
|
507
|
-
const noteContent = `Found ${grouped.
|
|
653
|
+
const noteContent = `Found ${grouped.dotProp.length} config reference(s)\n${configList}`
|
|
508
654
|
p.note(noteContent, 'Config References')
|
|
509
655
|
|
|
510
|
-
for (const varInfo of grouped.
|
|
656
|
+
for (const varInfo of grouped.dotProp) {
|
|
511
657
|
const message = createPromptMessage(varInfo)
|
|
512
658
|
const isSensitive = isSensitiveVariable(varInfo.cleanName)
|
|
513
659
|
const promptFn = isSensitive ? p.password : p.text
|
|
@@ -536,7 +682,7 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
536
682
|
process.exit(0)
|
|
537
683
|
}
|
|
538
684
|
|
|
539
|
-
userInputs.
|
|
685
|
+
userInputs.dotProp[varInfo.cleanName] = value || varInfo.defaultValue
|
|
540
686
|
}
|
|
541
687
|
}
|
|
542
688
|
|
|
@@ -552,6 +698,9 @@ async function runConfigWizard(metadata, originalConfig = {}, configFilePath = '
|
|
|
552
698
|
if (Object.keys(userInputs.self).length === 0) {
|
|
553
699
|
delete userInputs.self
|
|
554
700
|
}
|
|
701
|
+
if (Object.keys(userInputs.dotProp).length === 0) {
|
|
702
|
+
delete userInputs.dotProp
|
|
703
|
+
}
|
|
555
704
|
|
|
556
705
|
return userInputs
|
|
557
706
|
}
|
|
@@ -563,5 +712,8 @@ module.exports = {
|
|
|
563
712
|
createPromptMessage,
|
|
564
713
|
getExpectedType,
|
|
565
714
|
getHelpText,
|
|
715
|
+
getAllowedValues,
|
|
566
716
|
validateType,
|
|
717
|
+
prefixMultilineText,
|
|
718
|
+
formatWizardMultilineText,
|
|
567
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()
|