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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 flush command
|
|
3
|
+
* Cleans up completed one-time jobs, old logs, and old history entries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { send } from '../../ipc/client.js';
|
|
7
|
+
import { MessageType } from '../../ipc/protocol.js';
|
|
8
|
+
import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
|
|
9
|
+
import { isDaemonRunning } from '../../daemon/index.js';
|
|
10
|
+
import { confirmDestructive } from '../utils/prompts.js';
|
|
11
|
+
import { parseDuration } from '../../utils/duration.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute the flush command
|
|
15
|
+
* @param {object} options - Command options
|
|
16
|
+
* @returns {Promise<number>} Exit code
|
|
17
|
+
*/
|
|
18
|
+
export async function flushCommand(options = {}) {
|
|
19
|
+
// Check if daemon is running
|
|
20
|
+
if (!isDaemonRunning()) {
|
|
21
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build a summary of what will be flushed
|
|
26
|
+
const actions = [];
|
|
27
|
+
if (options.jobs !== false) {
|
|
28
|
+
actions.push('completed one-time jobs');
|
|
29
|
+
}
|
|
30
|
+
if (options.logs) {
|
|
31
|
+
actions.push(`logs older than ${options.logs}`);
|
|
32
|
+
}
|
|
33
|
+
if (options.history) {
|
|
34
|
+
actions.push(`history entries older than ${options.history}`);
|
|
35
|
+
}
|
|
36
|
+
if (options.all) {
|
|
37
|
+
actions.length = 0;
|
|
38
|
+
actions.push('completed one-time jobs, all logs, and all history');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (actions.length === 0) {
|
|
42
|
+
actions.push('completed one-time jobs (default)');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Confirm destructive action unless --force is used
|
|
46
|
+
const action = `flush ${actions.join(', ')}`;
|
|
47
|
+
const confirmed = await confirmDestructive(action, options.force);
|
|
48
|
+
if (!confirmed) {
|
|
49
|
+
printInfo('Operation cancelled');
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Build flush request
|
|
55
|
+
const flushRequest = {
|
|
56
|
+
type: MessageType.FLUSH,
|
|
57
|
+
jobs: options.jobs !== false && !options.all,
|
|
58
|
+
logs: options.logs || options.all || false,
|
|
59
|
+
logsAge: options.logs || null,
|
|
60
|
+
history: options.history || options.all || false,
|
|
61
|
+
historyAge: options.history || null,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Parse duration options if provided
|
|
65
|
+
if (flushRequest.logsAge && !options.all) {
|
|
66
|
+
const duration = parseDuration(flushRequest.logsAge);
|
|
67
|
+
if (duration === null) {
|
|
68
|
+
printError(`Invalid duration for --logs: ${options.logs}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
flushRequest.logsAgeMs = duration;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (flushRequest.historyAge && !options.all) {
|
|
75
|
+
const duration = parseDuration(flushRequest.historyAge);
|
|
76
|
+
if (duration === null) {
|
|
77
|
+
printError(`Invalid duration for --history: ${options.history}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
flushRequest.historyAgeMs = duration;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await send(flushRequest);
|
|
84
|
+
|
|
85
|
+
if (response.type === MessageType.ERROR) {
|
|
86
|
+
printError(response.message);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (response.type === MessageType.FLUSH_RESULT) {
|
|
91
|
+
// Report results
|
|
92
|
+
let hasResults = false;
|
|
93
|
+
|
|
94
|
+
if (response.jobsRemoved > 0) {
|
|
95
|
+
printSuccess(`Removed ${response.jobsRemoved} completed one-time job(s)`);
|
|
96
|
+
hasResults = true;
|
|
97
|
+
} else if (options.jobs !== false && !options.all) {
|
|
98
|
+
printInfo('No completed one-time jobs to remove');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.logs || options.all) {
|
|
102
|
+
if (response.logsRemoved > 0) {
|
|
103
|
+
printSuccess(`Removed ${response.logsRemoved} log file(s)`);
|
|
104
|
+
hasResults = true;
|
|
105
|
+
} else {
|
|
106
|
+
printInfo('No log files to remove');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.history || options.all) {
|
|
111
|
+
if (response.historyRemoved > 0) {
|
|
112
|
+
printSuccess(`Removed ${response.historyRemoved} history entries(s)`);
|
|
113
|
+
hasResults = true;
|
|
114
|
+
} else {
|
|
115
|
+
printInfo('No history entries to remove');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!hasResults && options.jobs === false) {
|
|
120
|
+
printInfo('Nothing to flush');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
printError('Unexpected response from daemon');
|
|
127
|
+
return 1;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
printError(`Failed to flush: ${error.message}`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 history command
|
|
3
|
+
* Show execution history for jobs
|
|
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
|
+
formatDate,
|
|
14
|
+
formatDuration,
|
|
15
|
+
} from '../utils/output.js';
|
|
16
|
+
import { isDaemonRunning } from '../../daemon/index.js';
|
|
17
|
+
import { getHistory } from '../../core/storage.js';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import Table from 'cli-table3';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute the history command
|
|
23
|
+
* @param {string} jobRef - Job ID or name (optional)
|
|
24
|
+
* @param {object} options - Command options
|
|
25
|
+
* @returns {Promise<number>} Exit code
|
|
26
|
+
*/
|
|
27
|
+
export async function historyCommand(jobRef, options = {}) {
|
|
28
|
+
// Check if daemon is running
|
|
29
|
+
if (!isDaemonRunning()) {
|
|
30
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let history;
|
|
36
|
+
let jobName = null;
|
|
37
|
+
|
|
38
|
+
if (jobRef) {
|
|
39
|
+
// Get specific job's history
|
|
40
|
+
const jobId = parseInt(jobRef, 10);
|
|
41
|
+
const message = isNaN(jobId)
|
|
42
|
+
? { type: MessageType.JOB_GET, jobName: jobRef }
|
|
43
|
+
: { type: MessageType.JOB_GET, jobId };
|
|
44
|
+
|
|
45
|
+
const response = await send(message);
|
|
46
|
+
|
|
47
|
+
if (response.type === MessageType.ERROR) {
|
|
48
|
+
printError(response.message);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (response.type !== MessageType.JOB_GET_RESULT || !response.job) {
|
|
53
|
+
printError(`Job not found: ${jobRef}`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const job = response.job;
|
|
58
|
+
jobName = job.name || String(job.id);
|
|
59
|
+
|
|
60
|
+
// Get history and filter by job ID
|
|
61
|
+
const allHistory = getHistory();
|
|
62
|
+
history = allHistory.filter(entry => entry.jobId === job.id);
|
|
63
|
+
} else {
|
|
64
|
+
// Get all history
|
|
65
|
+
history = getHistory();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Apply filters
|
|
69
|
+
if (options.failed) {
|
|
70
|
+
history = history.filter(entry => entry.exitCode !== 0 || entry.status === 'failed');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.success) {
|
|
74
|
+
history = history.filter(entry => entry.exitCode === 0 || entry.status === 'success');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Sort by timestamp (newest first)
|
|
78
|
+
history.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
79
|
+
|
|
80
|
+
// Apply limit
|
|
81
|
+
const limit = options.limit || 20;
|
|
82
|
+
if (limit > 0) {
|
|
83
|
+
history = history.slice(0, limit);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Display results
|
|
87
|
+
if (history.length === 0) {
|
|
88
|
+
if (jobName) {
|
|
89
|
+
printInfo(`No execution history found for job: ${jobName}`);
|
|
90
|
+
} else {
|
|
91
|
+
printInfo('No execution history found');
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (jobName) {
|
|
97
|
+
printHeader(`Execution History for: ${jobName}`);
|
|
98
|
+
} else {
|
|
99
|
+
printHeader('Execution History');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
printHistoryTable(history, !jobName);
|
|
103
|
+
|
|
104
|
+
// Print summary
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(`Showing ${chalk.bold(history.length)} entries`);
|
|
107
|
+
|
|
108
|
+
return 0;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
printError(`Failed to get history: ${error.message}`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Print history as a formatted table
|
|
117
|
+
* @param {Array} history - Array of history entries
|
|
118
|
+
* @param {boolean} showJobName - Whether to show job name column
|
|
119
|
+
*/
|
|
120
|
+
function printHistoryTable(history, showJobName = false) {
|
|
121
|
+
const headers = [
|
|
122
|
+
chalk.bold('Time'),
|
|
123
|
+
chalk.bold('Status'),
|
|
124
|
+
chalk.bold('Duration'),
|
|
125
|
+
chalk.bold('Exit'),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
if (showJobName) {
|
|
129
|
+
headers.splice(1, 0, chalk.bold('Job'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const colWidths = showJobName
|
|
133
|
+
? [20, 15, 10, 12, 8]
|
|
134
|
+
: [20, 10, 12, 8];
|
|
135
|
+
|
|
136
|
+
const table = new Table({
|
|
137
|
+
head: headers,
|
|
138
|
+
colWidths,
|
|
139
|
+
wordWrap: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
for (const entry of history) {
|
|
143
|
+
const row = [
|
|
144
|
+
formatDate(entry.timestamp),
|
|
145
|
+
formatStatus(entry.status, entry.exitCode),
|
|
146
|
+
entry.duration ? formatDuration(entry.duration) : '-',
|
|
147
|
+
entry.exitCode !== undefined ? entry.exitCode : '-',
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (showJobName) {
|
|
151
|
+
row.splice(1, 0, entry.jobName || `Job ${entry.jobId}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
table.push(row);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(table.toString());
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format status with color
|
|
162
|
+
* @param {string} status - Status string
|
|
163
|
+
* @param {number} exitCode - Exit code
|
|
164
|
+
* @returns {string} Colorized status
|
|
165
|
+
*/
|
|
166
|
+
function formatStatus(status, exitCode) {
|
|
167
|
+
if (status === 'timeout') {
|
|
168
|
+
return chalk.yellow('timeout');
|
|
169
|
+
}
|
|
170
|
+
if (exitCode === 0 || status === 'success') {
|
|
171
|
+
return chalk.green('success');
|
|
172
|
+
}
|
|
173
|
+
if (status === 'failed' || (exitCode !== undefined && exitCode !== 0)) {
|
|
174
|
+
return chalk.red('failed');
|
|
175
|
+
}
|
|
176
|
+
return chalk.gray(status || 'unknown');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default { historyCommand };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 import command
|
|
3
|
+
* Imports job configurations from a JSON file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { getJobs, saveJobs, jobNameExists, getNextJobId } from '../../core/storage.js';
|
|
9
|
+
import { validateJob, createJob } from '../../core/job.js';
|
|
10
|
+
import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
|
|
11
|
+
import { confirmDestructive } from '../utils/prompts.js';
|
|
12
|
+
import { send } from '../../ipc/client.js';
|
|
13
|
+
import { MessageType } from '../../ipc/protocol.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a unique name by appending a number suffix
|
|
17
|
+
* @param {string} baseName - Base name to start with
|
|
18
|
+
* @param {Set<string>} existingNames - Set of existing names
|
|
19
|
+
* @returns {string} Unique name
|
|
20
|
+
*/
|
|
21
|
+
function makeUniqueName(baseName, existingNames) {
|
|
22
|
+
if (!existingNames.has(baseName)) {
|
|
23
|
+
return baseName;
|
|
24
|
+
}
|
|
25
|
+
let suffix = 2;
|
|
26
|
+
let newName;
|
|
27
|
+
do {
|
|
28
|
+
newName = `${baseName}-${suffix}`;
|
|
29
|
+
suffix++;
|
|
30
|
+
} while (existingNames.has(newName));
|
|
31
|
+
return newName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Execute the import command
|
|
36
|
+
* @param {string} file - Path to the import file
|
|
37
|
+
* @param {object} options - Command options
|
|
38
|
+
* @returns {Promise<number>} Exit code
|
|
39
|
+
*/
|
|
40
|
+
export async function importCommand(file, options = {}) {
|
|
41
|
+
try {
|
|
42
|
+
// Resolve file path
|
|
43
|
+
const filePath = resolve(file);
|
|
44
|
+
|
|
45
|
+
// Check if file exists
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
printError(`Import file not found: ${filePath}`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Read and parse the import file
|
|
52
|
+
let importData;
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(filePath, 'utf8');
|
|
55
|
+
importData = JSON.parse(content);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
printError(`Failed to parse import file: ${error.message}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate import data structure
|
|
62
|
+
if (!importData.jobs || !Array.isArray(importData.jobs)) {
|
|
63
|
+
printError('Invalid import file format: missing or invalid "jobs" array');
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (importData.jobs.length === 0) {
|
|
68
|
+
printInfo('No jobs found in import file');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get existing jobs
|
|
73
|
+
const existingJobs = getJobs();
|
|
74
|
+
const existingNames = new Set(existingJobs.map(j => j.name).filter(Boolean));
|
|
75
|
+
|
|
76
|
+
// Get starting ID for new jobs (imported jobs get new IDs)
|
|
77
|
+
let nextId = getNextJobId();
|
|
78
|
+
|
|
79
|
+
// Prepare jobs for import - track original name to final name mapping
|
|
80
|
+
const jobsToImport = [];
|
|
81
|
+
const nameMapping = []; // { original, final, renamed }
|
|
82
|
+
const skippedJobs = [];
|
|
83
|
+
const invalidJobs = [];
|
|
84
|
+
|
|
85
|
+
for (const jobData of importData.jobs) {
|
|
86
|
+
// Validate job structure
|
|
87
|
+
const validation = validateJob(jobData);
|
|
88
|
+
if (!validation.valid) {
|
|
89
|
+
invalidJobs.push({ name: jobData.name || 'unnamed', errors: validation.errors });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Determine the final name
|
|
94
|
+
const originalName = jobData.name || 'unnamed';
|
|
95
|
+
let finalName = jobData.name;
|
|
96
|
+
|
|
97
|
+
if (existingNames.has(finalName)) {
|
|
98
|
+
if (options.skip) {
|
|
99
|
+
skippedJobs.push(finalName);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Generate a unique name
|
|
103
|
+
finalName = makeUniqueName(finalName, existingNames);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create the job object with a new ID
|
|
107
|
+
const job = createJob({
|
|
108
|
+
...jobData,
|
|
109
|
+
id: nextId++, // Assign a new ID
|
|
110
|
+
name: finalName,
|
|
111
|
+
// Reset runtime state
|
|
112
|
+
status: jobData.status === 'paused' ? 'paused' : 'active',
|
|
113
|
+
runCount: 0,
|
|
114
|
+
lastRun: null,
|
|
115
|
+
lastResult: null,
|
|
116
|
+
nextRun: null,
|
|
117
|
+
retryCount: 0,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
jobsToImport.push(job);
|
|
121
|
+
existingNames.add(finalName);
|
|
122
|
+
nameMapping.push({
|
|
123
|
+
original: originalName,
|
|
124
|
+
final: finalName,
|
|
125
|
+
renamed: originalName !== finalName
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Report issues
|
|
130
|
+
if (invalidJobs.length > 0) {
|
|
131
|
+
printWarning(`Skipping ${invalidJobs.length} invalid job(s):`);
|
|
132
|
+
for (const { name, errors } of invalidJobs) {
|
|
133
|
+
printWarning(` - ${name}: ${errors.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (skippedJobs.length > 0) {
|
|
138
|
+
printWarning(`Skipping ${skippedJobs.length} existing job(s): ${skippedJobs.join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (jobsToImport.length === 0) {
|
|
142
|
+
printInfo('No jobs to import');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Confirm import unless --force is used
|
|
147
|
+
const action = `import ${jobsToImport.length} job(s)`;
|
|
148
|
+
const confirmed = await confirmDestructive(action, options.force);
|
|
149
|
+
if (!confirmed) {
|
|
150
|
+
printInfo('Import cancelled');
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Import the jobs
|
|
155
|
+
const allJobs = [...existingJobs, ...jobsToImport];
|
|
156
|
+
saveJobs(allJobs);
|
|
157
|
+
|
|
158
|
+
// Notify daemon to reload jobs
|
|
159
|
+
try {
|
|
160
|
+
await send({ type: MessageType.RELOAD_JOBS });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
printWarning(`Failed to notify daemon to reload jobs: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Report results
|
|
166
|
+
printSuccess(`Successfully imported ${jobsToImport.length} job(s)`);
|
|
167
|
+
for (const mapping of nameMapping) {
|
|
168
|
+
if (mapping.renamed) {
|
|
169
|
+
printInfo(` - ${mapping.original} → ${mapping.final} (renamed)`);
|
|
170
|
+
} else {
|
|
171
|
+
printInfo(` - ${mapping.final}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return 0;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
printError(`Failed to import jobs: ${error.message}`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 list command
|
|
3
|
+
* Lists all jobs with optional filtering
|
|
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
|
+
createJobTable,
|
|
14
|
+
colorizeStatus,
|
|
15
|
+
formatDate,
|
|
16
|
+
formatRelativeTime,
|
|
17
|
+
} from '../utils/output.js';
|
|
18
|
+
import { isDaemonRunning } from '../../daemon/index.js';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute the list command
|
|
23
|
+
* @param {object} options - Command options
|
|
24
|
+
* @returns {Promise<number>} Exit code
|
|
25
|
+
*/
|
|
26
|
+
export async function listCommand(options = {}) {
|
|
27
|
+
// Check if daemon is running
|
|
28
|
+
if (!isDaemonRunning()) {
|
|
29
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Build filters
|
|
35
|
+
const filters = {};
|
|
36
|
+
|
|
37
|
+
if (options.tag) {
|
|
38
|
+
filters.tag = options.tag;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.status) {
|
|
42
|
+
filters.status = options.status;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.type) {
|
|
46
|
+
filters.type = options.type;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Request jobs from daemon
|
|
50
|
+
const response = await send({
|
|
51
|
+
type: MessageType.JOB_LIST,
|
|
52
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (response.type === MessageType.ERROR) {
|
|
56
|
+
printError(response.message);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (response.type === MessageType.JOB_LIST_RESULT) {
|
|
61
|
+
const jobs = response.jobs || [];
|
|
62
|
+
|
|
63
|
+
if (jobs.length === 0) {
|
|
64
|
+
printInfo('No jobs found');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Print header
|
|
69
|
+
const filterDesc = [];
|
|
70
|
+
if (options.tag) filterDesc.push(`tag: ${options.tag}`);
|
|
71
|
+
if (options.status) filterDesc.push(`status: ${options.status}`);
|
|
72
|
+
if (options.type) filterDesc.push(`type: ${options.type}`);
|
|
73
|
+
|
|
74
|
+
if (filterDesc.length > 0) {
|
|
75
|
+
printHeader(`Jobs (${filterDesc.join(', ')})`);
|
|
76
|
+
} else {
|
|
77
|
+
printHeader('Jobs');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (options.verbose) {
|
|
81
|
+
// Verbose output - detailed list
|
|
82
|
+
for (const job of jobs) {
|
|
83
|
+
printJobDetails(job);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// Table output
|
|
87
|
+
const table = createJobTable();
|
|
88
|
+
|
|
89
|
+
for (const job of jobs) {
|
|
90
|
+
const schedule = job.cron
|
|
91
|
+
? chalk.gray(job.cron)
|
|
92
|
+
: job.runAt
|
|
93
|
+
? formatRelativeTime(job.runAt)
|
|
94
|
+
: chalk.gray('Manual');
|
|
95
|
+
|
|
96
|
+
table.push([
|
|
97
|
+
job.id,
|
|
98
|
+
job.name || chalk.gray('-'),
|
|
99
|
+
colorizeStatus(job.status),
|
|
100
|
+
schedule,
|
|
101
|
+
formatRelativeTime(job.nextRun),
|
|
102
|
+
formatRelativeTime(job.lastRun),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(table.toString());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log();
|
|
110
|
+
printInfo(`${jobs.length} job${jobs.length === 1 ? '' : 's'} found`);
|
|
111
|
+
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printError('Unexpected response from daemon');
|
|
116
|
+
return 1;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
printError(`Failed to list jobs: ${error.message}`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Print detailed job information
|
|
125
|
+
* @param {object} job - Job object
|
|
126
|
+
*/
|
|
127
|
+
function printJobDetails(job) {
|
|
128
|
+
console.log(chalk.bold(`\nJob: ${job.name || job.id}`));
|
|
129
|
+
console.log(` ID: ${job.id}`);
|
|
130
|
+
console.log(` Status: ${colorizeStatus(job.status)}`);
|
|
131
|
+
console.log(` Command: ${chalk.gray(job.command)}`);
|
|
132
|
+
|
|
133
|
+
if (job.cron) {
|
|
134
|
+
console.log(` Schedule: ${chalk.gray(job.cron)} (cron)`);
|
|
135
|
+
} else if (job.runAt) {
|
|
136
|
+
console.log(` Schedule: ${formatDate(job.runAt)} (one-time)`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (job.nextRun) {
|
|
140
|
+
console.log(` Next Run: ${formatRelativeTime(job.nextRun)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (job.lastRun) {
|
|
144
|
+
console.log(` Last Run: ${formatRelativeTime(job.lastRun)}`);
|
|
145
|
+
if (job.lastResult) {
|
|
146
|
+
const resultColor = job.lastResult === 'success' ? chalk.green : chalk.red;
|
|
147
|
+
console.log(` Last Result: ${resultColor(job.lastResult)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (job.tags && job.tags.length > 0) {
|
|
152
|
+
console.log(` Tags: ${job.tags.map(t => chalk.cyan(t)).join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (job.cwd) {
|
|
156
|
+
console.log(` Working Dir: ${chalk.gray(job.cwd)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (job.timeout) {
|
|
160
|
+
console.log(` Timeout: ${chalk.gray(job.timeout)}ms`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (job.retry > 0) {
|
|
164
|
+
console.log(` Retry: ${chalk.gray(job.retry)} attempts`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (job.runCount > 0) {
|
|
168
|
+
console.log(` Run Count: ${chalk.gray(job.runCount)}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(` Created: ${formatDate(job.createdAt)}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default listCommand;
|