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.
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 +775 -857
  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} +244 -94
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/paths/filePathUtils.js +136 -0
  20. package/src/utils/paths/filePathUtils.test.js +214 -0
  21. package/src/utils/paths/findLineForKey.js +47 -0
  22. package/src/utils/paths/findLineForKey.test.js +126 -0
  23. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
  24. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  25. package/src/utils/regex/index.js +23 -1
  26. package/src/utils/resolution/preResolveVariable.js +260 -0
  27. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  28. package/src/utils/strings/bracketMatcher.js +86 -0
  29. package/src/utils/strings/bracketMatcher.test.js +135 -0
  30. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  31. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  32. package/src/utils/strings/quoteUtils.js +89 -0
  33. package/src/utils/strings/quoteUtils.test.js +217 -0
  34. package/src/utils/strings/replaceAll.test.js +82 -0
  35. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  36. package/src/utils/strings/splitCsv.js +38 -0
  37. package/src/utils/strings/splitCsv.test.js +96 -0
  38. package/src/utils/strings/textUtils.test.js +86 -0
  39. package/src/utils/{configWizard.js → ui/configWizard.js} +212 -60
  40. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  41. package/src/utils/{logs.js → ui/logs.js} +3 -3
  42. package/src/utils/validation/isValidValue.test.js +64 -0
  43. package/src/utils/validation/warnIfNotFound.js +52 -0
  44. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  45. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  46. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  47. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  48. package/src/utils/variables/getVariableType.test.js +109 -0
  49. package/src/utils/variables/variableUtils.test.js +117 -0
  50. package/src/utils/isValidValue.js +0 -8
  51. package/src/utils/splitCsv.js +0 -29
  52. package/src/utils/trimSurroundingQuotes.js +0 -5
  53. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  54. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  55. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.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,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 {Array} occurrences - Variable occurrences
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(occurrences) {
231
- if (!occurrences || occurrences.length === 0) return null
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 {Array} occurrences - Variable occurrences
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(occurrences) {
252
- if (!occurrences || occurrences.length === 0) return null
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 (for backwards compatibility)
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 from filters
294
- const expectedType = getExpectedType(occurrences)
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
- // Collect all unique descriptions from occurrences
302
- const descriptions = []
303
- if (occurrences && occurrences.length > 0) {
304
- occurrences.forEach(occ => {
305
- if (occ.description && !descriptions.includes(occ.description)) {
306
- descriptions.push(occ.description)
307
- }
308
- })
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('...') including nested parens
329
- 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, '')
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
- const varPrefix = variableType === 'options' ? 'opt' : variableType === 'env' ? 'env' : 'self'
351
- 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
+ }
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 indent = '\u2800\u2800\u2800' // Braille blank pattern (invisible but not stripped)
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
- const leadingEmptyLine = index === 0 ? '│\n' : ''
366
- // Only show inline description if there are multiple occurrences (otherwise it's redundant with header)
367
- const descComment = description && parsedOccurrences.length > 1 ? ` - # ${description}` : ''
368
- return value ? `${leadingEmptyLine}│${indent}- ${key}:${padding} ${value}${descComment}` : `${leadingEmptyLine}│${indent}• ${key}${descComment}`
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' + usageList.join('\n') + '\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
- const varSyntax = `\${opt:${v.cleanName}}`
421
- return ` - ${varSyntax}`
422
- }).join('\n')
423
- 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}`
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
- const placeholder = varInfo.hasFallback
433
- ? `${varInfo.defaultValue} (default)`
434
- : `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
+ }
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.options[varInfo.cleanName] = value || varInfo.defaultValue
601
+ userInputs.env[varInfo.cleanName] = value || varInfo.resolvedValue || varInfo.defaultValue
456
602
  }
457
603
  }
458
604
 
459
- // Prompt for environment variables
460
- if (grouped.env.length > 0) {
461
- const envList = grouped.env.map(v => {
462
- 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}}`
463
609
  return ` - ${varSyntax}`
464
610
  }).join('\n')
465
- const noteContent = `Found ${grouped.env.length} environment variable(s)\n${envList}`
466
- p.note(noteContent, 'Environment Variables')
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.env) {
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 environment variable for ${varInfo.cleanName}`
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.env[varInfo.cleanName] = value || varInfo.defaultValue
643
+ userInputs.self[varInfo.cleanName] = value || varInfo.defaultValue
498
644
  }
499
645
  }
500
646
 
501
- // Prompt for self references (if any need values)
502
- if (grouped.self.length > 0) {
503
- const selfList = grouped.self.map(v => {
504
- const varSyntax = `\${self:${v.cleanName}}`
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.self.length} config reference(s)\n${selfList}`
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.self) {
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.self[varInfo.cleanName] = value || varInfo.defaultValue
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
- 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()