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.
Files changed (40) hide show
  1. package/GNU-AGPL-3.0 +665 -0
  2. package/README.md +603 -0
  3. package/bin/jm2.js +24 -0
  4. package/package.json +70 -0
  5. package/src/cli/commands/add.js +206 -0
  6. package/src/cli/commands/config.js +212 -0
  7. package/src/cli/commands/edit.js +198 -0
  8. package/src/cli/commands/export.js +61 -0
  9. package/src/cli/commands/flush.js +132 -0
  10. package/src/cli/commands/history.js +179 -0
  11. package/src/cli/commands/import.js +180 -0
  12. package/src/cli/commands/list.js +174 -0
  13. package/src/cli/commands/logs.js +415 -0
  14. package/src/cli/commands/pause.js +97 -0
  15. package/src/cli/commands/remove.js +107 -0
  16. package/src/cli/commands/restart.js +68 -0
  17. package/src/cli/commands/resume.js +96 -0
  18. package/src/cli/commands/run.js +115 -0
  19. package/src/cli/commands/show.js +159 -0
  20. package/src/cli/commands/start.js +46 -0
  21. package/src/cli/commands/status.js +47 -0
  22. package/src/cli/commands/stop.js +48 -0
  23. package/src/cli/index.js +274 -0
  24. package/src/cli/utils/output.js +267 -0
  25. package/src/cli/utils/prompts.js +56 -0
  26. package/src/core/config.js +227 -0
  27. package/src/core/history-db.js +439 -0
  28. package/src/core/job.js +329 -0
  29. package/src/core/logger.js +382 -0
  30. package/src/core/storage.js +315 -0
  31. package/src/daemon/executor.js +409 -0
  32. package/src/daemon/index.js +873 -0
  33. package/src/daemon/scheduler.js +465 -0
  34. package/src/ipc/client.js +112 -0
  35. package/src/ipc/protocol.js +183 -0
  36. package/src/ipc/server.js +92 -0
  37. package/src/utils/cron.js +205 -0
  38. package/src/utils/datetime.js +237 -0
  39. package/src/utils/duration.js +226 -0
  40. 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
+ };