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.
Files changed (66) hide show
  1. package/README.md +196 -24
  2. package/cli.js +3 -3
  3. package/package.json +1 -1
  4. package/src/index.js +22 -32
  5. package/src/main.js +690 -778
  6. package/src/parsers/yaml.js +3 -47
  7. package/src/resolvers/valueFromCron.js +3 -1
  8. package/src/resolvers/valueFromEnv.js +1 -0
  9. package/src/resolvers/valueFromEval.js +1 -0
  10. package/src/resolvers/valueFromFile.js +394 -0
  11. package/src/resolvers/valueFromGit.js +3 -2
  12. package/src/resolvers/valueFromOptions.js +1 -0
  13. package/src/resolvers/valueFromString.js +2 -1
  14. package/src/sync.js +12 -5
  15. package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
  16. package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +177 -15
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
  20. package/src/utils/paths/findLineForKey.js +47 -0
  21. package/src/utils/paths/findLineForKey.test.js +126 -0
  22. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
  23. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  24. package/src/utils/regex/index.js +23 -1
  25. package/src/utils/resolution/preResolveVariable.js +260 -0
  26. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  27. package/src/utils/strings/bracketMatcher.js +86 -0
  28. package/src/utils/strings/bracketMatcher.test.js +135 -0
  29. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  30. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  31. package/src/utils/strings/quoteUtils.js +89 -0
  32. package/src/utils/strings/quoteUtils.test.js +217 -0
  33. package/src/utils/strings/replaceAll.test.js +82 -0
  34. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  35. package/src/utils/strings/splitCsv.js +38 -0
  36. package/src/utils/strings/splitCsv.test.js +96 -0
  37. package/src/utils/strings/textUtils.test.js +86 -0
  38. package/src/utils/{configWizard.js → ui/configWizard.js} +177 -38
  39. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  40. package/src/utils/{logs.js → ui/logs.js} +3 -3
  41. package/src/utils/validation/isValidValue.test.js +64 -0
  42. package/src/utils/validation/warnIfNotFound.js +52 -0
  43. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  44. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  45. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  46. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  47. package/src/utils/variables/getVariableType.test.js +109 -0
  48. package/src/utils/variables/variableUtils.test.js +117 -0
  49. package/src/utils/isValidValue.js +0 -8
  50. package/src/utils/splitCsv.js +0 -29
  51. package/src/utils/trimSurroundingQuotes.js +0 -5
  52. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  53. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  54. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
  55. /package/src/utils/{filePathUtils.test.js → paths/filePathUtils.test.js} +0 -0
  56. /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
  57. /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
  58. /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
  59. /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
  60. /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
  61. /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
  62. /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
  63. /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
  64. /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
  65. /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
  66. /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('...') including nested parens
342
- originalValue = originalValue.replace(/\s*\|\s*help\([^)]*(?:\([^)]*\))?[^)]*\)/g, '')
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
- const varPrefix = variableType === 'options' ? 'opt' : variableType === 'env' ? 'env' : 'self'
364
- const varSyntax = `\${${varPrefix}:${cleanName}}`
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 indent = '\u2800\u2800\u2800' // Braille blank pattern (invisible but not stripped)
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
- const leadingEmptyLine = index === 0 ? '│\n' : ''
379
- // Only show inline description if there are multiple occurrences (otherwise it's redundant with header)
380
- const descComment = description && parsedOccurrences.length > 1 ? ` - # ${description}` : ''
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' + usageList.join('\n') + '\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
- const varSyntax = `\${opt:${v.cleanName}}`
434
- return ` - ${varSyntax}`
435
- }).join('\n')
436
- const noteContent = `Found ${grouped.options.length} CLI flag(s)\n${flagsList}`
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
- const placeholder = varInfo.hasFallback
446
- ? `${varInfo.defaultValue} (default)`
447
- : `Enter value for --${varInfo.cleanName}`
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.options[varInfo.cleanName] = value || varInfo.defaultValue
601
+ userInputs.env[varInfo.cleanName] = value || varInfo.resolvedValue || varInfo.defaultValue
469
602
  }
470
603
  }
471
604
 
472
- // Prompt for environment variables
473
- if (grouped.env.length > 0) {
474
- const envList = grouped.env.map(v => {
475
- const varSyntax = `\${env:${v.cleanName}}`
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.env.length} environment variable(s)\n${envList}`
479
- p.note(noteContent, 'Environment Variables')
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.env) {
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 environment variable for ${varInfo.cleanName}`
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.env[varInfo.cleanName] = value || varInfo.defaultValue
643
+ userInputs.self[varInfo.cleanName] = value || varInfo.defaultValue
511
644
  }
512
645
  }
513
646
 
514
- // Prompt for self references (if any need values)
515
- if (grouped.self.length > 0) {
516
- const selfList = grouped.self.map(v => {
517
- const varSyntax = `\${self:${v.cleanName}}`
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.self.length} config reference(s)\n${selfList}`
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.self) {
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.self[varInfo.cleanName] = value || varInfo.defaultValue
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
- return `\x1b]8;;${url}\x1b\\${chalk[color](display)}\x1b]8;;\x1b\\`
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
- console.log(makeHeader({
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()