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,96 @@
1
+ /**
2
+ * JM2 resume command
3
+ * Resumes one or more paused jobs
4
+ */
5
+
6
+ import { send } from '../../ipc/client.js';
7
+ import { MessageType } from '../../ipc/protocol.js';
8
+ import { printSuccess, printError, printWarning } from '../utils/output.js';
9
+ import { isDaemonRunning } from '../../daemon/index.js';
10
+
11
+ /**
12
+ * Execute the resume command
13
+ * @param {string|string[]} jobRefs - Job ID(s) or name(s)
14
+ * @param {object} options - Command options
15
+ * @returns {Promise<number>} Exit code
16
+ */
17
+ export async function resumeCommand(jobRefs, options = {}) {
18
+ // Check if daemon is running
19
+ if (!isDaemonRunning()) {
20
+ printError('Daemon is not running. Start it with: jm2 start');
21
+ return 1;
22
+ }
23
+
24
+ // Normalize jobRefs to array
25
+ const refs = Array.isArray(jobRefs) ? jobRefs : [jobRefs];
26
+
27
+ if (refs.length === 0 || (refs.length === 1 && !refs[0])) {
28
+ printError('Job ID or name is required');
29
+ return 1;
30
+ }
31
+
32
+ let successCount = 0;
33
+ let failCount = 0;
34
+
35
+ for (const jobRef of refs) {
36
+ const result = await resumeSingleJob(jobRef);
37
+ if (result) {
38
+ successCount++;
39
+ } else {
40
+ failCount++;
41
+ }
42
+ }
43
+
44
+ // Summary
45
+ if (successCount > 0) {
46
+ printSuccess(`Resumed ${successCount} job(s)`);
47
+ }
48
+
49
+ if (failCount > 0) {
50
+ printError(`Failed to resume ${failCount} job(s)`);
51
+ return 1;
52
+ }
53
+
54
+ return 0;
55
+ }
56
+
57
+ /**
58
+ * Resume a single job
59
+ * @param {string} jobRef - Job ID or name
60
+ * @returns {Promise<boolean>} True if successful
61
+ */
62
+ async function resumeSingleJob(jobRef) {
63
+ try {
64
+ // Determine if jobRef is an ID (numeric) or name
65
+ const jobId = parseInt(jobRef, 10);
66
+ const message = isNaN(jobId)
67
+ ? { type: MessageType.JOB_RESUME, jobName: jobRef }
68
+ : { type: MessageType.JOB_RESUME, jobId };
69
+
70
+ const response = await send(message);
71
+
72
+ if (response.type === MessageType.ERROR) {
73
+ printError(`${jobRef}: ${response.message}`);
74
+ return false;
75
+ }
76
+
77
+ if (response.type === MessageType.JOB_RESUMED) {
78
+ if (response.job) {
79
+ const name = response.job.name || response.job.id;
80
+ printSuccess(`Resumed: ${name}`);
81
+ return true;
82
+ } else {
83
+ printWarning(`Job not found: ${jobRef}`);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ printError(`${jobRef}: Unexpected response from daemon`);
89
+ return false;
90
+ } catch (error) {
91
+ printError(`${jobRef}: ${error.message}`);
92
+ return false;
93
+ }
94
+ }
95
+
96
+ export default { resumeCommand };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * JM2 run command
3
+ * Manually execute a job immediately
4
+ */
5
+
6
+ import { send } from '../../ipc/client.js';
7
+ import { MessageType } from '../../ipc/protocol.js';
8
+ import { printSuccess, printError, printInfo } from '../utils/output.js';
9
+ import { isDaemonRunning } from '../../daemon/index.js';
10
+
11
+ /**
12
+ * Execute the run command
13
+ * @param {string} jobRef - Job ID or name
14
+ * @param {object} options - Command options
15
+ * @returns {Promise<number>} Exit code
16
+ */
17
+ export async function runCommand(jobRef, options = {}) {
18
+ // Check if daemon is running
19
+ if (!isDaemonRunning()) {
20
+ printError('Daemon is not running. Start it with: jm2 start');
21
+ return 1;
22
+ }
23
+
24
+ if (!jobRef || jobRef.trim() === '') {
25
+ printError('Job ID or name is required');
26
+ return 1;
27
+ }
28
+
29
+ try {
30
+ // Determine if jobRef is an ID (numeric) or name
31
+ const jobId = parseInt(jobRef, 10);
32
+ const message = isNaN(jobId)
33
+ ? { type: MessageType.JOB_RUN, jobName: jobRef, wait: options.wait }
34
+ : { type: MessageType.JOB_RUN, jobId, wait: options.wait };
35
+
36
+ printInfo(`Running job: ${jobRef}...`);
37
+
38
+ const response = await send(message);
39
+
40
+ if (response.type === MessageType.ERROR) {
41
+ printError(response.message);
42
+ return 1;
43
+ }
44
+
45
+ if (response.type === MessageType.JOB_RUN_RESULT) {
46
+ const result = response.result;
47
+
48
+ if (result.error) {
49
+ printError(`Job execution failed: ${result.error}`);
50
+ return 1;
51
+ }
52
+
53
+ if (options.wait) {
54
+ // Display execution results
55
+ if (result.status === 'success') {
56
+ printSuccess('Job completed successfully');
57
+ if (result.stdout) {
58
+ console.log('\n--- stdout ---');
59
+ console.log(result.stdout);
60
+ }
61
+ if (result.stderr) {
62
+ console.log('\n--- stderr ---');
63
+ console.log(result.stderr);
64
+ }
65
+ console.log(`\nExit code: ${result.exitCode || 0}`);
66
+ console.log(`Duration: ${formatDuration(result.duration || 0)}`);
67
+ } else if (result.status === 'timeout') {
68
+ printError('Job timed out');
69
+ return 1;
70
+ } else {
71
+ printError(`Job failed with status: ${result.status}`);
72
+ if (result.stdout) {
73
+ console.log('\n--- stdout ---');
74
+ console.log(result.stdout);
75
+ }
76
+ if (result.stderr) {
77
+ console.log('\n--- stderr ---');
78
+ console.log(result.stderr);
79
+ }
80
+ return 1;
81
+ }
82
+ } else {
83
+ printSuccess(`Job queued for execution (ID: ${result.jobId || jobRef})`);
84
+ printInfo('Use --wait to wait for completion and see output');
85
+ }
86
+
87
+ return 0;
88
+ }
89
+
90
+ printError('Unexpected response from daemon');
91
+ return 1;
92
+ } catch (error) {
93
+ printError(`Failed to run job: ${error.message}`);
94
+ return 1;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Format duration in milliseconds to human-readable string
100
+ * @param {number} ms - Duration in milliseconds
101
+ * @returns {string} Formatted duration
102
+ */
103
+ function formatDuration(ms) {
104
+ if (ms < 1000) {
105
+ return `${ms}ms`;
106
+ }
107
+ if (ms < 60000) {
108
+ return `${(ms / 1000).toFixed(2)}s`;
109
+ }
110
+ const minutes = Math.floor(ms / 60000);
111
+ const seconds = ((ms % 60000) / 1000).toFixed(1);
112
+ return `${minutes}m ${seconds}s`;
113
+ }
114
+
115
+ export default { runCommand };
@@ -0,0 +1,159 @@
1
+ /**
2
+ * JM2 show command
3
+ * Shows detailed information about a job
4
+ */
5
+
6
+ import { send } from '../../ipc/client.js';
7
+ import { MessageType } from '../../ipc/protocol.js';
8
+ import {
9
+ printSuccess,
10
+ printError,
11
+ printInfo,
12
+ printHeader,
13
+ colorizeStatus,
14
+ formatDate,
15
+ formatRelativeTime,
16
+ formatJobSchedule,
17
+ } from '../utils/output.js';
18
+ import { isDaemonRunning } from '../../daemon/index.js';
19
+ import { getJobLogFile } from '../../utils/paths.js';
20
+ import chalk from 'chalk';
21
+
22
+ /**
23
+ * Execute the show command
24
+ * @param {string} jobRef - Job ID or name
25
+ * @param {object} options - Command options
26
+ * @returns {Promise<number>} Exit code
27
+ */
28
+ export async function showCommand(jobRef, options = {}) {
29
+ // Check if daemon is running
30
+ if (!isDaemonRunning()) {
31
+ printError('Daemon is not running. Start it with: jm2 start');
32
+ return 1;
33
+ }
34
+
35
+ if (!jobRef || jobRef.trim() === '') {
36
+ printError('Job ID or name is required');
37
+ return 1;
38
+ }
39
+
40
+ try {
41
+ // Determine if jobRef is an ID (numeric) or name
42
+ const jobId = parseInt(jobRef, 10);
43
+ const message = isNaN(jobId)
44
+ ? { type: MessageType.JOB_GET, jobName: jobRef }
45
+ : { type: MessageType.JOB_GET, jobId };
46
+
47
+ const response = await send(message);
48
+
49
+ if (response.type === MessageType.ERROR) {
50
+ printError(response.message);
51
+ return 1;
52
+ }
53
+
54
+ if (response.type === MessageType.JOB_GET_RESULT) {
55
+ const job = response.job;
56
+
57
+ if (!job) {
58
+ printError(`Job not found: ${jobRef}`);
59
+ return 1;
60
+ }
61
+
62
+ printJobDetails(job);
63
+ return 0;
64
+ }
65
+
66
+ printError('Unexpected response from daemon');
67
+ return 1;
68
+ } catch (error) {
69
+ printError(`Failed to get job: ${error.message}`);
70
+ return 1;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Print detailed job information
76
+ * @param {object} job - Job object
77
+ */
78
+ function printJobDetails(job) {
79
+ printHeader(`Job: ${job.name || job.id}`);
80
+
81
+ // Basic info
82
+ console.log(`${chalk.bold('ID:')} ${job.id}`);
83
+ console.log(`${chalk.bold('Name:')} ${job.name || chalk.gray('-')}`);
84
+ console.log(`${chalk.bold('Status:')} ${colorizeStatus(job.status)}`);
85
+ console.log(`${chalk.bold('Type:')} ${job.type || 'manual'}`);
86
+
87
+ // Schedule
88
+ console.log(`${chalk.bold('Schedule:')} ${formatJobSchedule(job)}`);
89
+
90
+ if (job.nextRun) {
91
+ console.log(`${chalk.bold('Next Run:')} ${formatDate(job.nextRun)} (${formatRelativeTime(job.nextRun)})`);
92
+ }
93
+
94
+ if (job.lastRun) {
95
+ console.log(`${chalk.bold('Last Run:')} ${formatDate(job.lastRun)} (${formatRelativeTime(job.lastRun)})`);
96
+ }
97
+
98
+ // Command
99
+ console.log();
100
+ console.log(`${chalk.bold('Command:')}`);
101
+ console.log(` ${job.command}`);
102
+
103
+ // Working directory
104
+ if (job.cwd) {
105
+ console.log();
106
+ console.log(`${chalk.bold('Working Directory:')}`);
107
+ console.log(` ${job.cwd}`);
108
+ }
109
+
110
+ // Environment variables
111
+ if (job.env && Object.keys(job.env).length > 0) {
112
+ console.log();
113
+ console.log(`${chalk.bold('Environment Variables:')}`);
114
+ for (const [key, value] of Object.entries(job.env)) {
115
+ console.log(` ${key}=${value}`);
116
+ }
117
+ }
118
+
119
+ // Tags
120
+ if (job.tags && job.tags.length > 0) {
121
+ console.log();
122
+ console.log(`${chalk.bold('Tags:')} ${job.tags.join(', ')}`);
123
+ }
124
+
125
+ // Timeout and retry
126
+ if (job.timeout) {
127
+ console.log();
128
+ console.log(`${chalk.bold('Timeout:')} ${job.timeout}ms`);
129
+ }
130
+
131
+ if (job.retry > 0) {
132
+ console.log(`${chalk.bold('Retries:')} ${job.retry}`);
133
+ }
134
+
135
+ // Metadata
136
+ console.log();
137
+ console.log(`${chalk.bold('Created:')} ${formatDate(job.createdAt)}`);
138
+ console.log(`${chalk.bold('Updated:')} ${formatDate(job.updatedAt)}`);
139
+
140
+ // Log file path
141
+ console.log();
142
+ const logFile = getJobLogFile(job.name || `job-${job.id}`);
143
+ console.log(`${chalk.bold('Log File:')} ${logFile}`);
144
+
145
+ // Execution info
146
+ if (job.lastExitCode !== undefined && job.lastExitCode !== null) {
147
+ const exitColor = job.lastExitCode === 0 ? chalk.green : chalk.red;
148
+ console.log();
149
+ console.log(`${chalk.bold('Last Exit Code:')} ${exitColor(job.lastExitCode)}`);
150
+ }
151
+
152
+ if (job.retryCount > 0) {
153
+ console.log(`${chalk.bold('Retry Count:')} ${job.retryCount}`);
154
+ }
155
+
156
+ console.log();
157
+ }
158
+
159
+ export default { showCommand };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * JM2 start command
3
+ * Starts the JM2 daemon process
4
+ */
5
+
6
+ import { startDaemon, isDaemonRunning, getDaemonStatus } from '../../daemon/index.js';
7
+ import { printSuccess, printError, printInfo } from '../utils/output.js';
8
+
9
+ /**
10
+ * Execute the start command
11
+ * @param {object} options - Command options
12
+ * @param {boolean} options.foreground - Run in foreground mode
13
+ * @returns {Promise<number>} Exit code
14
+ */
15
+ export async function startCommand(options = {}) {
16
+ const { foreground = false } = options;
17
+
18
+ // Check if daemon is already running
19
+ if (isDaemonRunning()) {
20
+ const { pid } = getDaemonStatus();
21
+ printInfo(`Daemon is already running (PID: ${pid})`);
22
+ return 0;
23
+ }
24
+
25
+ try {
26
+ if (foreground) {
27
+ printInfo('Starting daemon in foreground mode...');
28
+ } else {
29
+ printInfo('Starting daemon...');
30
+ }
31
+
32
+ await startDaemon({ foreground });
33
+
34
+ if (!foreground) {
35
+ const { pid } = getDaemonStatus();
36
+ printSuccess(`Daemon started (PID: ${pid})`);
37
+ }
38
+
39
+ return 0;
40
+ } catch (error) {
41
+ printError(`Failed to start daemon: ${error.message}`);
42
+ return 1;
43
+ }
44
+ }
45
+
46
+ export default startCommand;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * JM2 status command
3
+ * Shows the daemon status and statistics
4
+ */
5
+
6
+ import { isDaemonRunning, getDaemonStatus } from '../../daemon/index.js';
7
+ import { getJobs } from '../../core/storage.js';
8
+ import {
9
+ colorizeDaemonStatus,
10
+ createStatusTable,
11
+ printHeader,
12
+ } from '../utils/output.js';
13
+
14
+ /**
15
+ * Execute the status command
16
+ * @param {object} _options - Command options (none for status)
17
+ * @returns {Promise<number>} Exit code
18
+ */
19
+ export async function statusCommand(_options = {}) {
20
+ const running = isDaemonRunning();
21
+ const { pid } = getDaemonStatus();
22
+
23
+ // Count jobs
24
+ const jobs = getJobs();
25
+ const totalJobs = jobs.length;
26
+ const activeJobs = jobs.filter(j => j.status === 'active').length;
27
+ const pausedJobs = jobs.filter(j => j.status === 'paused').length;
28
+
29
+ // Print header
30
+ printHeader('JM2 Daemon Status');
31
+
32
+ // Create status table
33
+ const table = createStatusTable();
34
+
35
+ table.push(
36
+ ['Status:', colorizeDaemonStatus(running)],
37
+ ['PID:', pid ? pid.toString() : '-'],
38
+ ['Jobs:', `${totalJobs} total (${activeJobs} active, ${pausedJobs} paused)`]
39
+ );
40
+
41
+ console.log(table.toString());
42
+ console.log();
43
+
44
+ return 0;
45
+ }
46
+
47
+ export default statusCommand;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * JM2 stop command
3
+ * Stops the JM2 daemon process
4
+ */
5
+
6
+ import { stopDaemon, isDaemonRunning, getDaemonStatus } from '../../daemon/index.js';
7
+ import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
8
+
9
+ /**
10
+ * Execute the stop command
11
+ * @param {object} _options - Command options (none for stop)
12
+ * @returns {Promise<number>} Exit code
13
+ */
14
+ export async function stopCommand(_options = {}) {
15
+ // Check if daemon is running
16
+ if (!isDaemonRunning()) {
17
+ printError('Daemon is not running');
18
+ return 3; // Specific exit code for daemon not running
19
+ }
20
+
21
+ const { pid } = getDaemonStatus();
22
+ printInfo(`Stopping daemon (PID: ${pid})...`);
23
+
24
+ try {
25
+ const stopped = stopDaemon();
26
+
27
+ if (stopped) {
28
+ // Wait a moment to confirm the daemon stopped
29
+ await new Promise(resolve => setTimeout(resolve, 500));
30
+
31
+ if (!isDaemonRunning()) {
32
+ printSuccess('Daemon stopped');
33
+ return 0;
34
+ } else {
35
+ printWarning('Daemon may not have stopped cleanly');
36
+ return 1;
37
+ }
38
+ } else {
39
+ printError('Failed to send stop signal to daemon');
40
+ return 1;
41
+ }
42
+ } catch (error) {
43
+ printError(`Failed to stop daemon: ${error.message}`);
44
+ return 1;
45
+ }
46
+ }
47
+
48
+ export default stopCommand;