jm2 0.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/GNU-AGPL-3.0 +665 -0
- package/README.md +603 -0
- package/bin/jm2.js +24 -0
- package/package.json +70 -0
- package/src/cli/commands/add.js +206 -0
- package/src/cli/commands/config.js +212 -0
- package/src/cli/commands/edit.js +198 -0
- package/src/cli/commands/export.js +61 -0
- package/src/cli/commands/flush.js +132 -0
- package/src/cli/commands/history.js +179 -0
- package/src/cli/commands/import.js +180 -0
- package/src/cli/commands/list.js +174 -0
- package/src/cli/commands/logs.js +415 -0
- package/src/cli/commands/pause.js +97 -0
- package/src/cli/commands/remove.js +107 -0
- package/src/cli/commands/restart.js +68 -0
- package/src/cli/commands/resume.js +96 -0
- package/src/cli/commands/run.js +115 -0
- package/src/cli/commands/show.js +159 -0
- package/src/cli/commands/start.js +46 -0
- package/src/cli/commands/status.js +47 -0
- package/src/cli/commands/stop.js +48 -0
- package/src/cli/index.js +274 -0
- package/src/cli/utils/output.js +267 -0
- package/src/cli/utils/prompts.js +56 -0
- package/src/core/config.js +227 -0
- package/src/core/history-db.js +439 -0
- package/src/core/job.js +329 -0
- package/src/core/logger.js +382 -0
- package/src/core/storage.js +315 -0
- package/src/daemon/executor.js +409 -0
- package/src/daemon/index.js +873 -0
- package/src/daemon/scheduler.js +465 -0
- package/src/ipc/client.js +112 -0
- package/src/ipc/protocol.js +183 -0
- package/src/ipc/server.js +92 -0
- package/src/utils/cron.js +205 -0
- package/src/utils/datetime.js +237 -0
- package/src/utils/duration.js +226 -0
- package/src/utils/paths.js +164 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration parsing utilities for JM2
|
|
3
|
+
* Parses human-readable duration strings like "30s", "5m", "2h", "1d", "1w", "1h30m"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Duration unit multipliers in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
const UNIT_MS = {
|
|
10
|
+
s: 1000, // seconds
|
|
11
|
+
m: 60 * 1000, // minutes
|
|
12
|
+
h: 60 * 60 * 1000, // hours
|
|
13
|
+
d: 24 * 60 * 60 * 1000, // days
|
|
14
|
+
w: 7 * 24 * 60 * 60 * 1000, // weeks
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Valid duration units
|
|
19
|
+
*/
|
|
20
|
+
const VALID_UNITS = Object.keys(UNIT_MS);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Regular expression to match duration components
|
|
24
|
+
* Matches patterns like "30s", "5m", "2h", "1d", "1w"
|
|
25
|
+
*/
|
|
26
|
+
const DURATION_COMPONENT_REGEX = /(\d+)([smhdw])/gi;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Regular expression to validate the entire duration string
|
|
30
|
+
* Ensures the string only contains valid duration components
|
|
31
|
+
*/
|
|
32
|
+
const DURATION_FULL_REGEX = /^(\d+[smhdw])+$/i;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a duration string into milliseconds
|
|
36
|
+
*
|
|
37
|
+
* @param {string} durationStr - Duration string (e.g., "30s", "5m", "2h", "1d", "1w", "1h30m")
|
|
38
|
+
* @returns {number} Duration in milliseconds
|
|
39
|
+
* @throws {Error} If the duration string is invalid
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* parseDuration("30s") // 30000
|
|
43
|
+
* parseDuration("5m") // 300000
|
|
44
|
+
* parseDuration("2h") // 7200000
|
|
45
|
+
* parseDuration("1d") // 86400000
|
|
46
|
+
* parseDuration("1w") // 604800000
|
|
47
|
+
* parseDuration("1h30m") // 5400000
|
|
48
|
+
*/
|
|
49
|
+
export function parseDuration(durationStr) {
|
|
50
|
+
if (typeof durationStr !== 'string') {
|
|
51
|
+
throw new Error('Duration must be a string');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const trimmed = durationStr.trim();
|
|
55
|
+
|
|
56
|
+
if (trimmed === '') {
|
|
57
|
+
throw new Error('Duration cannot be empty');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate the overall format
|
|
61
|
+
if (!DURATION_FULL_REGEX.test(trimmed)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid duration format: "${durationStr}". ` +
|
|
64
|
+
`Expected format like "30s", "5m", "2h", "1d", "1w", or combined like "1h30m"`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let totalMs = 0;
|
|
69
|
+
let match;
|
|
70
|
+
|
|
71
|
+
// Reset regex lastIndex for global matching
|
|
72
|
+
DURATION_COMPONENT_REGEX.lastIndex = 0;
|
|
73
|
+
|
|
74
|
+
while ((match = DURATION_COMPONENT_REGEX.exec(trimmed)) !== null) {
|
|
75
|
+
const value = parseInt(match[1], 10);
|
|
76
|
+
const unit = match[2].toLowerCase();
|
|
77
|
+
|
|
78
|
+
if (value < 0) {
|
|
79
|
+
throw new Error('Duration values cannot be negative');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value === 0 && trimmed === `0${unit}`) {
|
|
83
|
+
throw new Error('Duration cannot be zero');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
totalMs += value * UNIT_MS[unit];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (totalMs === 0) {
|
|
90
|
+
throw new Error('Duration cannot be zero');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return totalMs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse a duration string into seconds
|
|
98
|
+
*
|
|
99
|
+
* @param {string} durationStr - Duration string
|
|
100
|
+
* @returns {number} Duration in seconds
|
|
101
|
+
* @throws {Error} If the duration string is invalid
|
|
102
|
+
*/
|
|
103
|
+
export function parseDurationSeconds(durationStr) {
|
|
104
|
+
return Math.floor(parseDuration(durationStr) / 1000);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format milliseconds into a human-readable duration string
|
|
109
|
+
*
|
|
110
|
+
* @param {number} ms - Duration in milliseconds
|
|
111
|
+
* @param {object} options - Formatting options
|
|
112
|
+
* @param {boolean} options.short - Use short format (default: true)
|
|
113
|
+
* @param {boolean} options.largest - Only show the largest unit (default: false)
|
|
114
|
+
* @returns {string} Formatted duration string
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* formatDuration(5400000) // "1h30m"
|
|
118
|
+
* formatDuration(5400000, { largest: true }) // "1h"
|
|
119
|
+
* formatDuration(90000) // "1m30s"
|
|
120
|
+
*/
|
|
121
|
+
export function formatDuration(ms, options = {}) {
|
|
122
|
+
const { short = true, largest = false } = options;
|
|
123
|
+
|
|
124
|
+
if (typeof ms !== 'number' || isNaN(ms)) {
|
|
125
|
+
throw new Error('Duration must be a number');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ms < 0) {
|
|
129
|
+
throw new Error('Duration cannot be negative');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (ms === 0) {
|
|
133
|
+
return short ? '0s' : '0 seconds';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const units = [
|
|
137
|
+
{ unit: 'w', ms: UNIT_MS.w, long: 'week' },
|
|
138
|
+
{ unit: 'd', ms: UNIT_MS.d, long: 'day' },
|
|
139
|
+
{ unit: 'h', ms: UNIT_MS.h, long: 'hour' },
|
|
140
|
+
{ unit: 'm', ms: UNIT_MS.m, long: 'minute' },
|
|
141
|
+
{ unit: 's', ms: UNIT_MS.s, long: 'second' },
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const parts = [];
|
|
145
|
+
let remaining = ms;
|
|
146
|
+
|
|
147
|
+
for (const { unit, ms: unitMs, long } of units) {
|
|
148
|
+
if (remaining >= unitMs) {
|
|
149
|
+
const value = Math.floor(remaining / unitMs);
|
|
150
|
+
remaining = remaining % unitMs;
|
|
151
|
+
|
|
152
|
+
if (short) {
|
|
153
|
+
parts.push(`${value}${unit}`);
|
|
154
|
+
} else {
|
|
155
|
+
parts.push(`${value} ${long}${value !== 1 ? 's' : ''}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (largest) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (parts.length === 0) {
|
|
165
|
+
// Less than 1 second
|
|
166
|
+
return short ? `${ms}ms` : `${ms} milliseconds`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return short ? parts.join('') : parts.join(' ');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a string is a valid duration format
|
|
174
|
+
*
|
|
175
|
+
* @param {string} durationStr - String to validate
|
|
176
|
+
* @returns {boolean} True if valid duration format
|
|
177
|
+
*/
|
|
178
|
+
export function isValidDuration(durationStr) {
|
|
179
|
+
if (typeof durationStr !== 'string') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const trimmed = durationStr.trim();
|
|
184
|
+
|
|
185
|
+
if (trimmed === '') {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!DURATION_FULL_REGEX.test(trimmed)) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check that it doesn't result in zero
|
|
194
|
+
try {
|
|
195
|
+
const ms = parseDuration(trimmed);
|
|
196
|
+
return ms > 0;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get the valid duration units
|
|
204
|
+
* @returns {string[]} Array of valid unit characters
|
|
205
|
+
*/
|
|
206
|
+
export function getValidUnits() {
|
|
207
|
+
return [...VALID_UNITS];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the milliseconds for a specific unit
|
|
212
|
+
* @param {string} unit - Unit character (s, m, h, d, w)
|
|
213
|
+
* @returns {number|undefined} Milliseconds for the unit, or undefined if invalid
|
|
214
|
+
*/
|
|
215
|
+
export function getUnitMs(unit) {
|
|
216
|
+
return UNIT_MS[unit.toLowerCase()];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default {
|
|
220
|
+
parseDuration,
|
|
221
|
+
parseDurationSeconds,
|
|
222
|
+
formatDuration,
|
|
223
|
+
isValidDuration,
|
|
224
|
+
getValidUnits,
|
|
225
|
+
getUnitMs,
|
|
226
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path utilities for JM2
|
|
3
|
+
* Provides consistent paths for data directory, config files, logs, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default data directory name
|
|
12
|
+
*/
|
|
13
|
+
const DATA_DIR_NAME = '.jm2';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the base data directory path (~/.jm2/)
|
|
17
|
+
* Can be overridden via JM2_DATA_DIR environment variable for testing
|
|
18
|
+
* @returns {string} The data directory path
|
|
19
|
+
*/
|
|
20
|
+
export function getDataDir() {
|
|
21
|
+
if (process.env.JM2_DATA_DIR) {
|
|
22
|
+
return process.env.JM2_DATA_DIR;
|
|
23
|
+
}
|
|
24
|
+
return join(homedir(), DATA_DIR_NAME);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the jobs.json file path
|
|
29
|
+
* @returns {string} The jobs file path
|
|
30
|
+
*/
|
|
31
|
+
export function getJobsFile() {
|
|
32
|
+
return join(getDataDir(), 'jobs.json');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the config.json file path
|
|
37
|
+
* @returns {string} The config file path
|
|
38
|
+
*/
|
|
39
|
+
export function getConfigFile() {
|
|
40
|
+
return join(getDataDir(), 'config.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the daemon.pid file path
|
|
45
|
+
* @returns {string} The PID file path
|
|
46
|
+
*/
|
|
47
|
+
export function getPidFile() {
|
|
48
|
+
return join(getDataDir(), 'daemon.pid');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the daemon.log file path
|
|
53
|
+
* @returns {string} The daemon log file path
|
|
54
|
+
*/
|
|
55
|
+
export function getDaemonLogFile() {
|
|
56
|
+
return join(getDataDir(), 'daemon.log');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the logs directory path
|
|
61
|
+
* @returns {string} The logs directory path
|
|
62
|
+
*/
|
|
63
|
+
export function getLogsDir() {
|
|
64
|
+
return join(getDataDir(), 'logs');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the log file path for a specific job
|
|
69
|
+
* @param {string} jobName - The job name
|
|
70
|
+
* @returns {string} The job log file path
|
|
71
|
+
*/
|
|
72
|
+
export function getJobLogFile(jobName) {
|
|
73
|
+
return join(getLogsDir(), `${jobName}.log`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the IPC socket path
|
|
78
|
+
* On Unix: ~/.jm2/daemon.sock
|
|
79
|
+
* On Windows: \\.\pipe\jm2-daemon
|
|
80
|
+
* @returns {string} The socket path
|
|
81
|
+
*/
|
|
82
|
+
export function getSocketPath() {
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
return '\\\\.\\pipe\\jm2-daemon';
|
|
85
|
+
}
|
|
86
|
+
return join(getDataDir(), 'daemon.sock');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the history.json file path for execution history (deprecated, use getHistoryDbFile)
|
|
91
|
+
* @returns {string} The history file path
|
|
92
|
+
* @deprecated Use getHistoryDbFile() instead
|
|
93
|
+
*/
|
|
94
|
+
export function getHistoryFile() {
|
|
95
|
+
return join(getDataDir(), 'history.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the history.db file path for SQLite-based execution history
|
|
100
|
+
* @returns {string} The history database file path
|
|
101
|
+
*/
|
|
102
|
+
export function getHistoryDbFile() {
|
|
103
|
+
return join(getDataDir(), 'history.db');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the data directory exists
|
|
108
|
+
* Creates ~/.jm2/ if it doesn't exist
|
|
109
|
+
* @returns {string} The data directory path
|
|
110
|
+
*/
|
|
111
|
+
export function ensureDataDir() {
|
|
112
|
+
const dataDir = getDataDir();
|
|
113
|
+
if (!existsSync(dataDir)) {
|
|
114
|
+
mkdirSync(dataDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
return dataDir;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Ensure the logs directory exists
|
|
121
|
+
* Creates ~/.jm2/logs/ if it doesn't exist
|
|
122
|
+
* @returns {string} The logs directory path
|
|
123
|
+
*/
|
|
124
|
+
export function ensureLogsDir() {
|
|
125
|
+
ensureDataDir();
|
|
126
|
+
const logsDir = getLogsDir();
|
|
127
|
+
if (!existsSync(logsDir)) {
|
|
128
|
+
mkdirSync(logsDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
return logsDir;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if the data directory exists
|
|
135
|
+
* @returns {boolean} True if data directory exists
|
|
136
|
+
*/
|
|
137
|
+
export function dataDirExists() {
|
|
138
|
+
return existsSync(getDataDir());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if the PID file exists
|
|
143
|
+
* @returns {boolean} True if PID file exists
|
|
144
|
+
*/
|
|
145
|
+
export function pidFileExists() {
|
|
146
|
+
return existsSync(getPidFile());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default {
|
|
150
|
+
getDataDir,
|
|
151
|
+
getJobsFile,
|
|
152
|
+
getConfigFile,
|
|
153
|
+
getPidFile,
|
|
154
|
+
getDaemonLogFile,
|
|
155
|
+
getLogsDir,
|
|
156
|
+
getJobLogFile,
|
|
157
|
+
getSocketPath,
|
|
158
|
+
getHistoryFile,
|
|
159
|
+
getHistoryDbFile,
|
|
160
|
+
ensureDataDir,
|
|
161
|
+
ensureLogsDir,
|
|
162
|
+
dataDirExists,
|
|
163
|
+
pidFileExists,
|
|
164
|
+
};
|