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