configorama 0.11.2 → 1.0.0

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