cron-human 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.
- package/LICENSE +21 -0
- package/README.md +282 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +76 -0
- package/dist/lib.d.ts +7 -0
- package/dist/lib.js +118 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akin Ibitoye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# cron-human
|
|
2
|
+
|
|
3
|
+
> A command-line tool that converts cron expressions into human-readable English and calculates the next scheduled run times.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/cron-human)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## What is this?
|
|
9
|
+
|
|
10
|
+
If you've ever looked at a cron expression like `0 12 * * MON-FRI` and wondered "when does this actually run?", this tool is for you. It translates cryptic cron syntax into plain English and shows you exactly when the job will execute next.
|
|
11
|
+
|
|
12
|
+
Perfect for:
|
|
13
|
+
- 🔍 **Debugging cron jobs** - Verify your cron expressions do what you expect
|
|
14
|
+
- � **Understanding existing schedules** - Decode cron expressions in config files
|
|
15
|
+
- ✅ **Validating syntax** - Catch errors before deploying to production
|
|
16
|
+
- 🌐 **Timezone conversions** - See execution times in different timezones
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- �🗣 **Human readable** - Converts `*/5 * * * *` to "Every 5 minutes"
|
|
21
|
+
- 📅 **Next run times** - Calculates exact dates for upcoming executions
|
|
22
|
+
- 🌍 **Timezone aware** - Supports IANA timezone identifiers (e.g., `America/New_York`, `Asia/Tokyo`)
|
|
23
|
+
- 🤖 **JSON output** - Machine-readable format for scripting and automation
|
|
24
|
+
- ✅ **Strict validation** - Catches invalid expressions and provides clear error messages
|
|
25
|
+
- ⏱️ **Seconds support** - Handle 6-field cron expressions with `--seconds` flag
|
|
26
|
+
- 🏷️ **Macro shortcuts** - Supports `@daily`, `@hourly`, `@weekly`, etc.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### One-time use with npx
|
|
31
|
+
|
|
32
|
+
No installation needed - run directly with npx:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx cron-human "*/5 * * * *"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Global installation
|
|
39
|
+
|
|
40
|
+
Install once, use anywhere:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g cron-human
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then use it directly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cron-human "0 12 * * MON-FRI"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Basic Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cron-human "<cron-expression>"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Example:**
|
|
61
|
+
```bash
|
|
62
|
+
$ cron-human "30 4 * * *"
|
|
63
|
+
At 04:30
|
|
64
|
+
|
|
65
|
+
Next 5 runs:
|
|
66
|
+
- 2026-01-22 04:30:00
|
|
67
|
+
- 2026-01-23 04:30:00
|
|
68
|
+
- 2026-01-24 04:30:00
|
|
69
|
+
- 2026-01-25 04:30:00
|
|
70
|
+
- 2026-01-26 04:30:00
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Options
|
|
74
|
+
|
|
75
|
+
| Option | Alias | Description | Default |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `--next <number>` | `-n` | Number of upcoming run times to show (max 100) | 5 |
|
|
78
|
+
| `--tz <timezone>` | | IANA timezone for output dates (e.g., `America/New_York`) | System local time |
|
|
79
|
+
| `--json` | | Output in JSON format for scripting | false |
|
|
80
|
+
| `--quiet` | `-q` | Print only the human description (no next runs) | false |
|
|
81
|
+
| `--seconds` | | Enable 6-field cron format (includes seconds) | false |
|
|
82
|
+
| `--help` | `-h` | Show help message | |
|
|
83
|
+
| `--version` | `-v` | Show installed version | |
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Common Cron Patterns
|
|
88
|
+
|
|
89
|
+
**Every 5 minutes:**
|
|
90
|
+
```bash
|
|
91
|
+
$ cron-human "*/5 * * * *"
|
|
92
|
+
Every 5 minutes
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Daily at midnight:**
|
|
96
|
+
```bash
|
|
97
|
+
$ cron-human "0 0 * * *"
|
|
98
|
+
At 00:00
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Weekdays only at 9 AM:**
|
|
102
|
+
```bash
|
|
103
|
+
$ cron-human "0 9 * * MON-FRI"
|
|
104
|
+
At 09:00, Monday through Friday
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**First day of every month:**
|
|
108
|
+
```bash
|
|
109
|
+
$ cron-human "0 0 1 * *"
|
|
110
|
+
At 00:00, on day 1 of the month
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Using Macros
|
|
114
|
+
|
|
115
|
+
Cron macros are shortcuts for common schedules:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
$ cron-human "@daily"
|
|
119
|
+
At 00:00
|
|
120
|
+
|
|
121
|
+
$ cron-human "@hourly"
|
|
122
|
+
Every hour
|
|
123
|
+
|
|
124
|
+
$ cron-human "@weekly"
|
|
125
|
+
At 00:00, only on Sunday
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Supported macros:**
|
|
129
|
+
- `@yearly` or `@annually` - Run once a year: `0 0 1 1 *`
|
|
130
|
+
- `@monthly` - Run once a month: `0 0 1 * *`
|
|
131
|
+
- `@weekly` - Run once a week: `0 0 * * 0`
|
|
132
|
+
- `@daily` - Run once a day: `0 0 * * *`
|
|
133
|
+
- `@hourly` - Run once an hour: `0 * * * *`
|
|
134
|
+
|
|
135
|
+
### Timezone Examples
|
|
136
|
+
|
|
137
|
+
**New York time:**
|
|
138
|
+
```bash
|
|
139
|
+
$ cron-human "0 9 * * *" --tz America/New_York
|
|
140
|
+
At 09:00
|
|
141
|
+
|
|
142
|
+
Next 5 runs:
|
|
143
|
+
- 2026-01-22 09:00:00
|
|
144
|
+
- 2026-01-23 09:00:00
|
|
145
|
+
- ...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Tokyo time:**
|
|
149
|
+
```bash
|
|
150
|
+
$ cron-human "30 14 * * *" --tz Asia/Tokyo --next 3
|
|
151
|
+
At 14:30
|
|
152
|
+
|
|
153
|
+
Next 3 runs:
|
|
154
|
+
- 2026-01-22 14:30:00
|
|
155
|
+
- 2026-01-23 14:30:00
|
|
156
|
+
- 2026-01-24 14:30:00
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### JSON Output for Scripting
|
|
160
|
+
|
|
161
|
+
Perfect for parsing in scripts or CI/CD pipelines:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
$ cron-human "*/15 * * * *" --json --next 2
|
|
165
|
+
{
|
|
166
|
+
"expression": "*/15 * * * *",
|
|
167
|
+
"description": "Every 15 minutes",
|
|
168
|
+
"nextRuns": [
|
|
169
|
+
"2026-01-22 12:15:00",
|
|
170
|
+
"2026-01-22 12:30:00"
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Quiet Mode
|
|
176
|
+
|
|
177
|
+
Get just the description without the run times:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
$ cron-human "0 */6 * * *" --quiet
|
|
181
|
+
Every 6 hours
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Seconds-Precision Cron
|
|
185
|
+
|
|
186
|
+
Some systems (like certain schedulers) support 6-field cron with seconds:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
$ cron-human "*/30 * * * * *" --seconds
|
|
190
|
+
Every 30 seconds
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Supported Cron Format
|
|
194
|
+
|
|
195
|
+
### Standard 5-field format:
|
|
196
|
+
```
|
|
197
|
+
* * * * *
|
|
198
|
+
│ │ │ │ │
|
|
199
|
+
│ │ │ │ └─── Day of week (0-6, SUN-SAT)
|
|
200
|
+
│ │ │ └───── Month (1-12)
|
|
201
|
+
│ │ └─────── Day of month (1-31)
|
|
202
|
+
│ └───────── Hour (0-23)
|
|
203
|
+
└─────────── Minute (0-59)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 6-field format (with `--seconds`):
|
|
207
|
+
```
|
|
208
|
+
* * * * * *
|
|
209
|
+
│ │ │ │ │ │
|
|
210
|
+
│ │ │ │ │ └─── Day of week (0-6)
|
|
211
|
+
│ │ │ │ └───── Month (1-12)
|
|
212
|
+
│ │ │ └─────── Day of month (1-31)
|
|
213
|
+
│ │ └───────── Hour (0-23)
|
|
214
|
+
│ └─────────── Minute (0-59)
|
|
215
|
+
└───────────── Second (0-59)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Special characters:
|
|
219
|
+
- `*` - Any value
|
|
220
|
+
- `,` - List (e.g., `1,3,5`)
|
|
221
|
+
- `-` - Range (e.g., `1-5`)
|
|
222
|
+
- `/` - Step (e.g., `*/10` = every 10)
|
|
223
|
+
|
|
224
|
+
## Troubleshooting
|
|
225
|
+
|
|
226
|
+
### Invalid cron expression error
|
|
227
|
+
|
|
228
|
+
Make sure your expression has the correct number of fields:
|
|
229
|
+
- **5 fields** for standard cron (default)
|
|
230
|
+
- **6 fields** with `--seconds` flag
|
|
231
|
+
|
|
232
|
+
### Timezone not recognized
|
|
233
|
+
|
|
234
|
+
Use valid IANA timezone identifiers. Common examples:
|
|
235
|
+
- `UTC`
|
|
236
|
+
- `America/New_York`
|
|
237
|
+
- `Europe/London`
|
|
238
|
+
- `Asia/Tokyo`
|
|
239
|
+
|
|
240
|
+
Find your timezone: [IANA Timezone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
|
|
241
|
+
|
|
242
|
+
### Expression accepted but results look wrong
|
|
243
|
+
|
|
244
|
+
Use `--tz UTC` to see results in UTC and verify the pattern:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
cron-human "0 12 * * *" --tz UTC --next 7
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
Want to contribute or run locally?
|
|
253
|
+
|
|
254
|
+
1. **Clone the repository:**
|
|
255
|
+
```bash
|
|
256
|
+
git clone https://github.com/yourusername/cron-human.git
|
|
257
|
+
cd cron-human
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
2. **Install dependencies:**
|
|
261
|
+
```bash
|
|
262
|
+
npm install
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
3. **Run tests:**
|
|
266
|
+
```bash
|
|
267
|
+
npm test
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
4. **Build:**
|
|
271
|
+
```bash
|
|
272
|
+
npm run build
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
5. **Run locally:**
|
|
276
|
+
```bash
|
|
277
|
+
npm run dev "*/5 * * * *"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT © Akin Ibitoye
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
5
|
+
import { explainCron, getNextRuns, validateCron } from './lib.js';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pkg = require('../package.json');
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name('cron-human')
|
|
11
|
+
.description('Converts cron expressions to human-readable English and prints next run times.')
|
|
12
|
+
.version(pkg.version, '-v, --version')
|
|
13
|
+
.argument('<expression>', 'Cron expression to parse')
|
|
14
|
+
.option('-n, --next <number>', 'how many upcoming run times to print', '5')
|
|
15
|
+
.option('--tz <timezone>', 'timezone for output (default: system timezone)')
|
|
16
|
+
.option('--json', 'output machine-readable JSON', false)
|
|
17
|
+
.option('-q, --quiet', 'only print the one-line human explanation (no next runs)', false)
|
|
18
|
+
.option('--seconds', 'support 6-field cron expressions with seconds', false)
|
|
19
|
+
.action((expression, options) => {
|
|
20
|
+
if (options.tz) {
|
|
21
|
+
const test = DateTime.now().setZone(options.tz);
|
|
22
|
+
if (!test.isValid) {
|
|
23
|
+
console.error(`Error: invalid timezone "${options.tz}" (use IANA like "Europe/London")`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
let nextCount = 5;
|
|
28
|
+
if (options.next) {
|
|
29
|
+
const count = parseInt(options.next, 10);
|
|
30
|
+
if (!Number.isFinite(count) || count < 1 || count > 100) {
|
|
31
|
+
console.error("Error: --next must be a number between 1 and 100");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
nextCount = count;
|
|
35
|
+
}
|
|
36
|
+
const error = validateCron(expression, {
|
|
37
|
+
timezone: options.tz,
|
|
38
|
+
allowSeconds: options.seconds
|
|
39
|
+
});
|
|
40
|
+
if (error) {
|
|
41
|
+
console.error(error);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
let description = '';
|
|
46
|
+
try {
|
|
47
|
+
description = explainCron(expression);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error(`Error: Could not generate description. ${e?.message ?? e}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const output = {
|
|
54
|
+
expression,
|
|
55
|
+
description,
|
|
56
|
+
};
|
|
57
|
+
if (!options.quiet) {
|
|
58
|
+
output.nextRuns = getNextRuns(expression, nextCount, options.tz);
|
|
59
|
+
}
|
|
60
|
+
if (options.json) {
|
|
61
|
+
console.log(JSON.stringify(output, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(description);
|
|
65
|
+
if (output.nextRuns && output.nextRuns.length > 0) {
|
|
66
|
+
console.log(`\nNext ${output.nextRuns.length} runs:`);
|
|
67
|
+
output.nextRuns.forEach((run) => console.log(`- ${run}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error(`Error: ${err.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
program.parse();
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ValidateOptions {
|
|
2
|
+
timezone?: string;
|
|
3
|
+
allowSeconds?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function validateCron(expression: string, options?: ValidateOptions): string | null;
|
|
6
|
+
export declare function explainCron(expression: string): string;
|
|
7
|
+
export declare function getNextRuns(expression: string, count: number, timezone?: string): string[];
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import cronstrue from 'cronstrue';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
3
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
4
|
+
const MACROS = {
|
|
5
|
+
"@yearly": "0 0 0 1 1 *",
|
|
6
|
+
"@annually": "0 0 0 1 1 *",
|
|
7
|
+
"@monthly": "0 0 0 1 * *",
|
|
8
|
+
"@weekly": "0 0 0 * * 0",
|
|
9
|
+
"@daily": "0 0 0 * * *",
|
|
10
|
+
"@hourly": "0 0 * * * *",
|
|
11
|
+
"@minutely": "0 * * * * *",
|
|
12
|
+
"@secondly": "* * * * * *",
|
|
13
|
+
"@weekdays": "0 0 0 * * 1-5",
|
|
14
|
+
"@weekends": "0 0 0 * * 0,6",
|
|
15
|
+
};
|
|
16
|
+
function normalizeForParse(expr) {
|
|
17
|
+
const t = expr.trim();
|
|
18
|
+
if (!t.startsWith("@"))
|
|
19
|
+
return t;
|
|
20
|
+
const key = t.toLowerCase();
|
|
21
|
+
// for parsing, use expanded cron if known, otherwise keep original (will error)
|
|
22
|
+
return MACROS[key] ?? t;
|
|
23
|
+
}
|
|
24
|
+
function fieldCount(expr) {
|
|
25
|
+
return expr.trim().split(/\s+/).length;
|
|
26
|
+
}
|
|
27
|
+
function toJSDate(x) {
|
|
28
|
+
if (x instanceof Date)
|
|
29
|
+
return x;
|
|
30
|
+
if (x && typeof x.toDate === 'function')
|
|
31
|
+
return x.toDate();
|
|
32
|
+
if (x && typeof x.toJSDate === 'function')
|
|
33
|
+
return x.toJSDate();
|
|
34
|
+
if (x && typeof x.valueOf === 'function')
|
|
35
|
+
return new Date(x.valueOf());
|
|
36
|
+
const d = new Date(String(x));
|
|
37
|
+
if (isNaN(d.getTime())) {
|
|
38
|
+
throw new Error('cron-parser returned an invalid date iterator result.');
|
|
39
|
+
}
|
|
40
|
+
return d;
|
|
41
|
+
}
|
|
42
|
+
export function validateCron(expression, options = {}) {
|
|
43
|
+
if (expression.length > 1000) {
|
|
44
|
+
return 'Error: Cron expression too long (max 1000 chars).';
|
|
45
|
+
}
|
|
46
|
+
if (expression.match(/[\x00-\x08\x0E-\x1F\x7F]/)) {
|
|
47
|
+
return 'Error: Invalid control characters in expression.';
|
|
48
|
+
}
|
|
49
|
+
if (expression.match(/[\n\r]/)) {
|
|
50
|
+
return 'Error: Invalid cron expression (newlines not allowed).';
|
|
51
|
+
}
|
|
52
|
+
const normalized = normalizeForParse(expression);
|
|
53
|
+
if (expression.trim().startsWith("@")) {
|
|
54
|
+
try {
|
|
55
|
+
const parseOpts = {};
|
|
56
|
+
if (options.timezone)
|
|
57
|
+
parseOpts.tz = options.timezone;
|
|
58
|
+
CronExpressionParser.parse(normalized, parseOpts);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return `Invalid cron expression: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const fields = fieldCount(normalized);
|
|
66
|
+
if (!options.allowSeconds && fields === 6) {
|
|
67
|
+
return 'Error: 6-field cron detected. Use --seconds for seconds support.';
|
|
68
|
+
}
|
|
69
|
+
if (fields < 5 || fields > 6) {
|
|
70
|
+
return 'Error: cron must have 5 fields (or 6 with --seconds).';
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parseOpts = {};
|
|
74
|
+
if (options.timezone)
|
|
75
|
+
parseOpts.tz = options.timezone;
|
|
76
|
+
CronExpressionParser.parse(normalized, parseOpts);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return `Invalid cron expression: ${err.message}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function explainCron(expression) {
|
|
84
|
+
const normalized = normalizeForParse(expression);
|
|
85
|
+
return cronstrue.toString(normalized, {
|
|
86
|
+
use24HourTimeFormat: true,
|
|
87
|
+
throwExceptionOnParseError: true,
|
|
88
|
+
verbose: false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function getNextRuns(expression, count, timezone) {
|
|
92
|
+
const options = {};
|
|
93
|
+
if (!Number.isFinite(count) || count < 0 || count > 1000) {
|
|
94
|
+
throw new Error("Invalid count: must be between 0 and 1000.");
|
|
95
|
+
}
|
|
96
|
+
if (timezone) {
|
|
97
|
+
const test = DateTime.now().setZone(timezone);
|
|
98
|
+
if (!test.isValid)
|
|
99
|
+
throw new Error(`Invalid timezone "${timezone}". Use IANA like "Europe/London".`);
|
|
100
|
+
options.tz = timezone;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const interval = CronExpressionParser.parse(normalizeForParse(expression), options);
|
|
104
|
+
const dates = [];
|
|
105
|
+
for (let i = 0; i < count; i++) {
|
|
106
|
+
const obj = interval.next();
|
|
107
|
+
const jsDate = toJSDate(obj);
|
|
108
|
+
const dt = timezone
|
|
109
|
+
? DateTime.fromJSDate(jsDate).setZone(timezone)
|
|
110
|
+
: DateTime.fromJSDate(jsDate);
|
|
111
|
+
dates.push(dt.toFormat('yyyy-MM-dd HH:mm:ss'));
|
|
112
|
+
}
|
|
113
|
+
return dates;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new Error(`Failed to calculate next runs: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cron-human",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A CLI that converts cron expressions to human-readable English and prints next run times",
|
|
5
|
+
"main": "dist/lib.js",
|
|
6
|
+
"types": "dist/lib.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cron-human": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.build.json",
|
|
13
|
+
"start": "node dist/cli.js",
|
|
14
|
+
"dev": "tsx src/cli.ts",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "eslint src tests",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cron",
|
|
22
|
+
"cli",
|
|
23
|
+
"human-readable",
|
|
24
|
+
"schedule"
|
|
25
|
+
],
|
|
26
|
+
"author": "Akin Ibitoye",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^14.0.2",
|
|
35
|
+
"cron-parser": "^5.5.0",
|
|
36
|
+
"cronstrue": "^3.9.0",
|
|
37
|
+
"luxon": "^3.7.2"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/luxon": "^3.7.1",
|
|
41
|
+
"@types/node": "^25.0.10",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
43
|
+
"@typescript-eslint/parser": "^8.53.1",
|
|
44
|
+
"eslint": "^9.39.2",
|
|
45
|
+
"eslint-config-prettier": "^10.1.8",
|
|
46
|
+
"prettier": "^3.8.1",
|
|
47
|
+
"tsx": "^4.21.0",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vitest": "^4.0.17"
|
|
50
|
+
}
|
|
51
|
+
}
|