figranium 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -674
- package/README.md +336 -336
- package/agent.js +1 -1
- package/bin/cli.js +149 -149
- package/common-utils.js +211 -211
- package/dist/assets/{favicon-DmUMR1rm.svg → favicon-DXDXzv5K.svg} +290 -290
- package/dist/assets/index-BaVlGc48.js +18 -0
- package/dist/assets/index-T2xxnq_A.css +1 -0
- package/dist/favicon.svg +290 -290
- package/dist/figranium_icon.svg +290 -290
- package/dist/figranium_logo.svg +60 -60
- package/dist/index.html +26 -26
- package/dist/novnc.html +108 -108
- package/dist/styles.css +86 -86
- package/extraction-worker.js +211 -204
- package/headful.js +584 -569
- package/html-utils.js +24 -24
- package/package.json +82 -82
- package/proxy-rotation.js +261 -261
- package/proxy-utils.js +84 -84
- package/public/favicon.svg +290 -290
- package/public/figranium_icon.svg +290 -290
- package/public/figranium_logo.svg +60 -60
- package/public/novnc.html +108 -108
- package/public/styles.css +86 -86
- package/scrape.js +389 -389
- package/scripts/postinstall.js +21 -21
- package/server.js +626 -625
- package/src/server/cron-parser.js +325 -316
- package/src/server/routes/schedules.js +171 -171
- package/src/server/scheduler.js +379 -381
- package/url-utils.js +339 -295
- package/user-agent-settings.js +76 -76
- package/dist/assets/index-B1CypY6C.css +0 -1
- package/dist/assets/index-B295GWry.js +0 -18
|
@@ -1,316 +1,325 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zero-dependency cron expression parser, builder, and scheduler utility.
|
|
3
|
-
* Supports standard 5-field cron: minute hour day-of-month month day-of-week
|
|
4
|
-
* Plus presets: @yearly, @monthly, @weekly, @daily, @hourly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const PRESETS = {
|
|
8
|
-
'@yearly': '0 0 1 1 *',
|
|
9
|
-
'@annually': '0 0 1 1 *',
|
|
10
|
-
'@monthly': '0 0 1 * *',
|
|
11
|
-
'@weekly': '0 0 * * 0',
|
|
12
|
-
'@daily': '0 0 * * *',
|
|
13
|
-
'@midnight': '0 0 * * *',
|
|
14
|
-
'@hourly': '0 * * * *',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const FIELD_RANGES = [
|
|
18
|
-
{ name: 'minute', min: 0, max: 59 },
|
|
19
|
-
{ name: 'hour', min: 0, max: 23 },
|
|
20
|
-
{ name: 'dayOfMonth', min: 1, max: 31 },
|
|
21
|
-
{ name: 'month', min: 1, max: 12 },
|
|
22
|
-
{ name: 'dayOfWeek', min: 0, max: 7 }, // 0 and 7 both represent Sunday
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Parse a single cron field into a Set of valid integer values.
|
|
27
|
-
*/
|
|
28
|
-
function parseField(field, min, max) {
|
|
29
|
-
const values = new Set();
|
|
30
|
-
|
|
31
|
-
for (const part of field.split(',')) {
|
|
32
|
-
const stepMatch = part.match(/^(.+)\/(\d+)$/);
|
|
33
|
-
let base = stepMatch ? stepMatch[1] : part;
|
|
34
|
-
const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
|
|
35
|
-
|
|
36
|
-
if (step < 1) throw new Error(`Invalid step: ${part}`);
|
|
37
|
-
|
|
38
|
-
let rangeStart = min;
|
|
39
|
-
let rangeEnd = max;
|
|
40
|
-
|
|
41
|
-
if (base === '*') {
|
|
42
|
-
// full range
|
|
43
|
-
} else {
|
|
44
|
-
const rangeMatch = base.match(/^(\d+)-(\d+)$/);
|
|
45
|
-
if (rangeMatch) {
|
|
46
|
-
rangeStart = parseInt(rangeMatch[1], 10);
|
|
47
|
-
rangeEnd = parseInt(rangeMatch[2], 10);
|
|
48
|
-
} else {
|
|
49
|
-
const val = parseInt(base, 10);
|
|
50
|
-
if (isNaN(val)) throw new Error(`Invalid cron field value: ${base}`);
|
|
51
|
-
if (!stepMatch) {
|
|
52
|
-
// single value, clamp dayOfWeek 7 → 0
|
|
53
|
-
values.add(val === 7 && max === 7 ? 0 : val);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
rangeStart = val;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
for (let i = rangeStart; i <= rangeEnd; i += step) {
|
|
61
|
-
values.add(i === 7 && max === 7 ? 0 : i);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return values;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Parse a full cron expression into an object with Sets for each field.
|
|
70
|
-
* @param {string} expression
|
|
71
|
-
* @returns {{ minute: Set<number>, hour: Set<number>, dayOfMonth: Set<number>, month: Set<number>, dayOfWeek: Set<number> }}
|
|
72
|
-
*/
|
|
73
|
-
function parseCron(expression) {
|
|
74
|
-
if (!expression || typeof expression !== 'string') {
|
|
75
|
-
throw new Error('Invalid cron expression');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const expr = expression.trim().toLowerCase();
|
|
79
|
-
const resolved = PRESETS[expr] || expr;
|
|
80
|
-
const parts = resolved.split(/\s+/);
|
|
81
|
-
|
|
82
|
-
if (parts.length !== 5) {
|
|
83
|
-
throw new Error(`Cron expression must have 5 fields, got ${parts.length}: "${expression}"`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const result = {};
|
|
87
|
-
for (let i = 0; i < 5; i++) {
|
|
88
|
-
const { name, min, max } = FIELD_RANGES[i];
|
|
89
|
-
result[name] = parseField(parts[i], min, max);
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if a cron expression is valid.
|
|
96
|
-
* @param {string} expression
|
|
97
|
-
* @returns {boolean}
|
|
98
|
-
*/
|
|
99
|
-
function isValidCron(expression) {
|
|
100
|
-
try {
|
|
101
|
-
parseCron(expression);
|
|
102
|
-
return true;
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get the next run time after `from` that matches the cron expression.
|
|
110
|
-
* @param {string} expression
|
|
111
|
-
* @param {Date} [from]
|
|
112
|
-
* @returns {Date}
|
|
113
|
-
*/
|
|
114
|
-
function getNextRun(expression, from) {
|
|
115
|
-
const fields = parseCron(expression);
|
|
116
|
-
const d = from ? new Date(from) : new Date();
|
|
117
|
-
// Start from the next minute
|
|
118
|
-
d.setSeconds(0, 0);
|
|
119
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
for (let i = 0; i <
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return `${min} ${hr}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency cron expression parser, builder, and scheduler utility.
|
|
3
|
+
* Supports standard 5-field cron: minute hour day-of-month month day-of-week
|
|
4
|
+
* Plus presets: @yearly, @monthly, @weekly, @daily, @hourly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PRESETS = {
|
|
8
|
+
'@yearly': '0 0 1 1 *',
|
|
9
|
+
'@annually': '0 0 1 1 *',
|
|
10
|
+
'@monthly': '0 0 1 * *',
|
|
11
|
+
'@weekly': '0 0 * * 0',
|
|
12
|
+
'@daily': '0 0 * * *',
|
|
13
|
+
'@midnight': '0 0 * * *',
|
|
14
|
+
'@hourly': '0 * * * *',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const FIELD_RANGES = [
|
|
18
|
+
{ name: 'minute', min: 0, max: 59 },
|
|
19
|
+
{ name: 'hour', min: 0, max: 23 },
|
|
20
|
+
{ name: 'dayOfMonth', min: 1, max: 31 },
|
|
21
|
+
{ name: 'month', min: 1, max: 12 },
|
|
22
|
+
{ name: 'dayOfWeek', min: 0, max: 7 }, // 0 and 7 both represent Sunday
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a single cron field into a Set of valid integer values.
|
|
27
|
+
*/
|
|
28
|
+
function parseField(field, min, max) {
|
|
29
|
+
const values = new Set();
|
|
30
|
+
|
|
31
|
+
for (const part of field.split(',')) {
|
|
32
|
+
const stepMatch = part.match(/^(.+)\/(\d+)$/);
|
|
33
|
+
let base = stepMatch ? stepMatch[1] : part;
|
|
34
|
+
const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
|
|
35
|
+
|
|
36
|
+
if (step < 1) throw new Error(`Invalid step: ${part}`);
|
|
37
|
+
|
|
38
|
+
let rangeStart = min;
|
|
39
|
+
let rangeEnd = max;
|
|
40
|
+
|
|
41
|
+
if (base === '*') {
|
|
42
|
+
// full range
|
|
43
|
+
} else {
|
|
44
|
+
const rangeMatch = base.match(/^(\d+)-(\d+)$/);
|
|
45
|
+
if (rangeMatch) {
|
|
46
|
+
rangeStart = parseInt(rangeMatch[1], 10);
|
|
47
|
+
rangeEnd = parseInt(rangeMatch[2], 10);
|
|
48
|
+
} else {
|
|
49
|
+
const val = parseInt(base, 10);
|
|
50
|
+
if (isNaN(val)) throw new Error(`Invalid cron field value: ${base}`);
|
|
51
|
+
if (!stepMatch) {
|
|
52
|
+
// single value, clamp dayOfWeek 7 → 0
|
|
53
|
+
values.add(val === 7 && max === 7 ? 0 : val);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
rangeStart = val;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = rangeStart; i <= rangeEnd; i += step) {
|
|
61
|
+
values.add(i === 7 && max === 7 ? 0 : i);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a full cron expression into an object with Sets for each field.
|
|
70
|
+
* @param {string} expression
|
|
71
|
+
* @returns {{ minute: Set<number>, hour: Set<number>, dayOfMonth: Set<number>, month: Set<number>, dayOfWeek: Set<number> }}
|
|
72
|
+
*/
|
|
73
|
+
function parseCron(expression) {
|
|
74
|
+
if (!expression || typeof expression !== 'string') {
|
|
75
|
+
throw new Error('Invalid cron expression');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const expr = expression.trim().toLowerCase();
|
|
79
|
+
const resolved = PRESETS[expr] || expr;
|
|
80
|
+
const parts = resolved.split(/\s+/);
|
|
81
|
+
|
|
82
|
+
if (parts.length !== 5) {
|
|
83
|
+
throw new Error(`Cron expression must have 5 fields, got ${parts.length}: "${expression}"`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = {};
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
const { name, min, max } = FIELD_RANGES[i];
|
|
89
|
+
result[name] = parseField(parts[i], min, max);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a cron expression is valid.
|
|
96
|
+
* @param {string} expression
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isValidCron(expression) {
|
|
100
|
+
try {
|
|
101
|
+
parseCron(expression);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the next run time after `from` that matches the cron expression.
|
|
110
|
+
* @param {string} expression
|
|
111
|
+
* @param {Date} [from]
|
|
112
|
+
* @returns {Date}
|
|
113
|
+
*/
|
|
114
|
+
function getNextRun(expression, from) {
|
|
115
|
+
const fields = parseCron(expression);
|
|
116
|
+
const d = from ? new Date(from) : new Date();
|
|
117
|
+
// Start from the next minute
|
|
118
|
+
d.setSeconds(0, 0);
|
|
119
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
120
|
+
|
|
121
|
+
// ⚡ Bolt: Optimized skipping logic reduces iterations from ~500,000 to < 100 for sparse crons
|
|
122
|
+
// Instead of incrementing by minute, we jump to the next potential valid month, day, or hour.
|
|
123
|
+
for (let i = 0; i < 10000; i++) {
|
|
124
|
+
if (!fields.month.has(d.getMonth() + 1)) {
|
|
125
|
+
d.setMonth(d.getMonth() + 1, 1);
|
|
126
|
+
d.setHours(0, 0, 0, 0);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!fields.dayOfMonth.has(d.getDate()) || !fields.dayOfWeek.has(d.getDay())) {
|
|
130
|
+
d.setDate(d.getDate() + 1);
|
|
131
|
+
d.setHours(0, 0, 0, 0);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!fields.hour.has(d.getHours())) {
|
|
135
|
+
d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!fields.minute.has(d.getMinutes())) {
|
|
139
|
+
d.setMinutes(d.getMinutes() + 1);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
return d;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new Error(`Could not find next run for expression: ${expression}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Convert a no-code schedule config object into a cron expression.
|
|
150
|
+
* @param {object} config
|
|
151
|
+
* @param {string} config.frequency - 'interval' | 'hourly' | 'daily' | 'weekly' | 'monthly'
|
|
152
|
+
* @param {number} [config.intervalMinutes] - for 'interval' frequency
|
|
153
|
+
* @param {number} [config.hour] - hour (0-23)
|
|
154
|
+
* @param {number} [config.minute] - minute (0-59)
|
|
155
|
+
* @param {number[]} [config.daysOfWeek] - for 'weekly' (0=Sun..6=Sat)
|
|
156
|
+
* @param {number} [config.dayOfMonth] - for 'monthly' (1-31)
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
function scheduleToCron(config) {
|
|
160
|
+
if (!config || !config.frequency) {
|
|
161
|
+
throw new Error('Schedule config must include a frequency');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const min = config.minute ?? 0;
|
|
165
|
+
const hr = config.hour ?? 0;
|
|
166
|
+
|
|
167
|
+
switch (config.frequency) {
|
|
168
|
+
case 'interval': {
|
|
169
|
+
const interval = config.intervalMinutes || 5;
|
|
170
|
+
if (interval <= 0 || interval > 1440) throw new Error('Interval must be 1-1440 minutes');
|
|
171
|
+
|
|
172
|
+
if (interval <= 60) {
|
|
173
|
+
if (60 % interval === 0) {
|
|
174
|
+
return `*/${interval} * * * *`;
|
|
175
|
+
}
|
|
176
|
+
const minutes = [];
|
|
177
|
+
for (let m = 0; m < 60; m += interval) {
|
|
178
|
+
minutes.push(m);
|
|
179
|
+
}
|
|
180
|
+
return `${minutes.join(',')} * * * *`;
|
|
181
|
+
} else {
|
|
182
|
+
// For intervals > 60m, use hours/minutes
|
|
183
|
+
const hrs = Math.floor(interval / 60);
|
|
184
|
+
const mins = interval % 60;
|
|
185
|
+
if (hrs < 24 && 24 % hrs === 0 && mins === 0) {
|
|
186
|
+
return `0 */${hrs} * * *`;
|
|
187
|
+
}
|
|
188
|
+
// Fallback to daily if too complex, or just return hourly with 0 min
|
|
189
|
+
return `0 */${Math.max(1, hrs)} * * *`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
case 'hourly':
|
|
193
|
+
return `${min} * * * *`;
|
|
194
|
+
case 'daily':
|
|
195
|
+
return `${min} ${hr} * * *`;
|
|
196
|
+
case 'weekly': {
|
|
197
|
+
const days = Array.isArray(config.daysOfWeek) && config.daysOfWeek.length > 0
|
|
198
|
+
? config.daysOfWeek.sort().join(',')
|
|
199
|
+
: '*';
|
|
200
|
+
return `${min} ${hr} * * ${days}`;
|
|
201
|
+
}
|
|
202
|
+
case 'monthly': {
|
|
203
|
+
const dom = config.dayOfMonth || 1;
|
|
204
|
+
return `${min} ${hr} ${dom} * *`;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
throw new Error(`Unknown frequency: ${config.frequency}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Produce a human-readable description of a cron expression.
|
|
213
|
+
* @param {string} expression
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
function describeCron(expression) {
|
|
217
|
+
if (!expression || typeof expression !== 'string') return '';
|
|
218
|
+
|
|
219
|
+
const expr = expression.trim().toLowerCase();
|
|
220
|
+
if (PRESETS[expr]) {
|
|
221
|
+
const labels = {
|
|
222
|
+
'@yearly': 'Every year on January 1st at midnight',
|
|
223
|
+
'@annually': 'Every year on January 1st at midnight',
|
|
224
|
+
'@monthly': 'First day of every month at midnight',
|
|
225
|
+
'@weekly': 'Every Sunday at midnight',
|
|
226
|
+
'@daily': 'Every day at midnight',
|
|
227
|
+
'@midnight': 'Every day at midnight',
|
|
228
|
+
'@hourly': 'Every hour',
|
|
229
|
+
};
|
|
230
|
+
return labels[expr] || expr;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const parts = expr.split(/\s+/);
|
|
235
|
+
if (parts.length !== 5) return expression;
|
|
236
|
+
|
|
237
|
+
const [minPart, hrPart, domPart, monPart, dowPart] = parts;
|
|
238
|
+
|
|
239
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
240
|
+
const monthNames = ['', 'January', 'February', 'March', 'April', 'May', 'June',
|
|
241
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
242
|
+
|
|
243
|
+
const formatTime = (h, m) => {
|
|
244
|
+
const hour = parseInt(h, 10);
|
|
245
|
+
const minute = parseInt(m, 10);
|
|
246
|
+
if (isNaN(hour) || isNaN(minute)) return '';
|
|
247
|
+
const ampm = hour >= 12 ? 'PM' : 'AM';
|
|
248
|
+
const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
|
249
|
+
return `${h12}:${String(minute).padStart(2, '0')} ${ampm}`;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Every N minutes
|
|
253
|
+
const stepMatch = minPart.match(/^\*\/(\d+)$/);
|
|
254
|
+
if (stepMatch && hrPart === '*' && domPart === '*' && monPart === '*' && dowPart === '*') {
|
|
255
|
+
const n = parseInt(stepMatch[1], 10);
|
|
256
|
+
if (n === 1) return 'Every minute';
|
|
257
|
+
return `Every ${n} minutes`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Every minute
|
|
261
|
+
if (minPart === '*' && hrPart === '*' && domPart === '*' && monPart === '*' && dowPart === '*') {
|
|
262
|
+
return 'Every minute';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Comma separated minutes (common for non-divisible intervals)
|
|
266
|
+
if (hrPart === '*' && domPart === '*' && monPart === '*' && dowPart === '*' && minPart.includes(',')) {
|
|
267
|
+
const mins = minPart.split(',');
|
|
268
|
+
if (mins.every(m => /^\d+$/.test(m))) {
|
|
269
|
+
const diffs = [];
|
|
270
|
+
for (let i = 1; i < mins.length; i++) diffs.push(parseInt(mins[i]) - parseInt(mins[i-1]));
|
|
271
|
+
const uniqueDiffs = new Set(diffs);
|
|
272
|
+
if (uniqueDiffs.size === 1) {
|
|
273
|
+
return `Every ${uniqueDiffs.values().next().value} minutes`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Specific minute, every hour
|
|
279
|
+
if (/^\d+$/.test(minPart) && hrPart === '*' && domPart === '*' && monPart === '*' && dowPart === '*') {
|
|
280
|
+
const m = parseInt(minPart, 10);
|
|
281
|
+
return `Every hour at :${String(m).padStart(2, '0')}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Daily at specific time
|
|
285
|
+
if (/^\d+$/.test(minPart) && /^\d+$/.test(hrPart) && domPart === '*' && monPart === '*' && dowPart === '*') {
|
|
286
|
+
return `Every day at ${formatTime(hrPart, minPart)}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Weekly
|
|
290
|
+
if (/^\d+$/.test(minPart) && /^\d+$/.test(hrPart) && domPart === '*' && monPart === '*' && dowPart !== '*') {
|
|
291
|
+
const dows = dowPart.split(',').map(d => {
|
|
292
|
+
const n = parseInt(d, 10);
|
|
293
|
+
return dayNames[n === 7 ? 0 : n] || d;
|
|
294
|
+
});
|
|
295
|
+
if (dows.length === 5 && !dows.includes('Saturday') && !dows.includes('Sunday')) {
|
|
296
|
+
return `Every weekday at ${formatTime(hrPart, minPart)}`;
|
|
297
|
+
}
|
|
298
|
+
if (dows.length === 2 && dows.includes('Saturday') && dows.includes('Sunday')) {
|
|
299
|
+
return `Every weekend at ${formatTime(hrPart, minPart)}`;
|
|
300
|
+
}
|
|
301
|
+
return `Every ${dows.join(', ')} at ${formatTime(hrPart, minPart)}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Monthly
|
|
305
|
+
if (/^\d+$/.test(minPart) && /^\d+$/.test(hrPart) && /^\d+$/.test(domPart) && monPart === '*' && dowPart === '*') {
|
|
306
|
+
const dom = parseInt(domPart, 10);
|
|
307
|
+
const suffix = dom === 1 ? 'st' : dom === 2 ? 'nd' : dom === 3 ? 'rd' : 'th';
|
|
308
|
+
return `Monthly on the ${dom}${suffix} at ${formatTime(hrPart, minPart)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Fallback: return a structured description
|
|
312
|
+
return expression;
|
|
313
|
+
} catch {
|
|
314
|
+
return expression;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
parseCron,
|
|
320
|
+
isValidCron,
|
|
321
|
+
getNextRun,
|
|
322
|
+
scheduleToCron,
|
|
323
|
+
describeCron,
|
|
324
|
+
PRESETS,
|
|
325
|
+
};
|