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 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
+ [![npm version](https://img.shields.io/npm/v/cron-human.svg)](https://www.npmjs.com/package/cron-human)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }