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,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 logs command
|
|
3
|
+
* View job execution logs with tail, follow, and time filtering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createReadStream, existsSync, statSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { watch } from 'node:fs';
|
|
10
|
+
import { send } from '../../ipc/client.js';
|
|
11
|
+
import { MessageType } from '../../ipc/protocol.js';
|
|
12
|
+
import {
|
|
13
|
+
printSuccess,
|
|
14
|
+
printError,
|
|
15
|
+
printInfo,
|
|
16
|
+
printWarning,
|
|
17
|
+
} from '../utils/output.js';
|
|
18
|
+
import { isDaemonRunning } from '../../daemon/index.js';
|
|
19
|
+
import { getJobLogFile } from '../../utils/paths.js';
|
|
20
|
+
import { parseDuration } from '../../utils/duration.js';
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute the logs command
|
|
25
|
+
* @param {string} jobRef - Job ID or name
|
|
26
|
+
* @param {object} options - Command options
|
|
27
|
+
* @returns {Promise<number>} Exit code
|
|
28
|
+
*/
|
|
29
|
+
export async function logsCommand(jobRef, options = {}) {
|
|
30
|
+
// Check if daemon is running
|
|
31
|
+
if (!isDaemonRunning()) {
|
|
32
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!jobRef || jobRef.trim() === '') {
|
|
37
|
+
printError('Job ID or name is required');
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Get job details to find the job name (for log file path)
|
|
43
|
+
const jobId = parseInt(jobRef, 10);
|
|
44
|
+
const message = isNaN(jobId)
|
|
45
|
+
? { type: MessageType.JOB_GET, jobName: jobRef }
|
|
46
|
+
: { type: MessageType.JOB_GET, jobId };
|
|
47
|
+
|
|
48
|
+
const response = await send(message);
|
|
49
|
+
|
|
50
|
+
if (response.type === MessageType.ERROR) {
|
|
51
|
+
printError(response.message);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (response.type !== MessageType.JOB_GET_RESULT || !response.job) {
|
|
56
|
+
printError(`Job not found: ${jobRef}`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const job = response.job;
|
|
61
|
+
const logFile = getJobLogFile(job.name || `job-${job.id}`);
|
|
62
|
+
|
|
63
|
+
// Parse time filters
|
|
64
|
+
const sinceDate = options.since ? parseTimeOption(options.since) : null;
|
|
65
|
+
const untilDate = options.until ? parseTimeOption(options.until) : null;
|
|
66
|
+
|
|
67
|
+
// Handle follow mode
|
|
68
|
+
if (options.follow) {
|
|
69
|
+
// Check if log file exists, if not, wait for it to be created
|
|
70
|
+
if (!existsSync(logFile)) {
|
|
71
|
+
printInfo(`No log file found for job: ${job.name || job.id}`);
|
|
72
|
+
printInfo(`Log file would be at: ${logFile}`);
|
|
73
|
+
printInfo('Waiting for log file to be created...');
|
|
74
|
+
console.log();
|
|
75
|
+
await waitForLogFile(logFile);
|
|
76
|
+
}
|
|
77
|
+
await followLogFile(logFile, {
|
|
78
|
+
since: sinceDate,
|
|
79
|
+
until: untilDate,
|
|
80
|
+
timestamps: options.timestamps,
|
|
81
|
+
});
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if log file exists (non-follow mode)
|
|
86
|
+
if (!existsSync(logFile)) {
|
|
87
|
+
printInfo(`No log file found for job: ${job.name || job.id}`);
|
|
88
|
+
printInfo(`Log file would be at: ${logFile}`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle regular log viewing (tail)
|
|
93
|
+
await showLogFile(logFile, {
|
|
94
|
+
lines: options.lines,
|
|
95
|
+
since: sinceDate,
|
|
96
|
+
until: untilDate,
|
|
97
|
+
timestamps: options.timestamps,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
printError(`Failed to get logs: ${error.message}`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse time option (relative like "1h" or absolute date)
|
|
109
|
+
* @param {string} value - Time option value
|
|
110
|
+
* @returns {Date} Parsed date
|
|
111
|
+
*/
|
|
112
|
+
function parseTimeOption(value) {
|
|
113
|
+
if (!value) return null;
|
|
114
|
+
|
|
115
|
+
// Check if it's a relative time (e.g., "1h", "30m", "2d")
|
|
116
|
+
const relativeMatch = value.match(/^(\d+)([smhd])$/i);
|
|
117
|
+
if (relativeMatch) {
|
|
118
|
+
const amount = parseInt(relativeMatch[1], 10);
|
|
119
|
+
const unit = relativeMatch[2].toLowerCase();
|
|
120
|
+
const multipliers = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
121
|
+
const ms = amount * multipliers[unit];
|
|
122
|
+
return new Date(Date.now() - ms);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Try parsing as absolute date
|
|
126
|
+
const date = new Date(value);
|
|
127
|
+
if (!isNaN(date.getTime())) {
|
|
128
|
+
return date;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Invalid time format: "${value}". Use relative (e.g., "1h", "30m") or absolute date.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Show log file content with tail and time filtering
|
|
136
|
+
* @param {string} logFile - Path to log file
|
|
137
|
+
* @param {object} options - Options
|
|
138
|
+
*/
|
|
139
|
+
async function showLogFile(logFile, options) {
|
|
140
|
+
const { lines = 50, since, until, timestamps = true } = options;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Read file stats
|
|
144
|
+
const stats = statSync(logFile);
|
|
145
|
+
|
|
146
|
+
if (stats.size === 0) {
|
|
147
|
+
printInfo('Log file is empty');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If we need to filter by time or show all lines, read entire file
|
|
152
|
+
// Otherwise use efficient tail
|
|
153
|
+
let logLines;
|
|
154
|
+
if (since || until) {
|
|
155
|
+
// Read entire file for time filtering
|
|
156
|
+
const content = await readFile(logFile, 'utf8');
|
|
157
|
+
logLines = content.split('\n').filter(line => line.trim() !== '');
|
|
158
|
+
} else {
|
|
159
|
+
// Use efficient tail
|
|
160
|
+
logLines = await tailFile(logFile, lines);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Filter by time if specified
|
|
164
|
+
if (since || until) {
|
|
165
|
+
logLines = filterLinesByTime(logLines, since, until);
|
|
166
|
+
// Apply line limit after filtering
|
|
167
|
+
if (lines && logLines.length > lines) {
|
|
168
|
+
logLines = logLines.slice(-lines);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Print the lines
|
|
173
|
+
if (logLines.length === 0) {
|
|
174
|
+
printInfo('No log entries match the specified criteria');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const line of logLines) {
|
|
179
|
+
printLogLine(line, timestamps);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
printError(`Error reading log file: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Efficiently read the last N lines from a file
|
|
188
|
+
* @param {string} filePath - Path to file
|
|
189
|
+
* @param {number} lineCount - Number of lines to read
|
|
190
|
+
* @returns {Promise<string[]>} Array of lines
|
|
191
|
+
*/
|
|
192
|
+
async function tailFile(filePath, lineCount) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
const fileStream = createReadStream(filePath);
|
|
195
|
+
const rl = createInterface({
|
|
196
|
+
input: fileStream,
|
|
197
|
+
crlfDelay: Infinity,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
for await (const line of rl) {
|
|
201
|
+
lines.push(line);
|
|
202
|
+
if (lines.length > lineCount) {
|
|
203
|
+
lines.shift();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Filter log lines by time range
|
|
212
|
+
* @param {string[]} lines - Log lines
|
|
213
|
+
* @param {Date} since - Start date
|
|
214
|
+
* @param {Date} until - End date
|
|
215
|
+
* @returns {string[]} Filtered lines
|
|
216
|
+
*/
|
|
217
|
+
function filterLinesByTime(lines, since, until) {
|
|
218
|
+
return lines.filter(line => {
|
|
219
|
+
const timestamp = extractTimestamp(line);
|
|
220
|
+
if (!timestamp) return true; // Include lines without timestamps
|
|
221
|
+
|
|
222
|
+
if (since && timestamp < since) return false;
|
|
223
|
+
if (until && timestamp > until) return false;
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extract timestamp from log line
|
|
230
|
+
* Assumes ISO 8601 format at start of line
|
|
231
|
+
* @param {string} line - Log line
|
|
232
|
+
* @returns {Date|null} Extracted date or null
|
|
233
|
+
*/
|
|
234
|
+
function extractTimestamp(line) {
|
|
235
|
+
// Match ISO 8601 timestamp at start of line (e.g., 2026-01-31T10:00:00.000Z)
|
|
236
|
+
const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)/);
|
|
237
|
+
if (match) {
|
|
238
|
+
const date = new Date(match[1]);
|
|
239
|
+
if (!isNaN(date.getTime())) {
|
|
240
|
+
return date;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Print a log line with optional timestamp formatting
|
|
248
|
+
* @param {string} line - Log line
|
|
249
|
+
* @param {boolean} showTimestamps - Whether to show timestamps
|
|
250
|
+
*/
|
|
251
|
+
function printLogLine(line, showTimestamps = true) {
|
|
252
|
+
if (!showTimestamps) {
|
|
253
|
+
// Remove timestamp prefix if present
|
|
254
|
+
const cleaned = line.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z\s*/, '');
|
|
255
|
+
console.log(cleaned);
|
|
256
|
+
} else {
|
|
257
|
+
// Highlight timestamp if present
|
|
258
|
+
const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z)(\s*)(.*)/);
|
|
259
|
+
if (match) {
|
|
260
|
+
const [, timestamp, space, rest] = match;
|
|
261
|
+
console.log(`${chalk.gray(timestamp)}${space}${rest}`);
|
|
262
|
+
} else {
|
|
263
|
+
console.log(line);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Wait for a log file to be created
|
|
270
|
+
* @param {string} logFile - Path to log file
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async function waitForLogFile(logFile) {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
// Check if file already exists
|
|
276
|
+
if (existsSync(logFile)) {
|
|
277
|
+
resolve();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Watch the directory for the file to be created
|
|
282
|
+
const dir = logFile.substring(0, logFile.lastIndexOf('/'));
|
|
283
|
+
|
|
284
|
+
// If directory doesn't exist, wait a bit and retry
|
|
285
|
+
if (!existsSync(dir)) {
|
|
286
|
+
// Poll every 500ms for up to 30 seconds
|
|
287
|
+
let attempts = 0;
|
|
288
|
+
const maxAttempts = 60;
|
|
289
|
+
const interval = setInterval(() => {
|
|
290
|
+
attempts++;
|
|
291
|
+
if (existsSync(logFile)) {
|
|
292
|
+
clearInterval(interval);
|
|
293
|
+
resolve();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (attempts >= maxAttempts) {
|
|
297
|
+
clearInterval(interval);
|
|
298
|
+
reject(new Error('Timeout waiting for log file to be created'));
|
|
299
|
+
}
|
|
300
|
+
}, 500);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Use fs.watch to monitor the directory
|
|
305
|
+
const watcher = watch(dir, (eventType, filename) => {
|
|
306
|
+
if (eventType === 'rename' && existsSync(logFile)) {
|
|
307
|
+
watcher.close();
|
|
308
|
+
resolve();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Set a timeout (30 seconds)
|
|
313
|
+
const timeout = setTimeout(() => {
|
|
314
|
+
watcher.close();
|
|
315
|
+
reject(new Error('Timeout waiting for log file to be created'));
|
|
316
|
+
}, 30000);
|
|
317
|
+
|
|
318
|
+
// Clean up timeout when resolved
|
|
319
|
+
watcher.on('close', () => {
|
|
320
|
+
clearTimeout(timeout);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Follow log file in real-time
|
|
327
|
+
* @param {string} logFile - Path to log file
|
|
328
|
+
* @param {object} options - Options
|
|
329
|
+
*/
|
|
330
|
+
async function followLogFile(logFile, options) {
|
|
331
|
+
const { since, until, timestamps = true } = options;
|
|
332
|
+
|
|
333
|
+
printInfo(`Following log file: ${logFile}`);
|
|
334
|
+
printInfo('Press Ctrl+C to stop');
|
|
335
|
+
console.log();
|
|
336
|
+
|
|
337
|
+
// Show existing content first
|
|
338
|
+
await showLogFile(logFile, { lines: 50, since, until, timestamps });
|
|
339
|
+
|
|
340
|
+
// Set up file watcher
|
|
341
|
+
let lastSize = statSync(logFile).size;
|
|
342
|
+
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
const watcher = watch(logFile, async (eventType) => {
|
|
345
|
+
if (eventType === 'change') {
|
|
346
|
+
try {
|
|
347
|
+
const stats = statSync(logFile);
|
|
348
|
+
if (stats.size > lastSize) {
|
|
349
|
+
// Read only new content using createReadStream with start option
|
|
350
|
+
const newLines = await readNewContent(logFile, lastSize, stats.size);
|
|
351
|
+
|
|
352
|
+
for (const line of newLines) {
|
|
353
|
+
// Check until filter
|
|
354
|
+
if (until) {
|
|
355
|
+
const timestamp = extractTimestamp(line);
|
|
356
|
+
if (timestamp && timestamp > until) {
|
|
357
|
+
watcher.close();
|
|
358
|
+
resolve();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
printLogLine(line, timestamps);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
lastSize = stats.size;
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
printWarning(`Error reading log file: ${error.message}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Handle Ctrl+C gracefully
|
|
374
|
+
process.on('SIGINT', () => {
|
|
375
|
+
watcher.close();
|
|
376
|
+
console.log();
|
|
377
|
+
printInfo('Stopped following logs');
|
|
378
|
+
resolve();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Read new content from a file starting at a specific position
|
|
385
|
+
* @param {string} filePath - Path to file
|
|
386
|
+
* @param {number} start - Starting byte position
|
|
387
|
+
* @param {number} end - Ending byte position (file size)
|
|
388
|
+
* @returns {Promise<string[]>} Array of new lines
|
|
389
|
+
*/
|
|
390
|
+
async function readNewContent(filePath, start, end) {
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
const lines = [];
|
|
393
|
+
const stream = createReadStream(filePath, { start, end: end - 1 });
|
|
394
|
+
const rl = createInterface({
|
|
395
|
+
input: stream,
|
|
396
|
+
crlfDelay: Infinity,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
rl.on('line', (line) => {
|
|
400
|
+
if (line.trim() !== '') {
|
|
401
|
+
lines.push(line);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
rl.on('close', () => {
|
|
406
|
+
resolve(lines);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
rl.on('error', (error) => {
|
|
410
|
+
reject(error);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export default { logsCommand };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 pause command
|
|
3
|
+
* Pauses one or more jobs (prevents them from running)
|
|
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
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute the pause command
|
|
14
|
+
* @param {string|string[]} jobRefs - Job ID(s) or name(s)
|
|
15
|
+
* @param {object} options - Command options
|
|
16
|
+
* @returns {Promise<number>} Exit code
|
|
17
|
+
*/
|
|
18
|
+
export async function pauseCommand(jobRefs, 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
|
+
// Normalize jobRefs to array
|
|
26
|
+
const refs = Array.isArray(jobRefs) ? jobRefs : [jobRefs];
|
|
27
|
+
|
|
28
|
+
if (refs.length === 0 || (refs.length === 1 && !refs[0])) {
|
|
29
|
+
printError('Job ID or name is required');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let successCount = 0;
|
|
34
|
+
let failCount = 0;
|
|
35
|
+
|
|
36
|
+
for (const jobRef of refs) {
|
|
37
|
+
const result = await pauseSingleJob(jobRef);
|
|
38
|
+
if (result) {
|
|
39
|
+
successCount++;
|
|
40
|
+
} else {
|
|
41
|
+
failCount++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Summary
|
|
46
|
+
if (successCount > 0) {
|
|
47
|
+
printSuccess(`Paused ${successCount} job(s)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (failCount > 0) {
|
|
51
|
+
printError(`Failed to pause ${failCount} job(s)`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pause a single job
|
|
60
|
+
* @param {string} jobRef - Job ID or name
|
|
61
|
+
* @returns {Promise<boolean>} True if successful
|
|
62
|
+
*/
|
|
63
|
+
async function pauseSingleJob(jobRef) {
|
|
64
|
+
try {
|
|
65
|
+
// Determine if jobRef is an ID (numeric) or name
|
|
66
|
+
const jobId = parseInt(jobRef, 10);
|
|
67
|
+
const message = isNaN(jobId)
|
|
68
|
+
? { type: MessageType.JOB_PAUSE, jobName: jobRef }
|
|
69
|
+
: { type: MessageType.JOB_PAUSE, jobId };
|
|
70
|
+
|
|
71
|
+
const response = await send(message);
|
|
72
|
+
|
|
73
|
+
if (response.type === MessageType.ERROR) {
|
|
74
|
+
printError(`${jobRef}: ${response.message}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (response.type === MessageType.JOB_PAUSED) {
|
|
79
|
+
if (response.job) {
|
|
80
|
+
const name = response.job.name || response.job.id;
|
|
81
|
+
printSuccess(`Paused: ${name}`);
|
|
82
|
+
return true;
|
|
83
|
+
} else {
|
|
84
|
+
printWarning(`Job not found: ${jobRef}`);
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
printError(`${jobRef}: Unexpected response from daemon`);
|
|
90
|
+
return false;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
printError(`${jobRef}: ${error.message}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default { pauseCommand };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 remove command
|
|
3
|
+
* Removes one or more jobs from the scheduler
|
|
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
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute the remove command
|
|
14
|
+
* @param {string|string[]} jobRefs - Job ID(s) or name(s)
|
|
15
|
+
* @param {object} options - Command options
|
|
16
|
+
* @returns {Promise<number>} Exit code
|
|
17
|
+
*/
|
|
18
|
+
export async function removeCommand(jobRefs, 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
|
+
// Normalize jobRefs to array
|
|
26
|
+
const refs = Array.isArray(jobRefs) ? jobRefs : [jobRefs];
|
|
27
|
+
|
|
28
|
+
if (refs.length === 0 || (refs.length === 1 && !refs[0])) {
|
|
29
|
+
printError('Job ID or name is required');
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Confirm destructive action unless --force is used
|
|
34
|
+
const action = refs.length === 1
|
|
35
|
+
? `remove job "${refs[0]}"`
|
|
36
|
+
: `remove ${refs.length} jobs`;
|
|
37
|
+
|
|
38
|
+
const confirmed = await confirmDestructive(action, options.force);
|
|
39
|
+
if (!confirmed) {
|
|
40
|
+
printInfo('Operation cancelled');
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let successCount = 0;
|
|
45
|
+
let failCount = 0;
|
|
46
|
+
|
|
47
|
+
for (const jobRef of refs) {
|
|
48
|
+
const result = await removeSingleJob(jobRef);
|
|
49
|
+
if (result) {
|
|
50
|
+
successCount++;
|
|
51
|
+
} else {
|
|
52
|
+
failCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Summary
|
|
57
|
+
if (successCount > 0) {
|
|
58
|
+
printSuccess(`Removed ${successCount} job(s)`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (failCount > 0) {
|
|
62
|
+
printError(`Failed to remove ${failCount} job(s)`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Remove a single job
|
|
71
|
+
* @param {string} jobRef - Job ID or name
|
|
72
|
+
* @returns {Promise<boolean>} True if successful
|
|
73
|
+
*/
|
|
74
|
+
async function removeSingleJob(jobRef) {
|
|
75
|
+
try {
|
|
76
|
+
// Determine if jobRef is an ID (numeric) or name
|
|
77
|
+
const jobId = parseInt(jobRef, 10);
|
|
78
|
+
const message = isNaN(jobId)
|
|
79
|
+
? { type: MessageType.JOB_REMOVE, jobName: jobRef }
|
|
80
|
+
: { type: MessageType.JOB_REMOVE, jobId };
|
|
81
|
+
|
|
82
|
+
const response = await send(message);
|
|
83
|
+
|
|
84
|
+
if (response.type === MessageType.ERROR) {
|
|
85
|
+
printError(`${jobRef}: ${response.message}`);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (response.type === MessageType.JOB_REMOVED) {
|
|
90
|
+
if (response.success) {
|
|
91
|
+
printSuccess(`Removed: ${jobRef}`);
|
|
92
|
+
return true;
|
|
93
|
+
} else {
|
|
94
|
+
printWarning(`Job not found: ${jobRef}`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
printError(`${jobRef}: Unexpected response from daemon`);
|
|
100
|
+
return false;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
printError(`${jobRef}: ${error.message}`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default { removeCommand };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 restart command
|
|
3
|
+
* Restarts the JM2 daemon process
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stopDaemon, isDaemonRunning, getDaemonStatus } from '../../daemon/index.js';
|
|
7
|
+
import { startDaemon } from '../../daemon/index.js';
|
|
8
|
+
import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute the restart command
|
|
12
|
+
* @param {object} options - Command options
|
|
13
|
+
* @returns {Promise<number>} Exit code
|
|
14
|
+
*/
|
|
15
|
+
export async function restartCommand(options = {}) {
|
|
16
|
+
const wasRunning = isDaemonRunning();
|
|
17
|
+
const oldPid = wasRunning ? getDaemonStatus().pid : null;
|
|
18
|
+
|
|
19
|
+
// Stop if running
|
|
20
|
+
if (wasRunning) {
|
|
21
|
+
printInfo(`Stopping daemon (PID: ${oldPid})...`);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const stopped = stopDaemon();
|
|
25
|
+
if (!stopped) {
|
|
26
|
+
printError('Failed to stop daemon');
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wait for daemon to stop
|
|
31
|
+
let attempts = 0;
|
|
32
|
+
const maxAttempts = 10;
|
|
33
|
+
while (isDaemonRunning() && attempts < maxAttempts) {
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
35
|
+
attempts++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isDaemonRunning()) {
|
|
39
|
+
printWarning('Daemon did not stop gracefully, forcing...');
|
|
40
|
+
stopDaemon(9); // SIGKILL
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
printSuccess('Daemon stopped');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
printError(`Failed to stop daemon: ${error.message}`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
printInfo('Daemon was not running');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Start daemon
|
|
54
|
+
printInfo('Starting daemon...');
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await startDaemon({ foreground: false });
|
|
58
|
+
|
|
59
|
+
const { pid } = getDaemonStatus();
|
|
60
|
+
printSuccess(`Daemon restarted (PID: ${pid})`);
|
|
61
|
+
return 0;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
printError(`Failed to start daemon: ${error.message}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default restartCommand;
|