cron-human 1.0.2 → 1.1.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 +1 -1
- package/README.md +54 -1
- package/dist/cli.js +26 -8
- package/dist/lib.js +12 -19
- package/dist/ui/app.d.ts +2 -0
- package/dist/ui/app.js +105 -0
- package/dist/ui/components/HelpScreen.d.ts +2 -0
- package/dist/ui/components/HelpScreen.js +5 -0
- package/dist/ui/components/HistoryPanel.d.ts +12 -0
- package/dist/ui/components/HistoryPanel.js +8 -0
- package/dist/ui/components/InputSection.d.ts +9 -0
- package/dist/ui/components/InputSection.js +6 -0
- package/dist/ui/components/OptionsPanel.d.ts +10 -0
- package/dist/ui/components/OptionsPanel.js +5 -0
- package/dist/ui/components/PreviewSection.d.ts +8 -0
- package/dist/ui/components/PreviewSection.js +24 -0
- package/dist/ui/launcher.d.ts +1 -0
- package/dist/ui/launcher.js +7 -0
- package/dist/utils/error.d.ts +5 -0
- package/dist/utils/error.js +17 -0
- package/package.json +18 -4
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -104,4 +104,57 @@ cron-human "*/15 * * * *" --json --quiet
|
|
|
104
104
|
|
|
105
105
|
## License
|
|
106
106
|
|
|
107
|
-
MIT
|
|
107
|
+
MIT © Akin Ibitoye
|
|
108
|
+
|
|
109
|
+
## Interactive Mode (TUI)
|
|
110
|
+
|
|
111
|
+
Launch the interactive Terminal UI with:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
cron-human tui
|
|
115
|
+
# or
|
|
116
|
+
cron-human --interactive
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+
### Key Features
|
|
122
|
+
- **Live Preview**: Real-time validation and English translation as you type.
|
|
123
|
+
- **History**: Auto-saves successful cron expressions. Navigate history with `Up/Down` and reload with `Enter`.
|
|
124
|
+
- **Clipboard Integration**: Paste (`Ctrl+V`) directly into the editor. Copy (`c`) saved expressions from history.
|
|
125
|
+
- **Toggle Options**: Quickly enable/disable seconds support (`Space`).
|
|
126
|
+
|
|
127
|
+
### Keybindings
|
|
128
|
+
|
|
129
|
+
| Key | Action |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `Tab` | Cycle focus (Input → Options → History) |
|
|
132
|
+
| `Ctrl+V` | Paste from clipboard |
|
|
133
|
+
| `Ctrl+R` | Reset/Clear input |
|
|
134
|
+
| `Ctrl+C` / `Q` | Quit |
|
|
135
|
+
|
|
136
|
+
**History Panel:**
|
|
137
|
+
| Key | Action |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `Up` / `Down` | Navigate history |
|
|
140
|
+
| `Enter` | Load selected expression |
|
|
141
|
+
| `c` | Copy selected expression |
|
|
142
|
+
|
|
143
|
+
**Options Panel:**
|
|
144
|
+
| Key | Action |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `Space` | Toggle checkbox |
|
|
147
|
+
|
|
148
|
+
## Manual Test Cases
|
|
149
|
+
|
|
150
|
+
1. **Basic Minute**: `* * * * *` -> "Every minute"
|
|
151
|
+
2. **Specific Time**: `30 14 * * *` -> "At 14:30"
|
|
152
|
+
3. **Interval**: `*/5 * * * *` -> "Every 5 minutes"
|
|
153
|
+
4. **Range**: `0 9-17 * * 1-5` -> "At 0 minutes past the hour, between 09:00 and 17:59, Monday through Friday"
|
|
154
|
+
5. **Macro**: `@daily` -> "At 00:00"
|
|
155
|
+
6. **With Seconds**: `*/30 * * * * *` (Enable "Allow Seconds") -> "Every 30 seconds"
|
|
156
|
+
7. **Invalid**: `* * * 99 *` -> "Error: Invalid cron expression..."
|
|
157
|
+
8. **Timezone**: Set TZ to `UTC` -> Verify next runs match UTC.
|
|
158
|
+
9. **History**: Enter a valid cron -> Press Up -> should see it.
|
|
159
|
+
10. **Help**: Press `Tab` until Focus is History, then `Tab` again -> Focus Input.
|
|
160
|
+
11. **Clipboard**: Press `Ctrl+V` to paste a cron string. Select history item and press `c` to copy.
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
3
|
import { DateTime } from 'luxon';
|
|
5
4
|
import { explainCron, getNextRuns, validateCron } from './lib.js';
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { handleErr } from "./utils/error.js";
|
|
6
|
+
import pkg from "../package.json" with { type: "json" };
|
|
8
7
|
const program = new Command();
|
|
9
8
|
program
|
|
10
9
|
.name('cron-human')
|
|
11
10
|
.description('Converts cron expressions to human-readable English and prints next run times.')
|
|
12
11
|
.version(pkg.version, '-v, --version')
|
|
13
|
-
.argument('
|
|
12
|
+
.argument('[expression]', 'Cron expression to parse')
|
|
14
13
|
.option('-n, --next <number>', 'how many upcoming run times to print', '5')
|
|
15
14
|
.option('--tz <timezone>', 'timezone for output (default: system timezone)')
|
|
16
15
|
.option('--json', 'output machine-readable JSON', false)
|
|
17
16
|
.option('-q, --quiet', 'only print the one-line human explanation (no next runs)', false)
|
|
18
17
|
.option('--seconds', 'support 6-field cron expressions with seconds', false)
|
|
19
|
-
.
|
|
18
|
+
.option('-i, --interactive', 'launch interactive TUI mode')
|
|
19
|
+
.action(async (expression, options) => {
|
|
20
|
+
if (options.interactive) {
|
|
21
|
+
const { startTui } = await import('./ui/launcher.js');
|
|
22
|
+
await startTui();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!expression) {
|
|
26
|
+
program.help();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
20
29
|
if (options.tz) {
|
|
21
30
|
const test = DateTime.now().setZone(options.tz);
|
|
22
31
|
if (!test.isValid) {
|
|
@@ -47,7 +56,8 @@ program
|
|
|
47
56
|
description = explainCron(expression);
|
|
48
57
|
}
|
|
49
58
|
catch (e) {
|
|
50
|
-
|
|
59
|
+
const err = handleErr(e);
|
|
60
|
+
console.error(`Error: Could not generate description. ${err.message}`);
|
|
51
61
|
process.exit(1);
|
|
52
62
|
}
|
|
53
63
|
const output = {
|
|
@@ -68,9 +78,17 @@ program
|
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
catch (
|
|
72
|
-
|
|
81
|
+
catch (e) {
|
|
82
|
+
const err = handleErr(e);
|
|
83
|
+
console.error(err);
|
|
73
84
|
process.exit(1);
|
|
74
85
|
}
|
|
75
86
|
});
|
|
87
|
+
// Add explicit command as well
|
|
88
|
+
program.command('tui')
|
|
89
|
+
.description('Launch the interactive TUI')
|
|
90
|
+
.action(async () => {
|
|
91
|
+
const { startTui } = await import('./ui/launcher.js');
|
|
92
|
+
await startTui();
|
|
93
|
+
});
|
|
76
94
|
program.parse();
|
package/dist/lib.js
CHANGED
|
@@ -2,23 +2,22 @@ import cronstrue from 'cronstrue';
|
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
3
|
import { CronExpressionParser } from 'cron-parser';
|
|
4
4
|
const MACROS = {
|
|
5
|
-
"@yearly": "0 0
|
|
6
|
-
"@annually": "0 0
|
|
7
|
-
"@monthly": "0 0
|
|
8
|
-
"@weekly": "0 0
|
|
9
|
-
"@daily": "0 0
|
|
10
|
-
"@hourly": "0
|
|
11
|
-
"@minutely": "
|
|
5
|
+
"@yearly": "0 0 1 1 *",
|
|
6
|
+
"@annually": "0 0 1 1 *",
|
|
7
|
+
"@monthly": "0 0 1 * *",
|
|
8
|
+
"@weekly": "0 0 * * 0",
|
|
9
|
+
"@daily": "0 0 * * *",
|
|
10
|
+
"@hourly": "0 * * * *",
|
|
11
|
+
"@minutely": "* * * * *",
|
|
12
12
|
"@secondly": "* * * * * *",
|
|
13
|
-
"@weekdays": "0 0
|
|
14
|
-
"@weekends": "0 0
|
|
13
|
+
"@weekdays": "0 0 * * 1-5",
|
|
14
|
+
"@weekends": "0 0 * * 0,6",
|
|
15
15
|
};
|
|
16
16
|
function normalizeForParse(expr) {
|
|
17
17
|
const t = expr.trim();
|
|
18
18
|
if (!t.startsWith("@"))
|
|
19
19
|
return t;
|
|
20
20
|
const key = t.toLowerCase();
|
|
21
|
-
// for parsing, use expanded cron if known, otherwise keep original (will error)
|
|
22
21
|
return MACROS[key] ?? t;
|
|
23
22
|
}
|
|
24
23
|
function fieldCount(expr) {
|
|
@@ -51,15 +50,9 @@ export function validateCron(expression, options = {}) {
|
|
|
51
50
|
}
|
|
52
51
|
const normalized = normalizeForParse(expression);
|
|
53
52
|
if (expression.trim().startsWith("@")) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
parseOpts.tz = options.timezone;
|
|
58
|
-
CronExpressionParser.parse(normalized, parseOpts);
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
return `Invalid cron expression: ${err.message}`;
|
|
53
|
+
const macro = expression.trim().toLowerCase();
|
|
54
|
+
if (macro === '@secondly' && !options.allowSeconds) {
|
|
55
|
+
return 'Error: @secondly requires --seconds flag.';
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
const fields = fieldCount(normalized);
|
package/dist/ui/app.d.ts
ADDED
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
|
+
import { InputSection } from './components/InputSection.js';
|
|
5
|
+
import { PreviewSection } from './components/PreviewSection.js';
|
|
6
|
+
import { OptionsPanel } from './components/OptionsPanel.js';
|
|
7
|
+
import { HistoryPanel } from './components/HistoryPanel.js';
|
|
8
|
+
import clipboardy from 'clipboardy';
|
|
9
|
+
var FocusArea;
|
|
10
|
+
(function (FocusArea) {
|
|
11
|
+
FocusArea[FocusArea["Input"] = 0] = "Input";
|
|
12
|
+
FocusArea[FocusArea["Options"] = 1] = "Options";
|
|
13
|
+
FocusArea[FocusArea["History"] = 2] = "History";
|
|
14
|
+
})(FocusArea || (FocusArea = {}));
|
|
15
|
+
export const App = () => {
|
|
16
|
+
const { exit } = useApp();
|
|
17
|
+
const [expression, setExpression] = useState('');
|
|
18
|
+
const [timezone, setTimezone] = useState(undefined);
|
|
19
|
+
const [allowSeconds, setAllowSeconds] = useState(false);
|
|
20
|
+
const [focus, setFocus] = useState(FocusArea.Input);
|
|
21
|
+
const [history, setHistory] = useState([]);
|
|
22
|
+
const [historyIndex, setHistoryIndex] = useState(0);
|
|
23
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
24
|
+
const [notification, setNotification] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (notification) {
|
|
27
|
+
const timer = setTimeout(() => setNotification(null), 2000);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}, [notification]);
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
if (key.ctrl && input === 'c') {
|
|
33
|
+
exit();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (input === 'q' && !key.ctrl && focus !== FocusArea.Input) {
|
|
37
|
+
exit();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (key.tab) {
|
|
41
|
+
setFocus((prev) => {
|
|
42
|
+
if (prev === FocusArea.Input)
|
|
43
|
+
return FocusArea.Options;
|
|
44
|
+
if (prev === FocusArea.Options)
|
|
45
|
+
return FocusArea.History;
|
|
46
|
+
return FocusArea.Input;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (key.ctrl && input === 'r') {
|
|
50
|
+
setExpression('');
|
|
51
|
+
setFocus(FocusArea.Input);
|
|
52
|
+
}
|
|
53
|
+
if (input === '?') {
|
|
54
|
+
}
|
|
55
|
+
if (key.ctrl && input === 'v') {
|
|
56
|
+
if (focus === FocusArea.Input) {
|
|
57
|
+
clipboardy.read().then(text => {
|
|
58
|
+
if (text) {
|
|
59
|
+
setExpression(text.trim());
|
|
60
|
+
setNotification('Pasted from clipboard!');
|
|
61
|
+
}
|
|
62
|
+
}).catch(err => {
|
|
63
|
+
setNotification(`Paste failed: ${err.message}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (focus === FocusArea.Options) {
|
|
68
|
+
if (input === ' ') {
|
|
69
|
+
setAllowSeconds(prev => !prev);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (focus === FocusArea.History) {
|
|
73
|
+
if (key.upArrow) {
|
|
74
|
+
setHistoryIndex(prev => Math.max(0, prev - 1));
|
|
75
|
+
}
|
|
76
|
+
if (key.downArrow) {
|
|
77
|
+
setHistoryIndex(prev => Math.min(history.length - 1, prev + 1));
|
|
78
|
+
}
|
|
79
|
+
if (key.return) {
|
|
80
|
+
if (history[historyIndex]) {
|
|
81
|
+
setExpression(history[historyIndex].expression);
|
|
82
|
+
setFocus(FocusArea.Input);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (input === 'c') {
|
|
86
|
+
if (history[historyIndex]) {
|
|
87
|
+
clipboardy.write(history[historyIndex].expression).then(() => {
|
|
88
|
+
setNotification('Copied to clipboard!');
|
|
89
|
+
}).catch((err) => {
|
|
90
|
+
setNotification(`Copy failed: ${err.message}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const handleInputSubmit = (val) => {
|
|
97
|
+
if (val.trim()) {
|
|
98
|
+
const last = history[0];
|
|
99
|
+
if (!last || last.expression !== val) {
|
|
100
|
+
setHistory(prev => [{ expression: val, timestamp: new Date() }, ...prev].slice(0, 50));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", height: "100%", children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "magenta", children: "Cron Human TUI" }), _jsx(Text, { children: " | " }), _jsx(Text, { dimColor: true, children: "Ctrl+C to Quit" })] }), notification && _jsx(Text, { color: "yellow", bold: true, children: notification })] }), _jsx(InputSection, { value: expression, onChange: setExpression, onSubmit: handleInputSubmit, isFocused: focus === FocusArea.Input }), _jsx(Box, { marginY: 1, children: _jsx(OptionsPanel, { isFocused: focus === FocusArea.Options, timezone: timezone || "Local", allowSeconds: allowSeconds, onToggleSeconds: () => setAllowSeconds(!allowSeconds), onChangeTimezone: setTimezone }) }), _jsx(PreviewSection, { expression: expression, timezone: timezone, allowSeconds: allowSeconds }), _jsxs(Box, { marginTop: 1, children: [_jsx(HistoryPanel, { isFocused: focus === FocusArea.History, items: history.slice(0, 5), selectedIndex: historyIndex }), history.length > 5 && _jsxs(Text, { dimColor: true, children: ["... ", history.length - 5, " more"] })] })] }));
|
|
105
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export const HelpScreen = () => {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "white", padding: 1, children: [_jsx(Text, { bold: true, underline: true, children: "Help & Keybindings" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, children: "Tab" }), _jsxs(Text, { children: [": Cycle focus (Input -", '>', " Options -", '>', " History)"] })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Enter" }), _jsx(Text, { children: ": Run conversion / Save to history" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Up/Down" }), _jsx(Text, { children: ": Navigate history" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "c" }), _jsx(Text, { children: ": Copy selected history item" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Ctrl+V" }), _jsx(Text, { children: ": Paste from clipboard" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Ctrl+R" }), _jsx(Text, { children: ": Reset input" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Ctrl+C / Q" }), _jsx(Text, { children: ": Quit" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: "Examples:" }) }), _jsx(Box, { children: _jsx(Text, { children: "* * * * * (Every minute)" }) }), _jsx(Box, { children: _jsx(Text, { children: "0 12 * * 1-5 (At 12:00 on weekdays)" }) }), _jsx(Box, { children: _jsx(Text, { children: "@daily (Run once a day at midnight)" }) })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface HistoryItem {
|
|
3
|
+
expression: string;
|
|
4
|
+
timestamp: Date;
|
|
5
|
+
}
|
|
6
|
+
interface HistoryPanelProps {
|
|
7
|
+
isFocused: boolean;
|
|
8
|
+
items: HistoryItem[];
|
|
9
|
+
selectedIndex: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const HistoryPanel: React.FC<HistoryPanelProps>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export const HistoryPanel = ({ isFocused, items, selectedIndex }) => {
|
|
4
|
+
if (items.length === 0) {
|
|
5
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "History:" }), _jsx(Text, { dimColor: true, children: "No history yet. Press Enter to save successful runs." })] }));
|
|
6
|
+
}
|
|
7
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "History (Up/Down to navigate, Enter to load, 'c' to copy):" }), items.map((item, index) => (_jsx(Box, { children: _jsxs(Text, { color: index === selectedIndex ? "cyan" : "white", children: [index === selectedIndex ? "> " : " ", item.expression] }) }, index)))] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
export const InputSection = ({ value, onChange, onSubmit, isFocused }) => {
|
|
5
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: isFocused ? "green" : "white", children: "Cron Expression:" }), _jsx(Box, { children: _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, focus: isFocused, placeholder: "* * * * *" }) })] }));
|
|
6
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface OptionsPanelProps {
|
|
3
|
+
isFocused: boolean;
|
|
4
|
+
timezone: string;
|
|
5
|
+
allowSeconds: boolean;
|
|
6
|
+
onToggleSeconds: () => void;
|
|
7
|
+
onChangeTimezone: (tz: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const OptionsPanel: React.FC<OptionsPanelProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export const OptionsPanel = ({ isFocused, timezone, allowSeconds, onToggleSeconds, onChangeTimezone }) => {
|
|
4
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: isFocused ? "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Options (Press Tab to focus, then Space to toggle):" }), _jsxs(Box, { children: [_jsxs(Text, { color: isFocused ? "cyan" : "white", children: ["[ ", allowSeconds ? 'X' : ' ', " ] Allow Seconds"] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["Timezone: ", timezone || 'Local', " (Ctrl+T to set)"] }) })] })] }));
|
|
5
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { validateCron, explainCron, getNextRuns } from '../../lib.js';
|
|
4
|
+
export const PreviewSection = ({ expression, timezone, allowSeconds }) => {
|
|
5
|
+
const validationError = validateCron(expression, { timezone, allowSeconds });
|
|
6
|
+
let content;
|
|
7
|
+
let nextRuns = [];
|
|
8
|
+
let isError = false;
|
|
9
|
+
if (validationError) {
|
|
10
|
+
content = validationError;
|
|
11
|
+
isError = true;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
try {
|
|
15
|
+
content = explainCron(expression);
|
|
16
|
+
nextRuns = getNextRuns(expression, 3, timezone);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
content = e.message;
|
|
20
|
+
isError = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: isError ? "red" : "blue", paddingX: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, color: "white", children: "Human Readable:" }), _jsx(Text, { color: isError ? "red" : "green", children: content }), !isError && nextRuns.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Next runs:" }), nextRuns.map((run, i) => (_jsxs(Text, { dimColor: true, children: [" - ", run] }, i)))] }))] }));
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startTui(): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CliError extends Error {
|
|
2
|
+
constructor(msg) {
|
|
3
|
+
super(msg);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
function handleErr(err) {
|
|
7
|
+
if (err instanceof Error) {
|
|
8
|
+
return new CliError(err.message);
|
|
9
|
+
}
|
|
10
|
+
else if (typeof err === "string") {
|
|
11
|
+
return new CliError(err);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return new CliError("unknown error");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export { CliError, handleErr };
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cron-human",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A CLI that converts cron expressions to human-readable English and prints next run times",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"types": "dist/lib.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"bin": {
|
|
8
9
|
"cron-human": "dist/cli.js"
|
|
9
10
|
},
|
|
10
|
-
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc -p tsconfig.build.json",
|
|
13
|
-
"start": "node dist/cli.js",
|
|
13
|
+
"start": "node dist/cli.js '*/5 * * * *'",
|
|
14
14
|
"dev": "tsx src/cli.ts",
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"test:watch": "vitest",
|
|
@@ -38,21 +38,35 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"clipboardy": "^5.1.0",
|
|
41
42
|
"commander": "^14.0.2",
|
|
42
43
|
"cron-parser": "^5.5.0",
|
|
43
44
|
"cronstrue": "^3.9.0",
|
|
44
|
-
"
|
|
45
|
+
"ink": "^6.6.0",
|
|
46
|
+
"ink-select-input": "^6.2.0",
|
|
47
|
+
"ink-text-input": "^6.0.0",
|
|
48
|
+
"luxon": "^3.7.2",
|
|
49
|
+
"react": "^19.2.4"
|
|
45
50
|
},
|
|
46
51
|
"devDependencies": {
|
|
52
|
+
"@testing-library/react": "^16.3.2",
|
|
53
|
+
"@types/ink-select-input": "^3.0.5",
|
|
54
|
+
"@types/ink-text-input": "^2.0.5",
|
|
47
55
|
"@types/luxon": "^3.7.1",
|
|
48
56
|
"@types/node": "^25.0.10",
|
|
57
|
+
"@types/react": "^19.2.10",
|
|
49
58
|
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
|
50
59
|
"@typescript-eslint/parser": "^8.53.1",
|
|
60
|
+
"chalk": "^5.6.2",
|
|
51
61
|
"eslint": "^9.39.2",
|
|
52
62
|
"eslint-config-prettier": "^10.1.8",
|
|
63
|
+
"ink-testing-library": "^4.0.0",
|
|
53
64
|
"prettier": "^3.8.1",
|
|
54
65
|
"tsx": "^4.21.0",
|
|
55
66
|
"typescript": "^5.9.3",
|
|
56
67
|
"vitest": "^4.0.17"
|
|
68
|
+
},
|
|
69
|
+
"overrides": {
|
|
70
|
+
"es-toolkit": "1.42.0"
|
|
57
71
|
}
|
|
58
72
|
}
|