configorama 0.6.3 → 0.6.5

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.
@@ -0,0 +1,69 @@
1
+ const path = require('path')
2
+
3
+ /**
4
+ * Execute ESM file and return its export using jiti
5
+ * @param {string} filePath - Full path to the ESM file
6
+ * @param {Object} opts - Additional options including dynamicArgs
7
+ * @returns {Promise<*>} The result of executing the ESM file
8
+ */
9
+ async function executeESMFile(filePath, opts = {}) {
10
+ try {
11
+ // Use require for now since ESM dynamic import in async context is complex
12
+ // We'll use jiti to handle ESM syntax
13
+ const { createJiti } = require('jiti')
14
+ const jiti = createJiti(__filename, {
15
+ interopDefault: true
16
+ })
17
+
18
+ // Load the ESM file - resolve to absolute path first
19
+ const resolvedPath = path.resolve(filePath)
20
+ let esmModule = jiti(resolvedPath)
21
+
22
+ // Handle different export patterns - jiti returns { default: Function } for ESM default exports
23
+ if (esmModule && typeof esmModule === 'object' && esmModule.default) {
24
+ esmModule = esmModule.default
25
+ }
26
+
27
+ // For ESM files, we just return the module (object or function)
28
+ // The calling code will determine whether to execute it or not
29
+ return esmModule
30
+ } catch (err) {
31
+ throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Synchronous ESM file execution using jiti
37
+ * @param {string} filePath - Full path to the ESM file
38
+ * @param {Object} opts - Additional options including dynamicArgs
39
+ * @returns {*} The result of executing the ESM file
40
+ */
41
+ function executeESMFileSync(filePath, opts = {}) {
42
+ try {
43
+ // Use jiti to handle ESM syntax synchronously
44
+ const { createJiti } = require('jiti')
45
+ const jiti = createJiti(__filename, {
46
+ interopDefault: true
47
+ })
48
+
49
+ // Load the ESM file - resolve to absolute path first
50
+ const resolvedPath = path.resolve(filePath)
51
+ let esmModule = jiti(resolvedPath)
52
+
53
+ // Handle different export patterns - jiti returns { default: Function } for ESM default exports
54
+ if (esmModule && typeof esmModule === 'object' && esmModule.default) {
55
+ esmModule = esmModule.default
56
+ }
57
+
58
+ // For ESM files, we just return the module (object or function)
59
+ // The calling code will determine whether to execute it or not
60
+ return esmModule
61
+ } catch (err) {
62
+ throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ executeESMFile,
68
+ executeESMFileSync
69
+ }
@@ -1,9 +1,11 @@
1
1
  const json = require('./json5')
2
2
  const toml = require('./toml')
3
3
  const yaml = require('./yaml')
4
+ const ini = require('./ini')
4
5
 
5
6
  module.exports = {
6
7
  json: json,
7
8
  toml: toml,
8
- yaml: yaml
9
+ yaml: yaml,
10
+ ini: ini
9
11
  }
@@ -0,0 +1,51 @@
1
+ const INI = require('ini')
2
+ const YAML = require('./yaml')
3
+ const JSON5 = require('./json5')
4
+
5
+ function parse(contents) {
6
+ let object
7
+ try {
8
+ object = JSON.parse(JSON.stringify(INI.parse(contents)))
9
+ } catch (e) {
10
+ throw new Error(e)
11
+ }
12
+ return object
13
+ }
14
+
15
+ function dump(object) {
16
+ let ini
17
+ try {
18
+ ini = INI.stringify(object)
19
+ } catch (e) {
20
+ throw new Error(e)
21
+ }
22
+ return ini
23
+ }
24
+
25
+ function toYaml(iniContents) {
26
+ let yml
27
+ try {
28
+ yml = YAML.dump(parse(iniContents))
29
+ } catch (e) {
30
+ throw new Error(e)
31
+ }
32
+ return yml
33
+ }
34
+
35
+ function toJson(iniContents) {
36
+ let json
37
+ try {
38
+ json = JSON.stringify(parse(iniContents))
39
+ } catch (e) {
40
+ throw new Error(e)
41
+ }
42
+ return json
43
+ }
44
+
45
+ module.exports = {
46
+ parse: parse,
47
+ dump: dump,
48
+ toYaml: toYaml,
49
+ toYml: toYaml,
50
+ toJson: toJson
51
+ }
@@ -0,0 +1,133 @@
1
+ /* eslint-disable no-template-curly-in-string */
2
+ const { test } = require('uvu')
3
+ const assert = require('uvu/assert')
4
+ const ini = require('./ini')
5
+
6
+ function normalize(obj) {
7
+ return JSON.parse(JSON.stringify(obj))
8
+ }
9
+
10
+ test('ini parse basic', () => {
11
+ const iniContent = `
12
+ key=value
13
+ number=123
14
+ boolean=true
15
+
16
+ [section]
17
+ sectionKey=sectionValue
18
+ `
19
+ const result = ini.parse(iniContent)
20
+ console.log('result', result)
21
+
22
+ assert.is(result.key, 'value', 'key should be value')
23
+ assert.is(result.number, '123', 'number should be 123')
24
+ assert.is(result.boolean, true, 'boolean should be true')
25
+ assert.equal(normalize(result.section), normalize({
26
+ sectionKey: 'sectionValue'
27
+ }), 'section should be sectionValue')
28
+ })
29
+
30
+ test('ini dump basic', () => {
31
+ const obj = {
32
+ key: 'value',
33
+ number: 123,
34
+ section: {
35
+ sectionKey: 'sectionValue'
36
+ }
37
+ }
38
+
39
+ const result = ini.dump(obj)
40
+ assert.ok(result.includes('key=value'))
41
+ assert.ok(result.includes('number=123'))
42
+ assert.ok(result.includes('[section]'))
43
+ assert.ok(result.includes('sectionKey=sectionValue'))
44
+ })
45
+
46
+ test('ini toYaml', () => {
47
+ const iniContent = `
48
+ key=value
49
+ [section]
50
+ sectionKey=sectionValue
51
+ `
52
+
53
+ const result = ini.toYaml(iniContent)
54
+ assert.ok(result.includes('key: value'))
55
+ assert.ok(result.includes('section:'))
56
+ assert.ok(result.includes('sectionKey: sectionValue'))
57
+ })
58
+
59
+ test('ini toJson', () => {
60
+ const iniContent = `
61
+ key=value
62
+ [section]
63
+ sectionKey=sectionValue
64
+ `
65
+
66
+ const result = ini.toJson(iniContent)
67
+ console.log('result', result)
68
+
69
+ const parsed = JSON.parse(result)
70
+ assert.is(parsed.key, 'value')
71
+ assert.equal(parsed.section,{
72
+ sectionKey: 'sectionValue'
73
+ })
74
+ })
75
+
76
+ test.skip('ini toYaml complex', () => {
77
+ const iniContent = `
78
+ # Top level values
79
+ string=hello world
80
+ number=42
81
+ boolean=true
82
+ float=3.14
83
+
84
+ [main]
85
+ nestedString=test
86
+ nestedNumber=123
87
+ nestedBoolean=false
88
+
89
+ [main.subsection]
90
+ deeplyNested=value
91
+ array=1,2,3,4,5
92
+
93
+ [another]
94
+ key=value
95
+ `
96
+
97
+ const result = ini.toYaml(iniContent)
98
+ console.log('complex yaml result', result)
99
+
100
+ // Check top level values
101
+ assert.ok(result.includes('string: hello world'), 'string should be hello world')
102
+ assert.ok(result.includes('number: 42'), 'number should be 42')
103
+ assert.ok(result.includes('boolean: true'), 'boolean should be true')
104
+ assert.ok(result.includes('float: 3.14'), 'float should be 3.14')
105
+
106
+ // Check nested structure
107
+ assert.ok(result.includes('main:'))
108
+ assert.ok(result.includes('nestedString: test'), 'nestedString should be test')
109
+ assert.ok(result.includes('nestedNumber: 123'), 'nestedNumber should be 123')
110
+ assert.ok(result.includes('nestedBoolean: false'), 'nestedBoolean should be false')
111
+
112
+ // Check deeply nested structure
113
+ assert.ok(result.includes('subsection:'), 'subsection should be present')
114
+ assert.ok(result.includes('deeplyNested: value'), 'deeplyNested should be value')
115
+ assert.ok(result.includes('array: 1,2,3,4,5'), 'array should be 1,2,3,4,5')
116
+
117
+ // Check another section
118
+ assert.ok(result.includes('another:'))
119
+ assert.ok(result.includes('key: value'))
120
+ })
121
+
122
+ test.skip('ini parse error handling', () => {
123
+ let error
124
+ try {
125
+ const result = ini.parse('invalid ini content')
126
+ console.log('ini parse error handling', result)
127
+ } catch (e) {
128
+ error = e
129
+ }
130
+ assert.ok(error instanceof Error)
131
+ })
132
+
133
+ test.run()
@@ -45,6 +45,7 @@ function toToml(jsonContents) {
45
45
  module.exports = {
46
46
  parse: parse,
47
47
  dump: dump,
48
+ stringify: JSON.stringify,
48
49
  toYaml: toYaml,
49
50
  toYml: toYaml,
50
51
  toToml: toToml
@@ -0,0 +1,154 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ /**
5
+ * Execute TypeScript file and return its export
6
+ * @param {string} filePath - Full path to the TypeScript file
7
+ * @param {Object} opts - Additional options including dynamicArgs
8
+ * @returns {Promise<*>} The result of executing the TypeScript file
9
+ */
10
+ async function executeTypeScriptFile(filePath, opts = {}) {
11
+ // Check if tsx is available first (preferred)
12
+ let useTsx = false
13
+ try {
14
+ require.resolve('tsx/cjs/api')
15
+ useTsx = true
16
+ } catch (err) {
17
+ // Fallback to ts-node if tsx is not available
18
+ try {
19
+ require.resolve('ts-node/register')
20
+ } catch (tsNodeErr) {
21
+ throw new Error(
22
+ 'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
23
+ 'Please install one of them:\n' +
24
+ ' npm install tsx --save-dev (recommended)\n' +
25
+ ' npm install ts-node typescript --save-dev'
26
+ )
27
+ }
28
+ }
29
+
30
+ // Clear require cache to ensure fresh execution
31
+ const resolvedPath = require.resolve(filePath)
32
+ delete require.cache[resolvedPath]
33
+
34
+ let tsFile
35
+ if (useTsx) {
36
+ // Use tsx for modern, fast TypeScript execution
37
+ const { register } = require('tsx/cjs/api')
38
+ const restore = register()
39
+ try {
40
+ tsFile = require(filePath)
41
+ } catch (err) {
42
+ throw new Error(`Failed to load TypeScript file: ${err.message}`)
43
+ } finally {
44
+ restore()
45
+ }
46
+ } else {
47
+ // Fallback to ts-node
48
+ try {
49
+ require('ts-node/register')
50
+ tsFile = require(filePath)
51
+ } catch (err) {
52
+ throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
53
+ }
54
+ }
55
+
56
+ if (typeof tsFile !== 'function') {
57
+ return tsFile
58
+ } else {
59
+ let tsArgs = opts.dynamicArgs || {}
60
+ if (tsArgs && typeof tsArgs === 'function') {
61
+ tsArgs = tsArgs()
62
+ }
63
+
64
+ try {
65
+ const result = tsFile(tsArgs)
66
+
67
+ // Handle promises
68
+ if (result && typeof result.then === 'function') {
69
+ return await result
70
+ }
71
+
72
+ return result
73
+ } catch (err) {
74
+ throw new Error(`Error executing TypeScript function: ${err.message}`)
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Synchronous TypeScript file execution (using tsx with sync API)
81
+ * @param {string} filePath - Full path to the TypeScript file
82
+ * @param {Object} opts - Additional options including dynamicArgs
83
+ * @returns {*} The result of executing the TypeScript file
84
+ */
85
+ function executeTypeScriptFileSync(filePath, opts = {}) {
86
+ // Check if tsx is available first (preferred)
87
+ let useTsx = false
88
+ try {
89
+ require.resolve('tsx/cjs/api')
90
+ useTsx = true
91
+ } catch (err) {
92
+ // Fallback to ts-node if tsx is not available
93
+ try {
94
+ require.resolve('ts-node/register')
95
+ } catch (tsNodeErr) {
96
+ throw new Error(
97
+ 'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
98
+ 'Please install one of them:\n' +
99
+ ' npm install tsx --save-dev (recommended)\n' +
100
+ ' npm install ts-node typescript --save-dev'
101
+ )
102
+ }
103
+ }
104
+
105
+ // Clear require cache to ensure fresh execution
106
+ const resolvedPath = require.resolve(filePath)
107
+ delete require.cache[resolvedPath]
108
+
109
+ let tsFile
110
+ if (useTsx) {
111
+ // Use tsx for modern, fast TypeScript execution
112
+ const { register } = require('tsx/cjs/api')
113
+ const restore = register()
114
+ try {
115
+ tsFile = require(filePath)
116
+ } catch (err) {
117
+ throw new Error(`Failed to load TypeScript file: ${err.message}`)
118
+ } finally {
119
+ restore()
120
+ }
121
+ } else {
122
+ // Fallback to ts-node
123
+ try {
124
+ require('ts-node/register')
125
+ tsFile = require(filePath)
126
+ } catch (err) {
127
+ throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
128
+ }
129
+ }
130
+
131
+ if (typeof tsFile !== 'function') {
132
+ return tsFile
133
+ } else {
134
+ let tsArgs = opts.dynamicArgs || {}
135
+ if (tsArgs && typeof tsArgs === 'function') {
136
+ tsArgs = tsArgs()
137
+ }
138
+
139
+ try {
140
+ const result = tsFile(tsArgs)
141
+
142
+ // Note: For sync execution, we don't await promises
143
+ // If the function returns a promise, it will be resolved by the calling code
144
+ return result
145
+ } catch (err) {
146
+ throw new Error(`Error executing TypeScript function: ${err.message}`)
147
+ }
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ executeTypeScriptFile,
153
+ executeTypeScriptFileSync
154
+ }
@@ -110,7 +110,7 @@ resolvedDomainNameTwo: \${domainsTwo.\${opt:stage, "prod"}}
110
110
  assert.equal(result, expected)
111
111
  })
112
112
 
113
- test('preProcess - should wrap variables in quotes inside array brackets', () => {
113
+ test('preProcess - should wrap variables in quotes inside array brackets two', () => {
114
114
  const input = `
115
115
  service: my-service
116
116
  custom:
@@ -0,0 +1,252 @@
1
+ const cronRefSyntax = RegExp(/^cron\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
2
+
3
+ /**
4
+ * Convert human-readable strings to cron expressions
5
+ * Based on common patterns and schedules
6
+ */
7
+ function parseCronExpression(input) {
8
+ if (!input || typeof input !== 'string') {
9
+ throw new Error('Cron input must be a non-empty string')
10
+ }
11
+
12
+ const normalizedInput = input.toLowerCase().trim()
13
+
14
+ // Pre-defined common cron expressions
15
+ const cronMap = {
16
+ // Every minute/hour/day patterns
17
+ 'every minute': '* * * * *',
18
+ 'every hour': '0 * * * *',
19
+ 'every day': '0 0 * * *',
20
+ 'every week': '0 0 * * 0',
21
+ 'every month': '0 0 1 * *',
22
+ 'every year': '0 0 1 1 *',
23
+ 'yearly': '0 0 1 1 *',
24
+ 'annually': '0 0 1 1 *',
25
+ 'monthly': '0 0 1 * *',
26
+ 'weekly': '0 0 * * 0',
27
+ 'daily': '0 0 * * *',
28
+ 'hourly': '0 * * * *',
29
+
30
+ // Common business schedules
31
+ 'weekdays': '0 0 * * 1-5',
32
+ 'weekends': '0 0 * * 0,6',
33
+ 'business hours': '0 9-17 * * 1-5',
34
+ 'after hours': '0 18-8 * * *',
35
+
36
+ // Specific times
37
+ 'midnight': '0 0 * * *',
38
+ 'noon': '0 12 * * *',
39
+ 'morning': '0 9 * * *',
40
+ 'evening': '0 18 * * *',
41
+
42
+ // Interval patterns
43
+ 'every 5 minutes': '*/5 * * * *',
44
+ 'every 10 minutes': '*/10 * * * *',
45
+ 'every 15 minutes': '*/15 * * * *',
46
+ 'every 30 minutes': '*/30 * * * *',
47
+ 'every 2 hours': '0 */2 * * *',
48
+ 'every 3 hours': '0 */3 * * *',
49
+ 'every 6 hours': '0 */6 * * *',
50
+ 'every 12 hours': '0 */12 * * *',
51
+
52
+ // Days of week
53
+ 'monday': '0 0 * * 1',
54
+ 'tuesday': '0 0 * * 2',
55
+ 'wednesday': '0 0 * * 3',
56
+ 'thursday': '0 0 * * 4',
57
+ 'friday': '0 0 * * 5',
58
+ 'saturday': '0 0 * * 6',
59
+ 'sunday': '0 0 * * 0',
60
+
61
+ // Monthly patterns
62
+ 'first day of month': '0 0 1 * *',
63
+ 'last day of month': '0 0 L * *',
64
+ 'middle of month': '0 0 15 * *',
65
+
66
+ // Special patterns
67
+ 'never': '0 0 30 2 *', // Feb 30th (never occurs)
68
+ 'reboot': '@reboot',
69
+ 'startup': '@reboot'
70
+ }
71
+
72
+ // Check direct mapping first
73
+ if (cronMap[normalizedInput]) {
74
+ return cronMap[normalizedInput]
75
+ }
76
+
77
+ // Parse "at X:XX" patterns (e.g., "at 9:30", "at 14:00")
78
+ const atTimeMatch = normalizedInput.match(/^at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
79
+ if (atTimeMatch) {
80
+ let hour = parseInt(atTimeMatch[1])
81
+ const minute = parseInt(atTimeMatch[2])
82
+ const amPm = atTimeMatch[4]
83
+
84
+ if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
85
+ hour += 12
86
+ } else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
87
+ hour = 0
88
+ }
89
+
90
+ return `${minute} ${hour} * * *`
91
+ }
92
+
93
+ // Parse "every X minutes/hours/days" patterns
94
+ const everyMatch = normalizedInput.match(/^every (\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
95
+ if (everyMatch) {
96
+ const interval = parseInt(everyMatch[1])
97
+ const unit = everyMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
98
+
99
+ switch (unit) {
100
+ case 'minute':
101
+ return `*/${interval} * * * *`
102
+ case 'hour':
103
+ return `0 */${interval} * * *`
104
+ case 'day':
105
+ return `0 0 */${interval} * *`
106
+ case 'week':
107
+ return `0 0 * * 0/${interval}`
108
+ case 'month':
109
+ return `0 0 1 */${interval} *`
110
+ default:
111
+ throw new Error(`Unsupported interval unit: ${unit}`)
112
+ }
113
+ }
114
+
115
+ // Parse "X minute(s)/hour(s)/day(s)" patterns (e.g., "1 minute", "5 minutes", "1 hour")
116
+ const intervalMatch = normalizedInput.match(/^(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
117
+ if (intervalMatch) {
118
+ const interval = parseInt(intervalMatch[1])
119
+ const unit = intervalMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
120
+
121
+ switch (unit) {
122
+ case 'minute':
123
+ return `*/${interval} * * * *`
124
+ case 'hour':
125
+ return `0 */${interval} * * *`
126
+ case 'day':
127
+ return `0 0 */${interval} * *`
128
+ case 'week':
129
+ return `0 0 * * 0/${interval}`
130
+ case 'month':
131
+ return `0 0 1 */${interval} *`
132
+ default:
133
+ throw new Error(`Unsupported interval unit: ${unit}`)
134
+ }
135
+ }
136
+
137
+ // Parse "on Xst/nd/rd/th of month at time" patterns (e.g., "on 1st of month at 00:00")
138
+ const ordinalDateMatch = normalizedInput.match(/^on (\d+)(?:st|nd|rd|th) of month at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
139
+ if (ordinalDateMatch) {
140
+ const dayOfMonth = parseInt(ordinalDateMatch[1])
141
+ let hour = parseInt(ordinalDateMatch[2])
142
+ const minute = parseInt(ordinalDateMatch[3])
143
+ const amPm = ordinalDateMatch[5]
144
+
145
+ if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
146
+ hour += 12
147
+ } else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
148
+ hour = 0
149
+ }
150
+
151
+ return `${minute} ${hour} ${dayOfMonth} * *`
152
+ }
153
+
154
+ // Parse "on weekday at time" patterns (e.g., "on monday at 9:00")
155
+ const weekdayTimeMatch = normalizedInput.match(/^on ((?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?:,(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday))*?) at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
156
+ if (weekdayTimeMatch) {
157
+ const dayMap = {
158
+ 'sunday': 0, 'monday': 1, 'tuesday': 2, 'wednesday': 3,
159
+ 'thursday': 4, 'friday': 5, 'saturday': 6
160
+ }
161
+
162
+ // Extract all days from the match
163
+ const days = weekdayTimeMatch[1].split(',').map(day => day.trim())
164
+ const dayOfWeek = days.map(day => dayMap[day.toLowerCase()]).join(',')
165
+
166
+ let hour = parseInt(weekdayTimeMatch[2])
167
+ const minute = parseInt(weekdayTimeMatch[3])
168
+ const amPm = weekdayTimeMatch[5]
169
+
170
+ if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
171
+ hour += 12
172
+ } else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
173
+ hour = 0
174
+ }
175
+
176
+ return `${minute} ${hour} * * ${dayOfWeek}`
177
+ }
178
+
179
+ // Parse "on weekdays/weekends at time" patterns (e.g., "on weekdays at 9:00")
180
+ const weekdaysTimeMatch = normalizedInput.match(/^on (weekdays|weekends) at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
181
+ if (weekdaysTimeMatch) {
182
+ const dayRange = weekdaysTimeMatch[1].toLowerCase() === 'weekdays' ? '1-5' : '0,6'
183
+ let hour = parseInt(weekdaysTimeMatch[2])
184
+ const minute = parseInt(weekdaysTimeMatch[3])
185
+ const amPm = weekdaysTimeMatch[5]
186
+
187
+ if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
188
+ hour += 12
189
+ } else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
190
+ hour = 0
191
+ }
192
+
193
+ return `${minute} ${hour} * * ${dayRange}`
194
+ }
195
+
196
+ // Check if it's already a valid cron expression (5 or 6 parts)
197
+ const parts = normalizedInput.split(/\s+/)
198
+ if (parts.length === 5 || parts.length === 6) {
199
+ // Basic validation for cron format
200
+ if (parts.every(part => /^[@*\d,\-\/]+$/.test(part) || part.startsWith('@'))) {
201
+ return normalizedInput
202
+ }
203
+ }
204
+
205
+ // If no pattern matches, throw an error with suggestions
206
+ const suggestions = Object.keys(cronMap).slice(0, 10).join(', ')
207
+ throw new Error(`Unrecognized cron pattern: "${input}". Supported patterns include: ${suggestions}`)
208
+ }
209
+
210
+ function getValueFromCron(variableString) {
211
+ // Get value from cron(expression)
212
+ const cronExpression = variableString.match(/cron\((.*)\)/)[1]
213
+ // console.log('cronExpression', cronExpression)
214
+
215
+ if (!cronExpression || cronExpression.trim() === '') {
216
+ throw new Error(`Invalid variable syntax for cron reference "${variableString}".
217
+
218
+ \${cron} variable must have a pattern.
219
+
220
+ Examples:
221
+ \${cron("every minute")}
222
+ \${cron("weekdays")}
223
+ \${cron("at 9:30")}
224
+ \${cron("every 5 minutes")}
225
+ `)
226
+ }
227
+
228
+ // Remove surrounding quotes if present
229
+ const cleanExpression = cronExpression.replace(/^['"`](.*)['"`]$/, '$1')
230
+
231
+ // If already a cron expression, return it
232
+ if (cleanExpression.match(/^[\*\/,\-\d]+$/)) {
233
+ return cleanExpression
234
+ }
235
+
236
+ try {
237
+ const resolvedCron = parseCronExpression(cleanExpression)
238
+ // console.log('resolvedCron', resolvedCron)
239
+ return Promise.resolve(resolvedCron)
240
+ } catch (error) {
241
+ throw new Error(`Failed to parse cron expression "${cleanExpression}": ${error.message}`)
242
+ }
243
+ }
244
+
245
+ module.exports = {
246
+ type: 'cron',
247
+ prefix: 'cron',
248
+ match: cronRefSyntax,
249
+ resolver: getValueFromCron,
250
+ // Export the parser for testing
251
+ _parseCronExpression: parseCronExpression
252
+ }