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,274 @@
1
+ /**
2
+ * JM2 CLI
3
+ * Command-line interface using Commander.js
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import { readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+
11
+ import { startCommand } from './commands/start.js';
12
+ import { stopCommand } from './commands/stop.js';
13
+ import { restartCommand } from './commands/restart.js';
14
+ import { statusCommand } from './commands/status.js';
15
+ import { addCommand } from './commands/add.js';
16
+ import { listCommand } from './commands/list.js';
17
+ import { showCommand } from './commands/show.js';
18
+ import { removeCommand } from './commands/remove.js';
19
+ import { pauseCommand } from './commands/pause.js';
20
+ import { resumeCommand } from './commands/resume.js';
21
+ import { runCommand } from './commands/run.js';
22
+ import { editCommand } from './commands/edit.js';
23
+ import { configCommand } from './commands/config.js';
24
+ import { logsCommand } from './commands/logs.js';
25
+ import { historyCommand } from './commands/history.js';
26
+ import { flushCommand } from './commands/flush.js';
27
+ import { exportCommand } from './commands/export.js';
28
+ import { importCommand } from './commands/import.js';
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = dirname(__filename);
32
+
33
+ /**
34
+ * Collect multiple option values
35
+ * @param {string} value - Current value
36
+ * @param {Array} previous - Previous values
37
+ * @returns {Array} Combined values
38
+ */
39
+ function collect(value, previous) {
40
+ return previous.concat([value]);
41
+ }
42
+
43
+ /**
44
+ * Get package version from package.json
45
+ * @returns {string} Package version
46
+ */
47
+ function getVersion() {
48
+ try {
49
+ const packagePath = join(__dirname, '../../package.json');
50
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
51
+ return packageJson.version;
52
+ } catch {
53
+ return '0.1.0';
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Run the CLI
59
+ */
60
+ export async function runCli() {
61
+ const program = new Command();
62
+
63
+ program
64
+ .name('jm2')
65
+ .description('JM2 (Job Manager 2) - A simple yet powerful job scheduler')
66
+ .version(getVersion(), '-v, --version', 'Display version number');
67
+
68
+ // Daemon management commands
69
+ program
70
+ .command('start')
71
+ .description('Start the JM2 daemon')
72
+ .option('-f, --foreground', 'Run in foreground (don\'t daemonize)', false)
73
+ .action(async (options) => {
74
+ const exitCode = await startCommand(options);
75
+ process.exit(exitCode);
76
+ });
77
+
78
+ program
79
+ .command('stop')
80
+ .description('Stop the JM2 daemon')
81
+ .action(async () => {
82
+ const exitCode = await stopCommand();
83
+ process.exit(exitCode);
84
+ });
85
+
86
+ program
87
+ .command('restart')
88
+ .description('Restart the JM2 daemon')
89
+ .action(async () => {
90
+ const exitCode = await restartCommand();
91
+ process.exit(exitCode);
92
+ });
93
+
94
+ program
95
+ .command('status')
96
+ .description('Show daemon status and statistics')
97
+ .action(async () => {
98
+ const exitCode = await statusCommand();
99
+ process.exit(exitCode);
100
+ });
101
+
102
+ // Job management commands
103
+ program
104
+ .command('add [command]')
105
+ .description('Add a new job')
106
+ .option('-n, --name <name>', 'Job name (unique identifier)')
107
+ .option('-c, --cron <expression>', 'Cron expression for recurring jobs')
108
+ .option('-a, --at <datetime>', 'Run once at specific datetime (ISO 8601, "today 10:00", "tomorrow 14:30")')
109
+ .option('-i, --delay <duration>', 'Run once after duration (e.g., "30m", "2h", "1d")')
110
+ .option('-t, --tag <tag>', 'Add a tag (can be used multiple times)', collect, [])
111
+ .option('--cwd <path>', 'Working directory for job execution')
112
+ .option('-e, --env <env>', 'Environment variable (format: KEY=value, can be used multiple times)', collect, [])
113
+ .option('--timeout <duration>', 'Timeout for job execution (e.g., "30m", "2h")')
114
+ .option('--retry <count>', 'Number of retry attempts on failure', '0')
115
+ .option('--examples', 'Show common examples of jm2 add')
116
+ .action(async (command, options) => {
117
+ const exitCode = await addCommand(command, options);
118
+ process.exit(exitCode);
119
+ });
120
+
121
+ program
122
+ .command('list')
123
+ .description('List all jobs')
124
+ .option('-t, --tag <tag>', 'Filter by tag')
125
+ .option('-s, --status <status>', 'Filter by status (active, paused, completed, failed)')
126
+ .option('--type <type>', 'Filter by type (cron, once)')
127
+ .option('-v, --verbose', 'Show detailed information', false)
128
+ .action(async (options) => {
129
+ const exitCode = await listCommand(options);
130
+ process.exit(exitCode);
131
+ });
132
+
133
+ program
134
+ .command('show <job>')
135
+ .description('Show detailed information about a job')
136
+ .action(async (job) => {
137
+ const exitCode = await showCommand(job);
138
+ process.exit(exitCode);
139
+ });
140
+
141
+ program
142
+ .command('remove <jobs...>')
143
+ .description('Remove one or more jobs')
144
+ .option('-f, --force', 'Force removal without confirmation', false)
145
+ .action(async (jobs, options) => {
146
+ const exitCode = await removeCommand(jobs, options);
147
+ process.exit(exitCode);
148
+ });
149
+
150
+ program
151
+ .command('pause <jobs...>')
152
+ .description('Pause one or more jobs')
153
+ .action(async (jobs) => {
154
+ const exitCode = await pauseCommand(jobs);
155
+ process.exit(exitCode);
156
+ });
157
+
158
+ program
159
+ .command('resume <jobs...>')
160
+ .description('Resume one or more paused jobs')
161
+ .action(async (jobs) => {
162
+ const exitCode = await resumeCommand(jobs);
163
+ process.exit(exitCode);
164
+ });
165
+
166
+ program
167
+ .command('run <job>')
168
+ .description('Run a job manually')
169
+ .option('-w, --wait', 'Wait for job to complete and show output', false)
170
+ .action(async (job, options) => {
171
+ const exitCode = await runCommand(job, options);
172
+ process.exit(exitCode);
173
+ });
174
+
175
+ program
176
+ .command('edit <job>')
177
+ .description('Edit an existing job')
178
+ .option('--command <command>', 'New command to execute')
179
+ .option('-n, --name <name>', 'New job name')
180
+ .option('-c, --cron <expression>', 'New cron expression')
181
+ .option('-a, --at <datetime>', 'New datetime to run once (replaces cron)')
182
+ .option('-i, --delay <duration>', 'New relative time to run once (replaces cron)')
183
+ .option('--cwd <path>', 'New working directory')
184
+ .option('-e, --env <env>', 'Set environment variable (format: KEY=value, can be used multiple times)', collect, [])
185
+ .option('--timeout <duration>', 'New timeout for job execution')
186
+ .option('--retry <count>', 'New retry count on failure')
187
+ .option('-t, --tag <tag>', 'Set tags (replaces all existing tags, can be used multiple times)', collect, [])
188
+ .action(async (job, options) => {
189
+ const exitCode = await editCommand(job, options);
190
+ process.exit(exitCode);
191
+ });
192
+
193
+ // Configuration command
194
+ program
195
+ .command('config')
196
+ .description('View or modify configuration settings')
197
+ .option('-s, --show', 'Show all configuration (default)')
198
+ .option('--log-max-size <size>', 'Set maximum log file size (e.g., 10mb, 50MB)')
199
+ .option('--log-max-files <count>', 'Set maximum number of log files to keep')
200
+ .option('--level <level>', 'Set log level (DEBUG, INFO, WARN, ERROR)')
201
+ .option('--max-concurrent <count>', 'Set maximum concurrent job executions')
202
+ .option('--reset', 'Reset configuration to defaults')
203
+ .action(async (options) => {
204
+ const exitCode = await configCommand(options);
205
+ process.exit(exitCode);
206
+ });
207
+
208
+ // Logs command
209
+ program
210
+ .command('logs <job>')
211
+ .description('View job execution logs')
212
+ .option('-n, --lines <count>', 'Number of lines to show (default: 50)', '50')
213
+ .option('-f, --follow', 'Follow log output in real-time', false)
214
+ .option('--since <time>', 'Show logs since time (e.g., "1h", "30m", "2026-01-31")')
215
+ .option('--until <time>', 'Show logs until time')
216
+ .option('--timestamps', 'Show timestamps', true)
217
+ .option('--no-timestamps', 'Hide timestamps')
218
+ .action(async (job, options) => {
219
+ const exitCode = await logsCommand(job, options);
220
+ process.exit(exitCode);
221
+ });
222
+
223
+ // History command
224
+ program
225
+ .command('history [job]')
226
+ .description('Show execution history for a job or all jobs')
227
+ .option('-f, --failed', 'Show only failed executions', false)
228
+ .option('-s, --success', 'Show only successful executions', false)
229
+ .option('-l, --limit <count>', 'Maximum number of entries to show (default: 20)', '20')
230
+ .action(async (job, options) => {
231
+ const exitCode = await historyCommand(job, options);
232
+ process.exit(exitCode);
233
+ });
234
+
235
+ // Flush command
236
+ program
237
+ .command('flush')
238
+ .description('Clean up completed one-time jobs, old logs, and history')
239
+ .option('--no-jobs', 'Skip removing completed one-time jobs')
240
+ .option('--logs <duration>', 'Remove logs older than duration (e.g., "7d", "24h")')
241
+ .option('--history <duration>', 'Remove history older than duration (e.g., "30d")')
242
+ .option('-a, --all', 'Remove all logs and history (equivalent to --logs --history with no age limit)')
243
+ .option('--force', 'Skip confirmation prompt', false)
244
+ .action(async (options) => {
245
+ const exitCode = await flushCommand(options);
246
+ process.exit(exitCode);
247
+ });
248
+
249
+ // Export command
250
+ program
251
+ .command('export')
252
+ .description('Export job configurations to a JSON file')
253
+ .option('-o, --output <file>', 'Output file path (default: jm2-export.json)', 'jm2-export.json')
254
+ .action(async (options) => {
255
+ const exitCode = await exportCommand(options);
256
+ process.exit(exitCode);
257
+ });
258
+
259
+ // Import command
260
+ program
261
+ .command('import <file>')
262
+ .description('Import job configurations from a JSON file')
263
+ .option('-s, --skip', 'Skip jobs with conflicting names instead of renaming', false)
264
+ .option('-f, --force', 'Skip confirmation prompt', false)
265
+ .action(async (file, options) => {
266
+ const exitCode = await importCommand(file, options);
267
+ process.exit(exitCode);
268
+ });
269
+
270
+ // Parse command line arguments
271
+ await program.parseAsync();
272
+ }
273
+
274
+ export default { runCli };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Output formatting utilities for JM2 CLI
3
+ * Provides table formatting, colors, and other output helpers
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import Table from 'cli-table3';
8
+
9
+ /**
10
+ * Format a duration in milliseconds to human-readable string
11
+ * @param {number} ms - Duration in milliseconds
12
+ * @returns {string} Formatted duration
13
+ */
14
+ export function formatDuration(ms) {
15
+ if (ms < 1000) {
16
+ return `${ms}ms`;
17
+ }
18
+
19
+ const seconds = Math.floor(ms / 1000);
20
+ if (seconds < 60) {
21
+ return `${seconds}s`;
22
+ }
23
+
24
+ const minutes = Math.floor(seconds / 60);
25
+ const remainingSeconds = seconds % 60;
26
+ if (minutes < 60) {
27
+ return remainingSeconds > 0
28
+ ? `${minutes}m ${remainingSeconds}s`
29
+ : `${minutes}m`;
30
+ }
31
+
32
+ const hours = Math.floor(minutes / 60);
33
+ const remainingMinutes = minutes % 60;
34
+ if (hours < 24) {
35
+ return remainingMinutes > 0
36
+ ? `${hours}h ${remainingMinutes}m`
37
+ : `${hours}h`;
38
+ }
39
+
40
+ const days = Math.floor(hours / 24);
41
+ const remainingHours = hours % 24;
42
+ return remainingHours > 0
43
+ ? `${days}d ${remainingHours}h`
44
+ : `${days}d`;
45
+ }
46
+
47
+ /**
48
+ * Format uptime from a start date
49
+ * @param {Date|string} startDate - When the process started
50
+ * @returns {string} Formatted uptime
51
+ */
52
+ export function formatUptime(startDate) {
53
+ const start = new Date(startDate);
54
+ const now = new Date();
55
+ const ms = now - start;
56
+ return formatDuration(ms);
57
+ }
58
+
59
+ /**
60
+ * Format a date for display
61
+ * @param {Date|string} date - Date to format
62
+ * @returns {string} Formatted date
63
+ */
64
+ export function formatDate(date) {
65
+ if (!date) return chalk.gray('Never');
66
+ const d = new Date(date);
67
+ return d.toLocaleString();
68
+ }
69
+
70
+ /**
71
+ * Format a relative time (e.g., "2 minutes ago", "in 5 minutes")
72
+ * @param {Date|string} date - Date to format
73
+ * @returns {string} Relative time string
74
+ */
75
+ export function formatRelativeTime(date) {
76
+ if (!date) return chalk.gray('Never');
77
+
78
+ const d = new Date(date);
79
+ const now = new Date();
80
+ const diffMs = d - now;
81
+ const absMs = Math.abs(diffMs);
82
+
83
+ const suffix = diffMs < 0 ? 'ago' : 'in';
84
+ const duration = formatDuration(absMs);
85
+
86
+ if (diffMs < 0) {
87
+ return chalk.gray(`${duration} ago`);
88
+ } else {
89
+ return chalk.cyan(`in ${duration}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Colorize a job status
95
+ * @param {string} status - Job status
96
+ * @returns {string} Colorized status
97
+ */
98
+ export function colorizeStatus(status) {
99
+ switch (status) {
100
+ case 'active':
101
+ return chalk.green('active');
102
+ case 'paused':
103
+ return chalk.yellow('paused');
104
+ case 'completed':
105
+ return chalk.blue('completed');
106
+ case 'failed':
107
+ return chalk.red('failed');
108
+ case 'running':
109
+ return chalk.cyan('running');
110
+ default:
111
+ return chalk.gray(status);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Colorize daemon status
117
+ * @param {boolean} running - Whether daemon is running
118
+ * @returns {string} Colorized status
119
+ */
120
+ export function colorizeDaemonStatus(running) {
121
+ return running
122
+ ? chalk.green('Running')
123
+ : chalk.red('Stopped');
124
+ }
125
+
126
+ /**
127
+ * Create a table for job listing
128
+ * @returns {Table} CLI table instance
129
+ */
130
+ export function createJobTable() {
131
+ return new Table({
132
+ head: [
133
+ chalk.bold('ID'),
134
+ chalk.bold('Name'),
135
+ chalk.bold('Status'),
136
+ chalk.bold('Schedule'),
137
+ chalk.bold('Next Run'),
138
+ chalk.bold('Last Run'),
139
+ ],
140
+ colWidths: [6, 20, 12, 20, 20, 20],
141
+ wordWrap: true,
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Create a table for daemon status
147
+ * @returns {Table} CLI table instance
148
+ */
149
+ export function createStatusTable() {
150
+ return new Table({
151
+ colWidths: [20, 40],
152
+ style: {
153
+ head: [],
154
+ border: [],
155
+ },
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Print a success message
161
+ * @param {string} message - Message to print
162
+ */
163
+ export function printSuccess(message) {
164
+ console.log(chalk.green('✓'), message);
165
+ }
166
+
167
+ /**
168
+ * Print an error message
169
+ * @param {string} message - Message to print
170
+ */
171
+ export function printError(message) {
172
+ console.error(chalk.red('✗'), message);
173
+ }
174
+
175
+ /**
176
+ * Print a warning message
177
+ * @param {string} message - Message to print
178
+ */
179
+ export function printWarning(message) {
180
+ console.warn(chalk.yellow('⚠'), message);
181
+ }
182
+
183
+ /**
184
+ * Print an info message
185
+ * @param {string} message - Message to print
186
+ */
187
+ export function printInfo(message) {
188
+ console.log(chalk.blue('ℹ'), message);
189
+ }
190
+
191
+ /**
192
+ * Print a section header
193
+ * @param {string} title - Section title
194
+ */
195
+ export function printHeader(title) {
196
+ console.log();
197
+ console.log(chalk.bold.underline(title));
198
+ console.log();
199
+ }
200
+
201
+ /**
202
+ * Print a section header (alias for printHeader)
203
+ * @param {string} title - Section title
204
+ */
205
+ export function printSection(title) {
206
+ console.log();
207
+ console.log(chalk.bold.underline(title));
208
+ console.log();
209
+ }
210
+
211
+ /**
212
+ * Print a key-value pair
213
+ * @param {string} key - Key label
214
+ * @param {*} value - Value to display
215
+ */
216
+ export function printKeyValue(key, value) {
217
+ console.log(` ${chalk.cyan(key)}: ${value}`);
218
+ }
219
+
220
+ /**
221
+ * Print an empty line
222
+ */
223
+ export function printEmptyLine() {
224
+ console.log();
225
+ }
226
+
227
+ /**
228
+ * Format job schedule for display
229
+ * @param {object} job - Job object
230
+ * @returns {string} Formatted schedule
231
+ */
232
+ export function formatJobSchedule(job) {
233
+ if (job.cron) {
234
+ return chalk.gray(job.cron);
235
+ } else if (job.runAt) {
236
+ return chalk.gray(`at ${formatDate(job.runAt)}`);
237
+ } else {
238
+ return chalk.gray('Manual only');
239
+ }
240
+ }
241
+
242
+ export default {
243
+ formatDuration,
244
+ formatUptime,
245
+ formatDate,
246
+ formatRelativeTime,
247
+ colorizeStatus,
248
+ colorizeDaemonStatus,
249
+ createJobTable,
250
+ createStatusTable,
251
+ printSuccess,
252
+ printError,
253
+ printWarning,
254
+ printInfo,
255
+ printHeader,
256
+ printEmptyLine,
257
+ formatJobSchedule,
258
+ printSection,
259
+ printKeyValue,
260
+ // Aliases for convenience
261
+ success: printSuccess,
262
+ error: printError,
263
+ warning: printWarning,
264
+ info: printInfo,
265
+ section: printSection,
266
+ keyValue: printKeyValue,
267
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Prompt utilities for JM2 CLI
3
+ * Provides confirmation prompts and user interaction helpers
4
+ */
5
+
6
+ import readline from 'node:readline';
7
+
8
+ /**
9
+ * Ask for confirmation
10
+ * @param {string} message - Confirmation message
11
+ * @param {boolean} [defaultValue=false] - Default value if user just presses enter
12
+ * @returns {Promise<boolean>} True if confirmed
13
+ */
14
+ export async function confirm(message, defaultValue = false) {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+
20
+ const defaultPrompt = defaultValue ? 'Y/n' : 'y/N';
21
+
22
+ return new Promise((resolve) => {
23
+ rl.question(`${message} [${defaultPrompt}] `, (answer) => {
24
+ rl.close();
25
+
26
+ const trimmed = answer.trim().toLowerCase();
27
+
28
+ if (trimmed === '') {
29
+ resolve(defaultValue);
30
+ } else if (trimmed === 'y' || trimmed === 'yes') {
31
+ resolve(true);
32
+ } else {
33
+ resolve(false);
34
+ }
35
+ });
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Confirm destructive action
41
+ * @param {string} action - Description of the action
42
+ * @param {boolean} [force=false] - Skip confirmation if true
43
+ * @returns {Promise<boolean>} True if confirmed or forced
44
+ */
45
+ export async function confirmDestructive(action, force = false) {
46
+ if (force) {
47
+ return true;
48
+ }
49
+
50
+ return await confirm(`Are you sure you want to ${action}?`, false);
51
+ }
52
+
53
+ export default {
54
+ confirm,
55
+ confirmDestructive,
56
+ };