jm2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/GNU-AGPL-3.0 +665 -0
  2. package/README.md +603 -0
  3. package/bin/jm2.js +24 -0
  4. package/package.json +70 -0
  5. package/src/cli/commands/add.js +206 -0
  6. package/src/cli/commands/config.js +212 -0
  7. package/src/cli/commands/edit.js +198 -0
  8. package/src/cli/commands/export.js +61 -0
  9. package/src/cli/commands/flush.js +132 -0
  10. package/src/cli/commands/history.js +179 -0
  11. package/src/cli/commands/import.js +180 -0
  12. package/src/cli/commands/list.js +174 -0
  13. package/src/cli/commands/logs.js +415 -0
  14. package/src/cli/commands/pause.js +97 -0
  15. package/src/cli/commands/remove.js +107 -0
  16. package/src/cli/commands/restart.js +68 -0
  17. package/src/cli/commands/resume.js +96 -0
  18. package/src/cli/commands/run.js +115 -0
  19. package/src/cli/commands/show.js +159 -0
  20. package/src/cli/commands/start.js +46 -0
  21. package/src/cli/commands/status.js +47 -0
  22. package/src/cli/commands/stop.js +48 -0
  23. package/src/cli/index.js +274 -0
  24. package/src/cli/utils/output.js +267 -0
  25. package/src/cli/utils/prompts.js +56 -0
  26. package/src/core/config.js +227 -0
  27. package/src/core/history-db.js +439 -0
  28. package/src/core/job.js +329 -0
  29. package/src/core/logger.js +382 -0
  30. package/src/core/storage.js +315 -0
  31. package/src/daemon/executor.js +409 -0
  32. package/src/daemon/index.js +873 -0
  33. package/src/daemon/scheduler.js +465 -0
  34. package/src/ipc/client.js +112 -0
  35. package/src/ipc/protocol.js +183 -0
  36. package/src/ipc/server.js +92 -0
  37. package/src/utils/cron.js +205 -0
  38. package/src/utils/datetime.js +237 -0
  39. package/src/utils/duration.js +226 -0
  40. package/src/utils/paths.js +164 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Storage utilities for JM2
3
+ * Provides JSON file persistence for jobs, config, and history
4
+ * History is now stored in SQLite (via history-db.js) for better performance
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
8
+ import { ensureDataDir, getJobsFile, getConfigFile } from '../utils/paths.js';
9
+ import {
10
+ addHistoryEntry as dbAddHistoryEntry,
11
+ getHistory as dbGetHistory,
12
+ getJobHistory as dbGetJobHistory,
13
+ clearHistoryBefore as dbClearHistoryBefore,
14
+ clearAllHistory as dbClearAllHistory,
15
+ } from './history-db.js';
16
+
17
+ /**
18
+ * Read a JSON file and parse its contents
19
+ * @param {string} filePath - Path to the JSON file
20
+ * @param {*} defaultValue - Default value if file doesn't exist
21
+ * @returns {*} Parsed JSON content or default value
22
+ */
23
+ export function readJsonFile(filePath, defaultValue = null) {
24
+ try {
25
+ if (!existsSync(filePath)) {
26
+ return defaultValue;
27
+ }
28
+ const content = readFileSync(filePath, 'utf8');
29
+ return JSON.parse(content);
30
+ } catch (error) {
31
+ if (error.code === 'ENOENT') {
32
+ return defaultValue;
33
+ }
34
+ throw new Error(`Failed to read JSON file ${filePath}: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Write data to a JSON file
40
+ * @param {string} filePath - Path to the JSON file
41
+ * @param {*} data - Data to write
42
+ * @param {boolean} pretty - Whether to pretty-print the JSON (default: true)
43
+ */
44
+ export function writeJsonFile(filePath, data, pretty = true) {
45
+ try {
46
+ ensureDataDir();
47
+ const content = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
48
+ writeFileSync(filePath, content, 'utf8');
49
+ } catch (error) {
50
+ throw new Error(`Failed to write JSON file ${filePath}: ${error.message}`);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Jobs storage operations
56
+ */
57
+
58
+ /**
59
+ * Get all jobs from storage
60
+ * @returns {Array} Array of job objects
61
+ */
62
+ export function getJobs() {
63
+ return readJsonFile(getJobsFile(), []);
64
+ }
65
+
66
+ /**
67
+ * Save all jobs to storage
68
+ * @param {Array} jobs - Array of job objects
69
+ */
70
+ export function saveJobs(jobs) {
71
+ writeJsonFile(getJobsFile(), jobs);
72
+ }
73
+
74
+ /**
75
+ * Get a job by ID
76
+ * @param {number} id - Job ID
77
+ * @returns {object|null} Job object or null if not found
78
+ */
79
+ export function getJobById(id) {
80
+ const jobs = getJobs();
81
+ return jobs.find(job => job.id === id) || null;
82
+ }
83
+
84
+ /**
85
+ * Get a job by name
86
+ * @param {string} name - Job name
87
+ * @returns {object|null} Job object or null if not found
88
+ */
89
+ export function getJobByName(name) {
90
+ const jobs = getJobs();
91
+ return jobs.find(job => job.name === name) || null;
92
+ }
93
+
94
+ /**
95
+ * Get a job by ID or name
96
+ * @param {string|number} identifier - Job ID or name
97
+ * @returns {object|null} Job object or null if not found
98
+ */
99
+ export function getJob(identifier) {
100
+ const id = parseInt(identifier, 10);
101
+ if (!isNaN(id)) {
102
+ const job = getJobById(id);
103
+ if (job) return job;
104
+ }
105
+ return getJobByName(String(identifier));
106
+ }
107
+
108
+ /**
109
+ * Get the next available job ID
110
+ * @returns {number} Next available ID
111
+ */
112
+ export function getNextJobId() {
113
+ const jobs = getJobs();
114
+ if (jobs.length === 0) {
115
+ return 1;
116
+ }
117
+ const maxId = Math.max(...jobs.map(job => job.id));
118
+ return maxId + 1;
119
+ }
120
+
121
+ /**
122
+ * Add a new job to storage
123
+ * @param {object} job - Job object (without ID)
124
+ * @returns {object} Job object with assigned ID
125
+ */
126
+ export function addJob(job) {
127
+ const jobs = getJobs();
128
+ const newJob = {
129
+ ...job,
130
+ id: job.id || getNextJobId(),
131
+ createdAt: job.createdAt || new Date().toISOString(),
132
+ updatedAt: new Date().toISOString(),
133
+ };
134
+ jobs.push(newJob);
135
+ saveJobs(jobs);
136
+ return newJob;
137
+ }
138
+
139
+ /**
140
+ * Update an existing job
141
+ * @param {number} id - Job ID
142
+ * @param {object} updates - Fields to update
143
+ * @returns {object|null} Updated job or null if not found
144
+ */
145
+ export function updateJob(id, updates) {
146
+ const jobs = getJobs();
147
+ const index = jobs.findIndex(job => job.id === id);
148
+ if (index === -1) {
149
+ return null;
150
+ }
151
+ jobs[index] = {
152
+ ...jobs[index],
153
+ ...updates,
154
+ id, // Ensure ID cannot be changed
155
+ updatedAt: new Date().toISOString(),
156
+ };
157
+ saveJobs(jobs);
158
+ return jobs[index];
159
+ }
160
+
161
+ /**
162
+ * Remove a job from storage
163
+ * @param {number} id - Job ID
164
+ * @returns {boolean} True if job was removed, false if not found
165
+ */
166
+ export function removeJob(id) {
167
+ const jobs = getJobs();
168
+ const index = jobs.findIndex(job => job.id === id);
169
+ if (index === -1) {
170
+ return false;
171
+ }
172
+ jobs.splice(index, 1);
173
+ saveJobs(jobs);
174
+ return true;
175
+ }
176
+
177
+ /**
178
+ * Check if a job name already exists
179
+ * @param {string} name - Job name to check
180
+ * @param {number} excludeId - Optional job ID to exclude from check (for updates)
181
+ * @returns {boolean} True if name exists
182
+ */
183
+ export function jobNameExists(name, excludeId = null) {
184
+ const jobs = getJobs();
185
+ return jobs.some(job => job.name === name && job.id !== excludeId);
186
+ }
187
+
188
+ /**
189
+ * Generate a unique job name with auto-suffix
190
+ * @param {string} baseName - Base name to use
191
+ * @returns {string} Unique name (baseName, baseName-2, baseName-3, etc.)
192
+ */
193
+ export function generateUniqueName(baseName) {
194
+ if (!jobNameExists(baseName)) {
195
+ return baseName;
196
+ }
197
+ let suffix = 2;
198
+ while (jobNameExists(`${baseName}-${suffix}`)) {
199
+ suffix++;
200
+ }
201
+ return `${baseName}-${suffix}`;
202
+ }
203
+
204
+ /**
205
+ * Generate an auto job name (job-1, job-2, etc.)
206
+ * @returns {string} Auto-generated job name
207
+ */
208
+ export function generateAutoName() {
209
+ return generateUniqueName('job-1').replace('job-1-', 'job-');
210
+ }
211
+
212
+ /**
213
+ * Get jobs filtered by tag
214
+ * @param {string} tag - Tag to filter by
215
+ * @returns {Array} Array of jobs with the specified tag
216
+ */
217
+ export function getJobsByTag(tag) {
218
+ const jobs = getJobs();
219
+ return jobs.filter(job => job.tags && job.tags.includes(tag));
220
+ }
221
+
222
+ /**
223
+ * Get jobs filtered by status
224
+ * @param {string} status - Status to filter by ('active', 'paused', 'completed')
225
+ * @returns {Array} Array of jobs with the specified status
226
+ */
227
+ export function getJobsByStatus(status) {
228
+ const jobs = getJobs();
229
+ return jobs.filter(job => job.status === status);
230
+ }
231
+
232
+ /**
233
+ * History storage operations
234
+ * Delegated to SQLite database via history-db.js
235
+ */
236
+
237
+ /**
238
+ * Get execution history
239
+ * @param {object} options - Query options (limit, offset, jobId, status, since, until, order)
240
+ * @returns {Array} Array of history entries
241
+ */
242
+ export function getHistory(options = {}) {
243
+ return dbGetHistory(options);
244
+ }
245
+
246
+ /**
247
+ * Save execution history (deprecated, no-op for SQLite)
248
+ * @param {Array} history - Array of history entries
249
+ * @deprecated History is now automatically saved to SQLite
250
+ */
251
+ export function saveHistory(history) {
252
+ // No-op: History is now managed by SQLite
253
+ // This function is kept for API compatibility
254
+ }
255
+
256
+ /**
257
+ * Add a history entry
258
+ * @param {object} entry - History entry
259
+ * @returns {object} Added history entry with timestamp and id
260
+ */
261
+ export function addHistoryEntry(entry) {
262
+ return dbAddHistoryEntry(entry);
263
+ }
264
+
265
+ /**
266
+ * Get history for a specific job
267
+ * @param {number} jobId - Job ID
268
+ * @param {number} limit - Maximum number of entries to return
269
+ * @returns {Array} Array of history entries for the job
270
+ */
271
+ export function getJobHistory(jobId, limit = 10) {
272
+ return dbGetJobHistory(jobId, limit);
273
+ }
274
+
275
+ /**
276
+ * Clear history older than a certain date
277
+ * @param {Date} beforeDate - Clear entries before this date
278
+ * @returns {number} Number of entries removed
279
+ */
280
+ export function clearHistoryBefore(beforeDate) {
281
+ return dbClearHistoryBefore(beforeDate);
282
+ }
283
+
284
+ /**
285
+ * Clear all history
286
+ * @returns {number} Number of entries removed
287
+ */
288
+ export function clearAllHistory() {
289
+ return dbClearAllHistory();
290
+ }
291
+
292
+ export default {
293
+ readJsonFile,
294
+ writeJsonFile,
295
+ getJobs,
296
+ saveJobs,
297
+ getJobById,
298
+ getJobByName,
299
+ getJob,
300
+ getNextJobId,
301
+ addJob,
302
+ updateJob,
303
+ removeJob,
304
+ jobNameExists,
305
+ generateUniqueName,
306
+ generateAutoName,
307
+ getJobsByTag,
308
+ getJobsByStatus,
309
+ getHistory,
310
+ saveHistory,
311
+ addHistoryEntry,
312
+ getJobHistory,
313
+ clearHistoryBefore,
314
+ clearAllHistory,
315
+ };
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Command executor for JM2 daemon
3
+ * Handles job execution with process management, timeout, and logging
4
+ */
5
+
6
+ import { spawn, exec } from 'node:child_process';
7
+ import { addHistoryEntry } from '../core/storage.js';
8
+ import { getJobLogFile, ensureLogsDir } from '../utils/paths.js';
9
+ import { createLogger } from '../core/logger.js';
10
+ import { parseDuration } from '../utils/duration.js';
11
+ import { promisify } from 'node:util';
12
+
13
+ const execAsync = promisify(exec);
14
+
15
+ /**
16
+ * Execution result status
17
+ */
18
+ export const ExecutionStatus = {
19
+ SUCCESS: 'success',
20
+ FAILED: 'failed',
21
+ TIMEOUT: 'timeout',
22
+ KILLED: 'killed',
23
+ };
24
+
25
+ /**
26
+ * Default execution options
27
+ */
28
+ const DEFAULT_OPTIONS = {
29
+ shell: '/bin/sh',
30
+ timeout: null, // No timeout by default
31
+ cwd: null,
32
+ env: null,
33
+ captureOutput: true,
34
+ };
35
+
36
+ /**
37
+ * Execute a job command
38
+ * @param {object} job - Job object to execute
39
+ * @param {object} options - Execution options
40
+ * @returns {Promise<object>} Execution result
41
+ */
42
+ export function executeJob(job, options = {}) {
43
+ const execOptions = {
44
+ ...DEFAULT_OPTIONS,
45
+ ...options,
46
+ };
47
+
48
+ return new Promise((resolve) => {
49
+ const startTime = new Date();
50
+ const startTimeISO = startTime.toISOString();
51
+ const jobId = job.id;
52
+ const jobName = job.name || `job-${jobId}`;
53
+
54
+ // Setup logging - use custom name or just job ID for logger name
55
+ const logFile = getJobLogFile(jobName);
56
+ ensureLogsDir();
57
+ const loggerName = job.name || String(jobId);
58
+ const jobLogger = createLogger({ name: loggerName, file: logFile });
59
+
60
+ // Log execution start
61
+ jobLogger.info(`Starting execution: ${job.command}`);
62
+ jobLogger.info(`Working directory: ${job.cwd || process.cwd()}`);
63
+
64
+ // Prepare spawn options
65
+ const spawnOptions = {
66
+ shell: job.shell || execOptions.shell,
67
+ cwd: job.cwd || execOptions.cwd || process.cwd(),
68
+ env: { ...process.env, ...(job.env || execOptions.env || {}) },
69
+ };
70
+
71
+ // Parse timeout
72
+ let timeoutMs = null;
73
+ if (job.timeout) {
74
+ try {
75
+ timeoutMs = parseDuration(job.timeout);
76
+ jobLogger.info(`Timeout set: ${job.timeout} (${timeoutMs}ms)`);
77
+ } catch (error) {
78
+ jobLogger.warn(`Invalid timeout format: ${job.timeout}`);
79
+ }
80
+ } else if (execOptions.timeout) {
81
+ timeoutMs = execOptions.timeout;
82
+ }
83
+
84
+ // Spawn the process with detached mode to create a new process group
85
+ let childProcess;
86
+ try {
87
+ childProcess = spawn(job.command, [], { ...spawnOptions, detached: true });
88
+ } catch (error) {
89
+ const result = {
90
+ status: ExecutionStatus.FAILED,
91
+ exitCode: null,
92
+ startTime: startTimeISO,
93
+ endTime: new Date().toISOString(),
94
+ duration: 0,
95
+ error: error.message,
96
+ stdout: '',
97
+ stderr: '',
98
+ };
99
+
100
+ jobLogger.error(`Failed to spawn process: ${error.message}`);
101
+ recordHistory(job, result);
102
+ resolve(result);
103
+ return;
104
+ }
105
+
106
+ let stdout = '';
107
+ let stderr = '';
108
+ let timeoutId = null;
109
+ let timedOut = false;
110
+
111
+ // Handle timeout
112
+ if (timeoutMs && timeoutMs > 0) {
113
+ timeoutId = setTimeout(() => {
114
+ timedOut = true;
115
+ jobLogger.warn(`Job timed out after ${timeoutMs}ms, killing process...`);
116
+
117
+ // Kill the entire process group (negative PID)
118
+ // This ensures child processes spawned by the shell are also killed
119
+ try {
120
+ process.kill(-childProcess.pid, 'SIGTERM');
121
+ } catch (err) {
122
+ // Fallback to single process kill if process group kill fails
123
+ childProcess.kill('SIGTERM');
124
+ }
125
+
126
+ // Force kill after grace period
127
+ setTimeout(() => {
128
+ try {
129
+ process.kill(-childProcess.pid, 'SIGKILL');
130
+ } catch (err) {
131
+ if (!childProcess.killed) {
132
+ jobLogger.warn('Process did not terminate gracefully, forcing kill...');
133
+ childProcess.kill('SIGKILL');
134
+ }
135
+ }
136
+ }, 1000);
137
+ }, timeoutMs);
138
+ }
139
+
140
+ // Capture stdout
141
+ if (childProcess.stdout) {
142
+ childProcess.stdout.on('data', (data) => {
143
+ const chunk = data.toString();
144
+ stdout += chunk;
145
+ jobLogger.info(`[stdout] ${chunk.trim()}`);
146
+ });
147
+ }
148
+
149
+ // Capture stderr
150
+ if (childProcess.stderr) {
151
+ childProcess.stderr.on('data', (data) => {
152
+ const chunk = data.toString();
153
+ stderr += chunk;
154
+ jobLogger.warn(`[stderr] ${chunk.trim()}`);
155
+ });
156
+ }
157
+
158
+ // Handle process completion
159
+ childProcess.on('close', (exitCode, signal) => {
160
+ // Clear timeout if set
161
+ if (timeoutId) {
162
+ clearTimeout(timeoutId);
163
+ }
164
+
165
+ const endTime = new Date();
166
+ const duration = endTime - startTime;
167
+
168
+ // Determine execution status
169
+ let status = ExecutionStatus.SUCCESS;
170
+ let error = null;
171
+
172
+ if (timedOut) {
173
+ status = ExecutionStatus.TIMEOUT;
174
+ error = `Job timed out after ${timeoutMs}ms`;
175
+ } else if (signal) {
176
+ status = ExecutionStatus.KILLED;
177
+ error = `Job killed with signal ${signal}`;
178
+ } else if (exitCode !== 0) {
179
+ status = ExecutionStatus.FAILED;
180
+ error = `Process exited with code ${exitCode}`;
181
+ }
182
+
183
+ const result = {
184
+ status,
185
+ exitCode: exitCode ?? null,
186
+ signal: signal || null,
187
+ startTime: startTimeISO,
188
+ endTime: endTime.toISOString(),
189
+ duration,
190
+ stdout,
191
+ stderr,
192
+ error,
193
+ };
194
+
195
+ // Log completion
196
+ if (status === ExecutionStatus.SUCCESS) {
197
+ jobLogger.info(`Execution completed successfully in ${duration}ms`);
198
+ } else {
199
+ jobLogger.error(`Execution ${status}: ${error}`);
200
+ }
201
+
202
+ // Record history
203
+ recordHistory(job, result);
204
+
205
+ resolve(result);
206
+ });
207
+
208
+ // Handle spawn errors
209
+ childProcess.on('error', (error) => {
210
+ if (timeoutId) {
211
+ clearTimeout(timeoutId);
212
+ }
213
+
214
+ const endTime = new Date();
215
+ const duration = endTime - startTime;
216
+
217
+ const result = {
218
+ status: ExecutionStatus.FAILED,
219
+ exitCode: null,
220
+ signal: null,
221
+ startTime: startTimeISO,
222
+ endTime: endTime.toISOString(),
223
+ duration,
224
+ stdout,
225
+ stderr,
226
+ error: error.message,
227
+ };
228
+
229
+ jobLogger.error(`Process error: ${error.message}`);
230
+ recordHistory(job, result);
231
+
232
+ resolve(result);
233
+ });
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Record execution history
239
+ * @param {object} job - Job object
240
+ * @param {object} result - Execution result
241
+ */
242
+ function recordHistory(job, result) {
243
+ try {
244
+ addHistoryEntry({
245
+ jobId: job.id,
246
+ jobName: job.name,
247
+ command: job.command,
248
+ status: result.status,
249
+ exitCode: result.exitCode,
250
+ startTime: result.startTime,
251
+ endTime: result.endTime,
252
+ duration: result.duration,
253
+ error: result.error,
254
+ });
255
+ } catch (error) {
256
+ // Don't let history recording failures break execution
257
+ console.error('Failed to record history:', error.message);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Execute a job with retry logic
263
+ * @param {object} job - Job object to execute
264
+ * @param {object} options - Execution options
265
+ * @returns {Promise<object>} Final execution result
266
+ */
267
+ export async function executeJobWithRetry(job, options = {}) {
268
+ const maxRetries = job.retry || 0;
269
+ const retryDelay = options.retryDelay || 1000; // Default 1 second delay between retries
270
+
271
+ let lastResult = null;
272
+ let attempt = 0;
273
+
274
+ while (attempt <= maxRetries) {
275
+ attempt++;
276
+
277
+ // Update job retry count for tracking
278
+ if (attempt > 1) {
279
+ const logger = createLogger({ name: 'executor' });
280
+ logger.info(`Retry attempt ${attempt - 1} of ${maxRetries} for job ${job.id}`);
281
+ }
282
+
283
+ lastResult = await executeJob(job, options);
284
+
285
+ // If successful, return immediately
286
+ if (lastResult.status === ExecutionStatus.SUCCESS) {
287
+ return {
288
+ ...lastResult,
289
+ attempts: attempt,
290
+ };
291
+ }
292
+
293
+ // If not the last attempt, wait before retrying
294
+ if (attempt <= maxRetries) {
295
+ await delay(retryDelay);
296
+ }
297
+ }
298
+
299
+ // All retries exhausted
300
+ return {
301
+ ...lastResult,
302
+ attempts: attempt,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Delay for a specified time
308
+ * @param {number} ms - Milliseconds to delay
309
+ * @returns {Promise<void>}
310
+ */
311
+ function delay(ms) {
312
+ return new Promise(resolve => setTimeout(resolve, ms));
313
+ }
314
+
315
+ /**
316
+ * Kill a running job process
317
+ * @param {object} childProcess - Child process to kill
318
+ * @param {number} gracePeriodMs - Grace period before force kill (default 5000ms)
319
+ * @returns {Promise<boolean>} True if killed successfully
320
+ */
321
+ export function killJob(childProcess, gracePeriodMs = 5000) {
322
+ return new Promise((resolve) => {
323
+ if (!childProcess || childProcess.killed) {
324
+ resolve(true);
325
+ return;
326
+ }
327
+
328
+ // Try graceful termination of the entire process group first
329
+ try {
330
+ process.kill(-childProcess.pid, 'SIGTERM');
331
+ } catch (err) {
332
+ childProcess.kill('SIGTERM');
333
+ }
334
+
335
+ // Force kill after grace period
336
+ const forceKillTimeout = setTimeout(() => {
337
+ try {
338
+ process.kill(-childProcess.pid, 'SIGKILL');
339
+ } catch (err) {
340
+ if (!childProcess.killed) {
341
+ childProcess.kill('SIGKILL');
342
+ }
343
+ }
344
+ }, gracePeriodMs);
345
+
346
+ childProcess.on('close', () => {
347
+ clearTimeout(forceKillTimeout);
348
+ resolve(true);
349
+ });
350
+
351
+ // Timeout if process doesn't close
352
+ setTimeout(() => {
353
+ try {
354
+ process.kill(-childProcess.pid, 'SIGKILL');
355
+ } catch (err) {
356
+ if (!childProcess.killed) {
357
+ childProcess.kill('SIGKILL');
358
+ }
359
+ }
360
+ resolve(false);
361
+ }, gracePeriodMs + 1000);
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Format duration in human-readable format
367
+ * @param {number} ms - Duration in milliseconds
368
+ * @returns {string} Formatted duration
369
+ */
370
+ export function formatDuration(ms) {
371
+ if (ms < 1000) {
372
+ return `${ms}ms`;
373
+ }
374
+ if (ms < 60000) {
375
+ return `${(ms / 1000).toFixed(1)}s`;
376
+ }
377
+ if (ms < 3600000) {
378
+ const minutes = Math.floor(ms / 60000);
379
+ const seconds = ((ms % 60000) / 1000).toFixed(0);
380
+ return `${minutes}m${seconds}s`;
381
+ }
382
+ const hours = Math.floor(ms / 3600000);
383
+ const minutes = Math.floor((ms % 3600000) / 60000);
384
+ return `${hours}h${minutes}m`;
385
+ }
386
+
387
+ /**
388
+ * Create an executor instance
389
+ * @param {object} options - Executor options
390
+ * @param {object} options.logger - Logger instance
391
+ * @returns {object} Executor instance with executeJob and executeJobWithRetry methods
392
+ */
393
+ export function createExecutor(options = {}) {
394
+ const { logger } = options;
395
+
396
+ return {
397
+ executeJob: (job, execOptions = {}) => executeJob(job, execOptions),
398
+ executeJobWithRetry: (job, execOptions = {}) => executeJobWithRetry(job, execOptions),
399
+ };
400
+ }
401
+
402
+ export default {
403
+ executeJob,
404
+ executeJobWithRetry,
405
+ killJob,
406
+ formatDuration,
407
+ ExecutionStatus,
408
+ createExecutor,
409
+ };