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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 add command
|
|
3
|
+
* Adds a new job to the scheduler
|
|
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
|
+
import { parseDateTime, parseRunIn } from '../../utils/datetime.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Print common examples of JM2 add command
|
|
14
|
+
*/
|
|
15
|
+
function printExamples() {
|
|
16
|
+
console.log(`
|
|
17
|
+
Common examples of JM2 add:
|
|
18
|
+
|
|
19
|
+
# Run a command once at a specific time
|
|
20
|
+
jm2 add "backup.sh" --at "today 14:30"
|
|
21
|
+
jm2 add "backup.sh" --at "tomorrow 09:00"
|
|
22
|
+
jm2 add "backup.sh" --at "2025-12-25 08:00"
|
|
23
|
+
|
|
24
|
+
# Run a command after a delay
|
|
25
|
+
jm2 add "cleanup.sh" --delay "30m"
|
|
26
|
+
jm2 add "cleanup.sh" --delay "2h"
|
|
27
|
+
jm2 add "cleanup.sh" --delay "1d"
|
|
28
|
+
|
|
29
|
+
# Run a command on a schedule (cron)
|
|
30
|
+
jm2 add "daily-report.sh" --cron "0 9 * * *"
|
|
31
|
+
jm2 add "weekly-backup.sh" --cron "0 0 * * 0"
|
|
32
|
+
jm2 add "monthly-task.sh" --cron "0 0 1 * *"
|
|
33
|
+
|
|
34
|
+
# Add a job with a name
|
|
35
|
+
jm2 add "backup.sh" --name "daily-backup" --cron "0 2 * * *"
|
|
36
|
+
|
|
37
|
+
# Add a job with tags
|
|
38
|
+
jm2 add "deploy.sh" --tag "production" --tag "deployment" --delay "5m"
|
|
39
|
+
|
|
40
|
+
# Add a job with environment variables
|
|
41
|
+
jm2 add "script.sh" --env "NODE_ENV=production" --env "DEBUG=true" --cron "0 */6 * * *"
|
|
42
|
+
|
|
43
|
+
# Add a job with a working directory
|
|
44
|
+
jm2 add "npm run build" --cwd /path/to/project --at "today 15:00"
|
|
45
|
+
|
|
46
|
+
# Add a job with timeout and retry
|
|
47
|
+
jm2 add "long-running.sh" --timeout "2h" --retry 3 --cron "0 3 * * *"
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute the add command
|
|
53
|
+
* @param {string} command - Command to execute
|
|
54
|
+
* @param {object} options - Command options
|
|
55
|
+
* @returns {Promise<number>} Exit code
|
|
56
|
+
*/
|
|
57
|
+
export async function addCommand(command, options = {}) {
|
|
58
|
+
// Handle --examples flag
|
|
59
|
+
if (options.examples) {
|
|
60
|
+
printExamples();
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if daemon is running
|
|
65
|
+
if (!isDaemonRunning()) {
|
|
66
|
+
printError('Daemon is not running. Start it with: jm2 start');
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!command || command.trim() === '') {
|
|
71
|
+
printError('Command is required');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Build job data
|
|
77
|
+
const jobData = {
|
|
78
|
+
command: command.trim(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Handle scheduling options
|
|
82
|
+
const { cron, at, delay } = options;
|
|
83
|
+
|
|
84
|
+
if (cron && at) {
|
|
85
|
+
printError('Cannot specify both --cron and --at');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (cron && delay) {
|
|
90
|
+
printError('Cannot specify both --cron and --delay');
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (at && delay) {
|
|
95
|
+
printError('Cannot specify both --at and --delay');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parse scheduling
|
|
100
|
+
if (cron) {
|
|
101
|
+
jobData.cron = cron;
|
|
102
|
+
} else if (at) {
|
|
103
|
+
try {
|
|
104
|
+
const date = parseDateTime(at);
|
|
105
|
+
jobData.runAt = date.toISOString();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
printError(`Invalid datetime: ${error.message}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
} else if (delay) {
|
|
111
|
+
try {
|
|
112
|
+
const date = parseRunIn(delay);
|
|
113
|
+
jobData.runAt = date.toISOString();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
printError(`Invalid duration: ${error.message}`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
printError('Scheduling option required: --cron, --at, or --delay');
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Optional fields
|
|
124
|
+
if (options.name) {
|
|
125
|
+
// Check if name is a pure number (would conflict with job ID access)
|
|
126
|
+
if (/^\d+$/.test(options.name)) {
|
|
127
|
+
printError('Job name cannot be a pure number (conflicts with job ID)');
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
jobData.name = options.name;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (options.tag) {
|
|
134
|
+
// Handle multiple tags
|
|
135
|
+
const tags = Array.isArray(options.tag)
|
|
136
|
+
? options.tag
|
|
137
|
+
: [options.tag];
|
|
138
|
+
jobData.tags = tags;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.cwd) {
|
|
142
|
+
jobData.cwd = options.cwd;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.env) {
|
|
146
|
+
// Parse env options (format: KEY=value)
|
|
147
|
+
const envVars = Array.isArray(options.env)
|
|
148
|
+
? options.env
|
|
149
|
+
: [options.env];
|
|
150
|
+
jobData.env = {};
|
|
151
|
+
for (const envVar of envVars) {
|
|
152
|
+
const [key, ...valueParts] = envVar.split('=');
|
|
153
|
+
if (key && valueParts.length > 0) {
|
|
154
|
+
jobData.env[key] = valueParts.join('=');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (options.timeout) {
|
|
160
|
+
jobData.timeout = options.timeout;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.retry !== undefined) {
|
|
164
|
+
jobData.retry = parseInt(options.retry, 10);
|
|
165
|
+
if (isNaN(jobData.retry) || jobData.retry < 0) {
|
|
166
|
+
printError('Retry must be a non-negative integer');
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Send to daemon
|
|
172
|
+
printInfo('Adding job...');
|
|
173
|
+
|
|
174
|
+
const response = await send({
|
|
175
|
+
type: MessageType.JOB_ADD,
|
|
176
|
+
jobData,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (response.type === MessageType.ERROR) {
|
|
180
|
+
printError(response.message);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (response.type === MessageType.JOB_ADDED && response.job) {
|
|
185
|
+
const job = response.job;
|
|
186
|
+
printSuccess(`Job added: ${job.name || job.id}`);
|
|
187
|
+
|
|
188
|
+
if (job.cron) {
|
|
189
|
+
printInfo(`Schedule: ${job.cron}`);
|
|
190
|
+
} else if (job.runAt) {
|
|
191
|
+
const runDate = new Date(job.runAt);
|
|
192
|
+
printInfo(`Scheduled for: ${runDate.toLocaleString()}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
printError('Unexpected response from daemon');
|
|
199
|
+
return 1;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
printError(`Failed to add job: ${error.message}`);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default addCommand;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config command for JM2
|
|
3
|
+
* View and modify configuration settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getConfig, saveConfig, setConfigValue, getConfigValue, validateConfig, DEFAULT_CONFIG } from '../../core/config.js';
|
|
7
|
+
import output from '../utils/output.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse a value to the appropriate type based on the config key
|
|
11
|
+
* @param {string} key - Config key
|
|
12
|
+
* @param {string} value - Value as string
|
|
13
|
+
* @returns {*} Parsed value
|
|
14
|
+
*/
|
|
15
|
+
function parseConfigValue(key, value) {
|
|
16
|
+
// Handle numeric values
|
|
17
|
+
if (key.includes('max') || key.includes('Count') || key.includes('Days') || key.includes('Size')) {
|
|
18
|
+
const num = parseInt(value, 10);
|
|
19
|
+
if (isNaN(num)) {
|
|
20
|
+
throw new Error(`Invalid numeric value: ${value}`);
|
|
21
|
+
}
|
|
22
|
+
return num;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle boolean values
|
|
26
|
+
if (value.toLowerCase() === 'true') return true;
|
|
27
|
+
if (value.toLowerCase() === 'false') return false;
|
|
28
|
+
|
|
29
|
+
// Return as string
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a config value for display
|
|
35
|
+
* @param {*} value - Config value
|
|
36
|
+
* @returns {string} Formatted value
|
|
37
|
+
*/
|
|
38
|
+
function formatValue(value) {
|
|
39
|
+
if (value === null) return 'null';
|
|
40
|
+
if (value === undefined) return 'undefined';
|
|
41
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
42
|
+
if (typeof value === 'string') return value;
|
|
43
|
+
if (typeof value === 'number') {
|
|
44
|
+
// Format large numbers (bytes) as human readable
|
|
45
|
+
if (value >= 1024 * 1024) {
|
|
46
|
+
return `${value} (${(value / 1024 / 1024).toFixed(1)}MB)`;
|
|
47
|
+
}
|
|
48
|
+
if (value >= 1024) {
|
|
49
|
+
return `${value} (${(value / 1024).toFixed(1)}KB)`;
|
|
50
|
+
}
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
54
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
55
|
+
return String(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Show all configuration settings
|
|
60
|
+
* @param {object} config - Config object
|
|
61
|
+
*/
|
|
62
|
+
function showAllConfig(config) {
|
|
63
|
+
output.section('Daemon Settings');
|
|
64
|
+
output.keyValue('Max Concurrent', formatValue(config.daemon?.maxConcurrent));
|
|
65
|
+
output.keyValue('Shell', formatValue(config.daemon?.shell));
|
|
66
|
+
output.keyValue('Shell Args', formatValue(config.daemon?.shellArgs));
|
|
67
|
+
|
|
68
|
+
output.section('Job Defaults');
|
|
69
|
+
output.keyValue('Default Timeout', formatValue(config.jobs?.defaultTimeout));
|
|
70
|
+
output.keyValue('Default Retry', formatValue(config.jobs?.defaultRetry));
|
|
71
|
+
output.keyValue('Default CWD', formatValue(config.jobs?.defaultCwd));
|
|
72
|
+
|
|
73
|
+
output.section('Logging Settings');
|
|
74
|
+
output.keyValue('Log Level', formatValue(config.logging?.level));
|
|
75
|
+
output.keyValue('Max File Size', formatValue(config.logging?.maxFileSize));
|
|
76
|
+
output.keyValue('Max Files', formatValue(config.logging?.maxFiles));
|
|
77
|
+
|
|
78
|
+
output.section('History Settings');
|
|
79
|
+
output.keyValue('Max Entries Per Job', formatValue(config.history?.maxEntriesPerJob));
|
|
80
|
+
output.keyValue('Retention Days', formatValue(config.history?.retentionDays));
|
|
81
|
+
|
|
82
|
+
output.section('Cleanup Settings');
|
|
83
|
+
output.keyValue('Completed Job Retention (days)', formatValue(config.cleanup?.completedJobRetentionDays));
|
|
84
|
+
output.keyValue('Log Retention (days)', formatValue(config.cleanup?.logRetentionDays));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Config command implementation
|
|
89
|
+
* @param {object} options - Command options
|
|
90
|
+
* @returns {number} Exit code
|
|
91
|
+
*/
|
|
92
|
+
export async function configCommand(options) {
|
|
93
|
+
try {
|
|
94
|
+
const config = getConfig();
|
|
95
|
+
|
|
96
|
+
// Show all config if no specific option provided
|
|
97
|
+
if (options.show || Object.keys(options).length === 0 ||
|
|
98
|
+
(!options.logMaxSize && !options.logMaxFiles && !options.level &&
|
|
99
|
+
!options.maxConcurrent && !options.reset)) {
|
|
100
|
+
showAllConfig(config);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle reset
|
|
105
|
+
if (options.reset) {
|
|
106
|
+
saveConfig({});
|
|
107
|
+
output.success('Configuration reset to defaults');
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let changes = [];
|
|
112
|
+
|
|
113
|
+
// Handle log-max-size
|
|
114
|
+
if (options.logMaxSize !== undefined) {
|
|
115
|
+
const size = parseSizeOption(options.logMaxSize);
|
|
116
|
+
if (size === null) {
|
|
117
|
+
output.error(`Invalid size format: ${options.logMaxSize}`);
|
|
118
|
+
output.info('Use formats like: 10mb, 50MB, 100kb, 1gb');
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
setConfigValue('logging.maxFileSize', size);
|
|
122
|
+
changes.push(`Log max file size: ${formatValue(size)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle log-max-files
|
|
126
|
+
if (options.logMaxFiles !== undefined) {
|
|
127
|
+
const count = parseInt(options.logMaxFiles, 10);
|
|
128
|
+
if (isNaN(count) || count < 1) {
|
|
129
|
+
output.error(`Invalid file count: ${options.logMaxFiles}`);
|
|
130
|
+
output.info('Must be a positive number');
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
setConfigValue('logging.maxFiles', count);
|
|
134
|
+
changes.push(`Log max files: ${count}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle log level
|
|
138
|
+
if (options.level !== undefined) {
|
|
139
|
+
const validLevels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
|
|
140
|
+
const level = options.level.toUpperCase();
|
|
141
|
+
if (!validLevels.includes(level)) {
|
|
142
|
+
output.error(`Invalid log level: ${options.level}`);
|
|
143
|
+
output.info(`Valid levels: ${validLevels.join(', ')}`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
setConfigValue('logging.level', level);
|
|
147
|
+
changes.push(`Log level: ${level}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle max concurrent
|
|
151
|
+
if (options.maxConcurrent !== undefined) {
|
|
152
|
+
const count = parseInt(options.maxConcurrent, 10);
|
|
153
|
+
if (isNaN(count) || count < 1) {
|
|
154
|
+
output.error(`Invalid concurrent count: ${options.maxConcurrent}`);
|
|
155
|
+
output.info('Must be a positive number');
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
setConfigValue('daemon.maxConcurrent', count);
|
|
159
|
+
changes.push(`Max concurrent jobs: ${count}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate the new config
|
|
163
|
+
const newConfig = getConfig();
|
|
164
|
+
const validation = validateConfig(newConfig);
|
|
165
|
+
if (!validation.valid) {
|
|
166
|
+
output.error('Configuration validation failed:');
|
|
167
|
+
for (const error of validation.errors) {
|
|
168
|
+
output.error(` - ${error}`);
|
|
169
|
+
}
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Show changes
|
|
174
|
+
if (changes.length > 0) {
|
|
175
|
+
output.success('Configuration updated:');
|
|
176
|
+
for (const change of changes) {
|
|
177
|
+
output.info(` • ${change}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return 0;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
output.error(`Config command failed: ${error.message}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Parse a size option (e.g., "10mb", "50MB")
|
|
190
|
+
* @param {string} value - Size string
|
|
191
|
+
* @returns {number|null} Size in bytes or null if invalid
|
|
192
|
+
*/
|
|
193
|
+
function parseSizeOption(value) {
|
|
194
|
+
const match = String(value).trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
|
|
195
|
+
if (!match) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const num = parseFloat(match[1]);
|
|
200
|
+
const unit = match[2] || 'b';
|
|
201
|
+
|
|
202
|
+
const multipliers = {
|
|
203
|
+
'b': 1,
|
|
204
|
+
'kb': 1024,
|
|
205
|
+
'mb': 1024 * 1024,
|
|
206
|
+
'gb': 1024 * 1024 * 1024,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return Math.floor(num * multipliers[unit]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default { configCommand };
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 edit command
|
|
3
|
+
* Edit an existing job's properties
|
|
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
|
+
import { parseDateTime, parseRunIn } from '../../utils/datetime.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute the edit command
|
|
14
|
+
* @param {string} jobRef - Job ID or name
|
|
15
|
+
* @param {object} options - Command options
|
|
16
|
+
* @returns {Promise<number>} Exit code
|
|
17
|
+
*/
|
|
18
|
+
export async function editCommand(jobRef, 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
|
+
if (!jobRef || jobRef.trim() === '') {
|
|
26
|
+
printError('Job ID or name is required');
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Build updates object from options
|
|
31
|
+
const updates = {};
|
|
32
|
+
|
|
33
|
+
// Validate that at least one option is provided
|
|
34
|
+
const hasUpdates =
|
|
35
|
+
options.command !== undefined ||
|
|
36
|
+
options.name !== undefined ||
|
|
37
|
+
options.cron !== undefined ||
|
|
38
|
+
options.at !== undefined ||
|
|
39
|
+
options.delay !== undefined ||
|
|
40
|
+
options.cwd !== undefined ||
|
|
41
|
+
options.env !== undefined ||
|
|
42
|
+
options.env !== undefined ||
|
|
43
|
+
options.timeout !== undefined ||
|
|
44
|
+
options.retry !== undefined ||
|
|
45
|
+
options.tag !== undefined;
|
|
46
|
+
|
|
47
|
+
if (!hasUpdates) {
|
|
48
|
+
printError('No changes specified. Use options like --command, --cron, --name, etc.');
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle mutually exclusive scheduling options
|
|
53
|
+
const hasCron = options.cron !== undefined;
|
|
54
|
+
const hasAt = options.at !== undefined;
|
|
55
|
+
const hasDelay = options.delay !== undefined;
|
|
56
|
+
|
|
57
|
+
if (hasCron && hasAt) {
|
|
58
|
+
printError('Cannot specify both --cron and --at');
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (hasCron && hasDelay) {
|
|
62
|
+
printError('Cannot specify both --cron and --delay');
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
if (hasAt && hasDelay) {
|
|
66
|
+
printError('Cannot specify both --at and --delay');
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Apply updates
|
|
71
|
+
if (options.command !== undefined) {
|
|
72
|
+
if (!options.command.trim()) {
|
|
73
|
+
printError('Command cannot be empty');
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
updates.command = options.command.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.name !== undefined) {
|
|
80
|
+
if (!options.name.trim()) {
|
|
81
|
+
printError('Name cannot be empty');
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
updates.name = options.name.trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle scheduling updates
|
|
88
|
+
if (hasCron) {
|
|
89
|
+
updates.cron = options.cron;
|
|
90
|
+
updates.runAt = null; // Clear runAt when switching to cron
|
|
91
|
+
} else if (hasAt) {
|
|
92
|
+
try {
|
|
93
|
+
const date = parseDateTime(options.at);
|
|
94
|
+
updates.runAt = date.toISOString();
|
|
95
|
+
updates.cron = null; // Clear cron when switching to runAt
|
|
96
|
+
} catch (error) {
|
|
97
|
+
printError(`Invalid datetime: ${error.message}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
} else if (hasDelay) {
|
|
101
|
+
try {
|
|
102
|
+
const date = parseRunIn(options.delay);
|
|
103
|
+
updates.runAt = date.toISOString();
|
|
104
|
+
updates.cron = null; // Clear cron when switching to runAt
|
|
105
|
+
} catch (error) {
|
|
106
|
+
printError(`Invalid duration: ${error.message}`);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options.cwd !== undefined) {
|
|
112
|
+
updates.cwd = options.cwd || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (options.env !== undefined) {
|
|
116
|
+
// Parse env options (format: KEY=value)
|
|
117
|
+
const envVars = Array.isArray(options.env)
|
|
118
|
+
? options.env
|
|
119
|
+
: options.env ? [options.env] : [];
|
|
120
|
+
|
|
121
|
+
if (envVars.length > 0) {
|
|
122
|
+
updates.env = {};
|
|
123
|
+
for (const envVar of envVars) {
|
|
124
|
+
const [key, ...valueParts] = envVar.split('=');
|
|
125
|
+
if (key && valueParts.length > 0) {
|
|
126
|
+
updates.env[key] = valueParts.join('=');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (options.timeout !== undefined) {
|
|
133
|
+
updates.timeout = options.timeout || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.retry !== undefined) {
|
|
137
|
+
const retry = parseInt(options.retry, 10);
|
|
138
|
+
if (isNaN(retry) || retry < 0) {
|
|
139
|
+
printError('Retry must be a non-negative integer');
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
updates.retry = retry;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.tag !== undefined) {
|
|
146
|
+
// Handle multiple tags
|
|
147
|
+
const tags = Array.isArray(options.tag)
|
|
148
|
+
? options.tag
|
|
149
|
+
: options.tag ? [options.tag] : [];
|
|
150
|
+
if (tags.length > 0) {
|
|
151
|
+
updates.tags = tags;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Determine if jobRef is an ID (numeric) or name
|
|
157
|
+
const jobId = parseInt(jobRef, 10);
|
|
158
|
+
const message = isNaN(jobId)
|
|
159
|
+
? { type: MessageType.JOB_UPDATE, jobName: jobRef, updates }
|
|
160
|
+
: { type: MessageType.JOB_UPDATE, jobId, updates };
|
|
161
|
+
|
|
162
|
+
printInfo(`Updating job: ${jobRef}...`);
|
|
163
|
+
|
|
164
|
+
const response = await send(message);
|
|
165
|
+
|
|
166
|
+
if (response.type === MessageType.ERROR) {
|
|
167
|
+
printError(response.message);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (response.type === MessageType.JOB_UPDATED) {
|
|
172
|
+
const job = response.job;
|
|
173
|
+
|
|
174
|
+
if (!job) {
|
|
175
|
+
printError(`Job not found: ${jobRef}`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
printSuccess(`Job updated: ${job.name || job.id}`);
|
|
180
|
+
|
|
181
|
+
// Show what was updated
|
|
182
|
+
const updatedFields = Object.keys(updates);
|
|
183
|
+
if (updatedFields.length > 0) {
|
|
184
|
+
printInfo(`Updated fields: ${updatedFields.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printError('Unexpected response from daemon');
|
|
191
|
+
return 1;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
printError(`Failed to update job: ${error.message}`);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default { editCommand };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JM2 export command
|
|
3
|
+
* Exports job configurations to a JSON file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { getJobs } from '../../core/storage.js';
|
|
9
|
+
import { printSuccess, printError, printInfo } from '../utils/output.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute the export command
|
|
13
|
+
* @param {object} options - Command options
|
|
14
|
+
* @returns {Promise<number>} Exit code
|
|
15
|
+
*/
|
|
16
|
+
export async function exportCommand(options = {}) {
|
|
17
|
+
try {
|
|
18
|
+
// Get all jobs from storage
|
|
19
|
+
const jobs = getJobs();
|
|
20
|
+
|
|
21
|
+
if (jobs.length === 0) {
|
|
22
|
+
printInfo('No jobs to export');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Prepare export data
|
|
27
|
+
const exportData = {
|
|
28
|
+
version: '1.0',
|
|
29
|
+
exportedAt: new Date().toISOString(),
|
|
30
|
+
jobs: jobs.map(job => ({
|
|
31
|
+
// Export all job fields except runtime state
|
|
32
|
+
// Generate a name for unnamed jobs using job ID
|
|
33
|
+
name: job.name || `job-${job.id}`,
|
|
34
|
+
command: job.command,
|
|
35
|
+
type: job.type,
|
|
36
|
+
cron: job.cron,
|
|
37
|
+
runAt: job.runAt,
|
|
38
|
+
status: job.status,
|
|
39
|
+
tags: job.tags,
|
|
40
|
+
env: job.env,
|
|
41
|
+
cwd: job.cwd,
|
|
42
|
+
timeout: job.timeout,
|
|
43
|
+
retry: job.retry,
|
|
44
|
+
createdAt: job.createdAt,
|
|
45
|
+
updatedAt: job.updatedAt,
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Determine output path
|
|
50
|
+
const outputPath = resolve(options.output || 'jm2-export.json');
|
|
51
|
+
|
|
52
|
+
// Write to file
|
|
53
|
+
writeFileSync(outputPath, JSON.stringify(exportData, null, 2), 'utf8');
|
|
54
|
+
|
|
55
|
+
printSuccess(`Exported ${jobs.length} job(s) to ${outputPath}`);
|
|
56
|
+
return 0;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
printError(`Failed to export jobs: ${error.message}`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|