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,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;