jm2 0.1.2 → 0.1.3

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/bin/jm2.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * jm2 CLI Entry Point
5
- *
5
+ *
6
6
  * Usage:
7
7
  * jm2 start Start the daemon
8
8
  * jm2 stop Stop the daemon
@@ -17,6 +17,8 @@
17
17
  * jm2 run <id|name> Run a job manually
18
18
  * jm2 logs <id|name> Show job logs
19
19
  * jm2 history <id|name> Show job history
20
+ * jm2 install Register JM2 to start on boot
21
+ * jm2 uninstall Unregister JM2 from system startup
20
22
  */
21
23
 
22
24
  import { runCli } from '../src/cli/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jm2",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Job Manager 2 - A simple yet powerful job scheduler combining cron and at functionality",
5
5
  "type": "module",
6
6
  "main": "src/cli/index.js",
@@ -0,0 +1,93 @@
1
+ /**
2
+ * JM2 install command
3
+ * Registers JM2 daemon to start on system boot
4
+ */
5
+
6
+ import {
7
+ installService,
8
+ getServiceStatus,
9
+ isElevated,
10
+ ServiceType,
11
+ getPlatform,
12
+ isPlatformSupported
13
+ } from '../../core/service.js';
14
+ import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
15
+
16
+ /**
17
+ * Execute the install command
18
+ * @param {object} options - Command options
19
+ * @param {boolean} options.user - Install for current user only
20
+ * @param {boolean} options.system - Install system-wide
21
+ * @returns {Promise<number>} Exit code
22
+ */
23
+ export async function installCommand(options = {}) {
24
+ // Validate platform support
25
+ if (!isPlatformSupported()) {
26
+ printError(`Service installation is not supported on platform: ${getPlatform()}`);
27
+ return 1;
28
+ }
29
+
30
+ // Determine installation type
31
+ const type = options.system ? ServiceType.SYSTEM : ServiceType.USER;
32
+
33
+ // Check current status
34
+ const status = getServiceStatus(type);
35
+ if (status.installed) {
36
+ printWarning(`JM2 service is already installed (${type} level).`);
37
+ printInfo(`Service status: ${status.running ? 'running' : 'stopped'}`);
38
+ printInfo('Use "jm2 uninstall" first if you want to reinstall.');
39
+ return 1;
40
+ }
41
+
42
+ // Show what we're doing
43
+ if (type === ServiceType.SYSTEM) {
44
+ printInfo('Installing JM2 service system-wide...');
45
+ if (!isElevated()) {
46
+ printWarning('Administrator/root privileges required for system-wide installation.');
47
+ printInfo('You will be prompted for elevation if needed.');
48
+ }
49
+ } else {
50
+ printInfo('Installing JM2 service for current user...');
51
+ }
52
+
53
+ // Perform installation
54
+ const result = await installService(type);
55
+
56
+ if (result.success) {
57
+ printSuccess(result.message);
58
+
59
+ // Show additional info
60
+ const newStatus = getServiceStatus(type);
61
+ if (newStatus.running) {
62
+ printInfo('Service is now running and will start automatically on boot.');
63
+ } else {
64
+ printWarning('Service installed but not running. You may need to start it manually.');
65
+ }
66
+
67
+ // Platform-specific notes
68
+ const platform = getPlatform();
69
+ if (platform === 'darwin') {
70
+ printInfo('macOS: Service configured via launchd');
71
+ } else if (platform === 'linux') {
72
+ printInfo('Linux: Service configured via systemd');
73
+ } else if (platform === 'win32') {
74
+ printInfo('Windows: Service configured via Service Control Manager');
75
+ }
76
+
77
+ return 0;
78
+ } else {
79
+ printError(result.message);
80
+
81
+ // Provide helpful guidance for common errors
82
+ if (result.message.includes('administrator') || result.message.includes('root') || result.message.includes('privileges')) {
83
+ if (type === ServiceType.SYSTEM) {
84
+ printInfo('Tip: Try installing without --system flag for user-level installation:');
85
+ printInfo(' jm2 install');
86
+ }
87
+ }
88
+
89
+ return 1;
90
+ }
91
+ }
92
+
93
+ export default installCommand;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * JM2 uninstall command
3
+ * Unregisters JM2 daemon from system startup
4
+ */
5
+
6
+ import {
7
+ uninstallService,
8
+ getServiceStatus,
9
+ isElevated,
10
+ ServiceType,
11
+ getPlatform,
12
+ isPlatformSupported
13
+ } from '../../core/service.js';
14
+ import { printSuccess, printError, printInfo, printWarning } from '../utils/output.js';
15
+ import { confirm } from '../utils/prompts.js';
16
+
17
+ /**
18
+ * Execute the uninstall command
19
+ * @param {object} options - Command options
20
+ * @param {boolean} options.user - Uninstall user-level registration
21
+ * @param {boolean} options.system - Uninstall system-wide registration
22
+ * @param {boolean} options.force - Skip confirmation prompt
23
+ * @returns {Promise<number>} Exit code
24
+ */
25
+ export async function uninstallCommand(options = {}) {
26
+ // Validate platform support
27
+ if (!isPlatformSupported()) {
28
+ printError(`Service uninstallation is not supported on platform: ${getPlatform()}`);
29
+ return 1;
30
+ }
31
+
32
+ // Determine installation type
33
+ const type = options.system ? ServiceType.SYSTEM : ServiceType.USER;
34
+
35
+ // Check current status
36
+ const status = getServiceStatus(type);
37
+ if (!status.installed) {
38
+ // Check if installed at the other level
39
+ const otherType = type === ServiceType.USER ? ServiceType.SYSTEM : ServiceType.USER;
40
+ const otherStatus = getServiceStatus(otherType);
41
+
42
+ if (otherStatus.installed) {
43
+ printWarning(`JM2 service is not installed at ${type} level, but is installed at ${otherType} level.`);
44
+ printInfo(`Use "jm2 uninstall --${otherType}" to remove it.`);
45
+ return 1;
46
+ }
47
+
48
+ printInfo(`JM2 service is not installed (${type} level).`);
49
+ return 0;
50
+ }
51
+
52
+ // Show what we're doing
53
+ if (type === ServiceType.SYSTEM) {
54
+ printInfo('Uninstalling JM2 service system-wide...');
55
+ if (!isElevated()) {
56
+ printWarning('Administrator/root privileges required for system-wide uninstallation.');
57
+ }
58
+ } else {
59
+ printInfo('Uninstalling JM2 service for current user...');
60
+ }
61
+
62
+ // Confirm if not forced
63
+ if (!options.force) {
64
+ const confirmed = await confirm(
65
+ `Are you sure you want to uninstall the JM2 service (${type} level)?`
66
+ );
67
+ if (!confirmed) {
68
+ printInfo('Uninstall cancelled.');
69
+ return 0;
70
+ }
71
+ }
72
+
73
+ // Perform uninstallation
74
+ const result = await uninstallService(type);
75
+
76
+ if (result.success) {
77
+ printSuccess(result.message);
78
+ printInfo('JM2 will no longer start automatically on boot.');
79
+ return 0;
80
+ } else {
81
+ printError(result.message);
82
+
83
+ // Provide helpful guidance for common errors
84
+ if (result.message.includes('administrator') || result.message.includes('root') || result.message.includes('privileges')) {
85
+ if (type === ServiceType.SYSTEM) {
86
+ printInfo('Tip: If you want to uninstall the user-level service instead:');
87
+ printInfo(' jm2 uninstall');
88
+ }
89
+ }
90
+
91
+ return 1;
92
+ }
93
+ }
94
+
95
+ export default uninstallCommand;
package/src/cli/index.js CHANGED
@@ -26,6 +26,8 @@ import { historyCommand } from './commands/history.js';
26
26
  import { flushCommand } from './commands/flush.js';
27
27
  import { exportCommand } from './commands/export.js';
28
28
  import { importCommand } from './commands/import.js';
29
+ import { installCommand } from './commands/install.js';
30
+ import { uninstallCommand } from './commands/uninstall.js';
29
31
 
30
32
  const __filename = fileURLToPath(import.meta.url);
31
33
  const __dirname = dirname(__filename);
@@ -271,6 +273,29 @@ export async function runCli() {
271
273
  process.exit(exitCode);
272
274
  });
273
275
 
276
+ // Install command
277
+ program
278
+ .command('install')
279
+ .description('Register JM2 daemon to start on system boot')
280
+ .option('--user', 'Install for current user only (default)', false)
281
+ .option('--system', 'Install system-wide (requires admin/root)', false)
282
+ .action(async (options) => {
283
+ const exitCode = await installCommand(options);
284
+ process.exit(exitCode);
285
+ });
286
+
287
+ // Uninstall command
288
+ program
289
+ .command('uninstall')
290
+ .description('Unregister JM2 daemon from system startup')
291
+ .option('--user', 'Uninstall user-level registration (default)', false)
292
+ .option('--system', 'Uninstall system-wide registration (requires admin/root)', false)
293
+ .option('-f, --force', 'Skip confirmation prompt', false)
294
+ .action(async (options) => {
295
+ const exitCode = await uninstallCommand(options);
296
+ process.exit(exitCode);
297
+ });
298
+
274
299
  // Parse command line arguments
275
300
  await program.parseAsync();
276
301
  }
@@ -0,0 +1,664 @@
1
+ /**
2
+ * Service management for JM2
3
+ * Handles install/uninstall of system service for auto-start on boot
4
+ * Supports macOS (launchd), Linux (systemd), and Windows (service)
5
+ */
6
+
7
+ import { homedir } from 'node:os';
8
+ import { join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import {
11
+ writeFileSync,
12
+ readFileSync,
13
+ existsSync,
14
+ unlinkSync,
15
+ mkdirSync,
16
+ constants
17
+ } from 'node:fs';
18
+ import { spawn, execSync } from 'node:child_process';
19
+ import { getDataDir, getLogsDir } from '../utils/paths.js';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ /**
25
+ * Service registration types
26
+ */
27
+ export const ServiceType = {
28
+ USER: 'user',
29
+ SYSTEM: 'system'
30
+ };
31
+
32
+ /**
33
+ * Supported platforms
34
+ */
35
+ export const Platform = {
36
+ DARWIN: 'darwin',
37
+ LINUX: 'linux',
38
+ WIN32: 'win32'
39
+ };
40
+
41
+ /**
42
+ * Check if running with elevated privileges (root/admin)
43
+ * @returns {boolean} True if running as admin/root
44
+ */
45
+ export function isElevated() {
46
+ if (process.platform === 'win32') {
47
+ // On Windows, check if we have admin privileges
48
+ try {
49
+ // Try to access a protected resource
50
+ execSync('net session', { stdio: 'ignore' });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ } else {
56
+ // On Unix systems, check uid
57
+ return process.getuid ? process.getuid() === 0 : false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Get the platform-specific service manager
63
+ * @returns {PlatformService} Platform service implementation
64
+ */
65
+ function getPlatformService() {
66
+ const platform = process.platform;
67
+
68
+ switch (platform) {
69
+ case Platform.DARWIN:
70
+ return new DarwinService();
71
+ case Platform.LINUX:
72
+ return new LinuxService();
73
+ case Platform.WIN32:
74
+ return new WindowsService();
75
+ default:
76
+ throw new Error(`Unsupported platform: ${platform}`);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get path to the jm2 executable
82
+ * @returns {string} Path to jm2 binary
83
+ */
84
+ function getJm2Path() {
85
+ // Use the bin/jm2.js entry point
86
+ const pkgPath = join(__dirname, '../../package.json');
87
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
88
+
89
+ if (pkg.bin && pkg.bin.jm2) {
90
+ return join(__dirname, '../..', pkg.bin.jm2);
91
+ }
92
+
93
+ return join(__dirname, '../../bin/jm2.js');
94
+ }
95
+
96
+ /**
97
+ * Get Node.js executable path
98
+ * @returns {string} Path to node executable
99
+ */
100
+ function getNodePath() {
101
+ return process.execPath;
102
+ }
103
+
104
+ /**
105
+ * Base class for platform-specific service management
106
+ */
107
+ class PlatformService {
108
+ /**
109
+ * Install the service
110
+ * @param {string} type - Service type (user or system)
111
+ * @returns {Promise<{success: boolean, message: string}>}
112
+ */
113
+ async install(type) {
114
+ throw new Error('Not implemented');
115
+ }
116
+
117
+ /**
118
+ * Uninstall the service
119
+ * @param {string} type - Service type (user or system)
120
+ * @returns {Promise<{success: boolean, message: string}>}
121
+ */
122
+ async uninstall(type) {
123
+ throw new Error('Not implemented');
124
+ }
125
+
126
+ /**
127
+ * Check if service is installed
128
+ * @param {string} type - Service type (user or system)
129
+ * @returns {boolean}
130
+ */
131
+ isInstalled(type) {
132
+ throw new Error('Not implemented');
133
+ }
134
+
135
+ /**
136
+ * Get service status
137
+ * @param {string} type - Service type (user or system)
138
+ * @returns {{installed: boolean, running: boolean}}
139
+ */
140
+ getStatus(type) {
141
+ throw new Error('Not implemented');
142
+ }
143
+ }
144
+
145
+ /**
146
+ * macOS launchd service implementation
147
+ */
148
+ class DarwinService extends PlatformService {
149
+ getPlistPath(type) {
150
+ if (type === ServiceType.USER) {
151
+ return join(homedir(), 'Library', 'LaunchAgents', 'com.jm2.daemon.plist');
152
+ } else {
153
+ return '/Library/LaunchDaemons/com.jm2.daemon.plist';
154
+ }
155
+ }
156
+
157
+ generatePlist(type) {
158
+ const jm2Path = getJm2Path();
159
+ const nodePath = getNodePath();
160
+ const dataDir = getDataDir();
161
+ const logDir = getLogsDir();
162
+
163
+ const label = 'com.jm2.daemon';
164
+ const stdoutPath = join(logDir, 'service-out.log');
165
+ const stderrPath = join(logDir, 'service-err.log');
166
+
167
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
168
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
169
+ <plist version="1.0">
170
+ <dict>
171
+ <key>Label</key>
172
+ <string>${label}</string>
173
+ <key>ProgramArguments</key>
174
+ <array>
175
+ <string>${nodePath}</string>
176
+ <string>${jm2Path}</string>
177
+ <string>start</string>
178
+ </array>
179
+ <key>RunAtLoad</key>
180
+ <true/>
181
+ <key>KeepAlive</key>
182
+ <true/>
183
+ <key>StandardOutPath</key>
184
+ <string>${stdoutPath}</string>
185
+ <key>StandardErrorPath</key>
186
+ <string>${stderrPath}</string>
187
+ <key>EnvironmentVariables</key>
188
+ <dict>
189
+ <key>JM2_DATA_DIR</key>
190
+ <string>${dataDir}</string>
191
+ </dict>
192
+ </dict>
193
+ </plist>`;
194
+
195
+ return plist;
196
+ }
197
+
198
+ async install(type) {
199
+ const plistPath = this.getPlistPath(type);
200
+
201
+ // Check if already installed
202
+ if (existsSync(plistPath)) {
203
+ return {
204
+ success: false,
205
+ message: `Service is already installed at ${plistPath}. Use uninstall first.`
206
+ };
207
+ }
208
+
209
+ // Check permissions for system install
210
+ if (type === ServiceType.SYSTEM && !isElevated()) {
211
+ return {
212
+ success: false,
213
+ message: 'System-wide installation requires administrator privileges. Run with sudo.'
214
+ };
215
+ }
216
+
217
+ try {
218
+ // Ensure directory exists
219
+ const dir = dirname(plistPath);
220
+ if (!existsSync(dir)) {
221
+ mkdirSync(dir, { recursive: true });
222
+ }
223
+
224
+ // Write plist file
225
+ const plist = this.generatePlist(type);
226
+ writeFileSync(plistPath, plist, 'utf8');
227
+
228
+ // Load the service
229
+ if (type === ServiceType.USER) {
230
+ execSync(`launchctl load ${plistPath}`, { stdio: 'inherit' });
231
+ execSync('launchctl start com.jm2.daemon', { stdio: 'inherit' });
232
+ } else {
233
+ execSync(`sudo launchctl load ${plistPath}`, { stdio: 'inherit' });
234
+ execSync('sudo launchctl start com.jm2.daemon', { stdio: 'inherit' });
235
+ }
236
+
237
+ return {
238
+ success: true,
239
+ message: `JM2 service installed successfully for ${type === ServiceType.USER ? 'current user' : 'system-wide'}.`
240
+ };
241
+ } catch (error) {
242
+ return {
243
+ success: false,
244
+ message: `Failed to install service: ${error.message}`
245
+ };
246
+ }
247
+ }
248
+
249
+ async uninstall(type) {
250
+ const plistPath = this.getPlistPath(type);
251
+
252
+ // Check if installed
253
+ if (!existsSync(plistPath)) {
254
+ return {
255
+ success: false,
256
+ message: `Service is not installed (${type} level).`
257
+ };
258
+ }
259
+
260
+ // Check permissions for system uninstall
261
+ if (type === ServiceType.SYSTEM && !isElevated()) {
262
+ return {
263
+ success: false,
264
+ message: 'System-wide uninstallation requires administrator privileges. Run with sudo.'
265
+ };
266
+ }
267
+
268
+ try {
269
+ // Unload the service
270
+ if (type === ServiceType.USER) {
271
+ execSync('launchctl stop com.jm2.daemon 2>/dev/null || true', { stdio: 'ignore' });
272
+ execSync(`launchctl unload ${plistPath} 2>/dev/null || true`, { stdio: 'ignore' });
273
+ } else {
274
+ execSync('sudo launchctl stop com.jm2.daemon 2>/dev/null || true', { stdio: 'ignore' });
275
+ execSync(`sudo launchctl unload ${plistPath} 2>/dev/null || true`, { stdio: 'ignore' });
276
+ }
277
+
278
+ // Remove plist file
279
+ unlinkSync(plistPath);
280
+
281
+ return {
282
+ success: true,
283
+ message: `JM2 service uninstalled successfully from ${type} level.`
284
+ };
285
+ } catch (error) {
286
+ return {
287
+ success: false,
288
+ message: `Failed to uninstall service: ${error.message}`
289
+ };
290
+ }
291
+ }
292
+
293
+ isInstalled(type) {
294
+ const plistPath = this.getPlistPath(type);
295
+ return existsSync(plistPath);
296
+ }
297
+
298
+ getStatus(type) {
299
+ const installed = this.isInstalled(type);
300
+ let running = false;
301
+
302
+ if (installed) {
303
+ try {
304
+ const result = execSync('launchctl list | grep com.jm2.daemon', {
305
+ encoding: 'utf8',
306
+ stdio: ['pipe', 'pipe', 'ignore']
307
+ });
308
+ running = result.includes('com.jm2.daemon');
309
+ } catch {
310
+ running = false;
311
+ }
312
+ }
313
+
314
+ return { installed, running };
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Linux systemd service implementation
320
+ */
321
+ class LinuxService extends PlatformService {
322
+ getServicePath(type) {
323
+ if (type === ServiceType.USER) {
324
+ return join(homedir(), '.config', 'systemd', 'user', 'jm2.service');
325
+ } else {
326
+ return '/etc/systemd/system/jm2.service';
327
+ }
328
+ }
329
+
330
+ generateService(type) {
331
+ const jm2Path = getJm2Path();
332
+ const nodePath = getNodePath();
333
+ const dataDir = getDataDir();
334
+ const logDir = getLogsDir();
335
+
336
+ const service = `[Unit]
337
+ Description=JM2 Job Manager Daemon
338
+ After=network.target
339
+
340
+ [Service]
341
+ Type=forking
342
+ ExecStart=${nodePath} ${jm2Path} start
343
+ ExecStop=${nodePath} ${jm2Path} stop
344
+ ExecReload=${nodePath} ${jm2Path} restart
345
+ Restart=always
346
+ RestartSec=10
347
+ Environment="JM2_DATA_DIR=${dataDir}"
348
+ StandardOutput=append:${join(logDir, 'service-out.log')}
349
+ StandardError=append:${join(logDir, 'service-err.log')}
350
+
351
+ [Install]
352
+ WantedBy=${type === ServiceType.USER ? 'default.target' : 'multi-user.target'}
353
+ `;
354
+
355
+ return service;
356
+ }
357
+
358
+ async install(type) {
359
+ const servicePath = this.getServicePath(type);
360
+
361
+ // Check if already installed
362
+ if (existsSync(servicePath)) {
363
+ return {
364
+ success: false,
365
+ message: `Service is already installed at ${servicePath}. Use uninstall first.`
366
+ };
367
+ }
368
+
369
+ // Check permissions for system install
370
+ if (type === ServiceType.SYSTEM && !isElevated()) {
371
+ return {
372
+ success: false,
373
+ message: 'System-wide installation requires root privileges. Run with sudo.'
374
+ };
375
+ }
376
+
377
+ try {
378
+ // Ensure directory exists
379
+ const dir = dirname(servicePath);
380
+ if (!existsSync(dir)) {
381
+ mkdirSync(dir, { recursive: true });
382
+ }
383
+
384
+ // Write service file
385
+ const service = this.generateService(type);
386
+ writeFileSync(servicePath, service, 'utf8');
387
+
388
+ // Reload systemd
389
+ if (type === ServiceType.USER) {
390
+ execSync('systemctl --user daemon-reload', { stdio: 'inherit' });
391
+ execSync('systemctl --user enable jm2', { stdio: 'inherit' });
392
+ execSync('systemctl --user start jm2', { stdio: 'inherit' });
393
+ } else {
394
+ execSync('systemctl daemon-reload', { stdio: 'inherit' });
395
+ execSync('systemctl enable jm2', { stdio: 'inherit' });
396
+ execSync('systemctl start jm2', { stdio: 'inherit' });
397
+ }
398
+
399
+ return {
400
+ success: true,
401
+ message: `JM2 service installed successfully for ${type === ServiceType.USER ? 'current user' : 'system-wide'}.`
402
+ };
403
+ } catch (error) {
404
+ return {
405
+ success: false,
406
+ message: `Failed to install service: ${error.message}`
407
+ };
408
+ }
409
+ }
410
+
411
+ async uninstall(type) {
412
+ const servicePath = this.getServicePath(type);
413
+
414
+ // Check if installed
415
+ if (!existsSync(servicePath)) {
416
+ return {
417
+ success: false,
418
+ message: `Service is not installed (${type} level).`
419
+ };
420
+ }
421
+
422
+ // Check permissions for system uninstall
423
+ if (type === ServiceType.SYSTEM && !isElevated()) {
424
+ return {
425
+ success: false,
426
+ message: 'System-wide uninstallation requires root privileges. Run with sudo.'
427
+ };
428
+ }
429
+
430
+ try {
431
+ // Stop and disable the service
432
+ if (type === ServiceType.USER) {
433
+ execSync('systemctl --user stop jm2 2>/dev/null || true', { stdio: 'ignore' });
434
+ execSync('systemctl --user disable jm2 2>/dev/null || true', { stdio: 'ignore' });
435
+ } else {
436
+ execSync('systemctl stop jm2 2>/dev/null || true', { stdio: 'ignore' });
437
+ execSync('systemctl disable jm2 2>/dev/null || true', { stdio: 'ignore' });
438
+ }
439
+
440
+ // Remove service file
441
+ unlinkSync(servicePath);
442
+
443
+ // Reload systemd
444
+ if (type === ServiceType.USER) {
445
+ execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
446
+ } else {
447
+ execSync('systemctl daemon-reload', { stdio: 'ignore' });
448
+ }
449
+
450
+ return {
451
+ success: true,
452
+ message: `JM2 service uninstalled successfully from ${type} level.`
453
+ };
454
+ } catch (error) {
455
+ return {
456
+ success: false,
457
+ message: `Failed to uninstall service: ${error.message}`
458
+ };
459
+ }
460
+ }
461
+
462
+ isInstalled(type) {
463
+ const servicePath = this.getServicePath(type);
464
+ return existsSync(servicePath);
465
+ }
466
+
467
+ getStatus(type) {
468
+ const installed = this.isInstalled(type);
469
+ let running = false;
470
+
471
+ if (installed) {
472
+ try {
473
+ const result = execSync(
474
+ type === ServiceType.USER ? 'systemctl --user is-active jm2' : 'systemctl is-active jm2',
475
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
476
+ );
477
+ running = result.trim() === 'active';
478
+ } catch {
479
+ running = false;
480
+ }
481
+ }
482
+
483
+ return { installed, running };
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Windows service implementation
489
+ * Uses a simple batch wrapper approach for now
490
+ */
491
+ class WindowsService extends PlatformService {
492
+ getServiceName() {
493
+ return 'JM2Daemon';
494
+ }
495
+
496
+ getWrapperPath() {
497
+ return join(getDataDir(), 'jm2-service.bat');
498
+ }
499
+
500
+ generateWrapper() {
501
+ const jm2Path = getJm2Path();
502
+ const nodePath = getNodePath();
503
+ const dataDir = getDataDir();
504
+
505
+ const wrapper = `@echo off
506
+ set JM2_DATA_DIR=${dataDir}
507
+ "${nodePath}" "${jm2Path}" start
508
+ `;
509
+
510
+ return wrapper;
511
+ }
512
+
513
+ async install(type) {
514
+ // Windows services require admin privileges
515
+ if (!isElevated()) {
516
+ return {
517
+ success: false,
518
+ message: 'Windows service installation requires administrator privileges. Run as Administrator.'
519
+ };
520
+ }
521
+
522
+ try {
523
+ // Create wrapper script
524
+ const wrapperPath = this.getWrapperPath();
525
+ const wrapper = this.generateWrapper();
526
+ writeFileSync(wrapperPath, wrapper, 'utf8');
527
+
528
+ // Install service using sc.exe
529
+ const serviceName = this.getServiceName();
530
+ const binPath = `cmd.exe /c "${wrapperPath}"`;
531
+
532
+ execSync(`sc create "${serviceName}" binPath= "${binPath}" start= auto`, {
533
+ stdio: 'inherit'
534
+ });
535
+
536
+ // Start the service
537
+ execSync(`sc start "${serviceName}"`, { stdio: 'inherit' });
538
+
539
+ return {
540
+ success: true,
541
+ message: 'JM2 service installed successfully.'
542
+ };
543
+ } catch (error) {
544
+ return {
545
+ success: false,
546
+ message: `Failed to install service: ${error.message}`
547
+ };
548
+ }
549
+ }
550
+
551
+ async uninstall(type) {
552
+ // Windows service removal requires admin privileges
553
+ if (!isElevated()) {
554
+ return {
555
+ success: false,
556
+ message: 'Windows service uninstallation requires administrator privileges. Run as Administrator.'
557
+ };
558
+ }
559
+
560
+ try {
561
+ const serviceName = this.getServiceName();
562
+
563
+ // Stop the service first
564
+ try {
565
+ execSync(`sc stop "${serviceName}"`, { stdio: 'ignore' });
566
+ } catch {
567
+ // Service might not be running
568
+ }
569
+
570
+ // Delete the service
571
+ execSync(`sc delete "${serviceName}"`, { stdio: 'inherit' });
572
+
573
+ // Remove wrapper script
574
+ const wrapperPath = this.getWrapperPath();
575
+ if (existsSync(wrapperPath)) {
576
+ unlinkSync(wrapperPath);
577
+ }
578
+
579
+ return {
580
+ success: true,
581
+ message: 'JM2 service uninstalled successfully.'
582
+ };
583
+ } catch (error) {
584
+ return {
585
+ success: false,
586
+ message: `Failed to uninstall service: ${error.message}`
587
+ };
588
+ }
589
+ }
590
+
591
+ isInstalled(type) {
592
+ try {
593
+ const serviceName = this.getServiceName();
594
+ execSync(`sc query "${serviceName}"`, { stdio: 'ignore' });
595
+ return true;
596
+ } catch {
597
+ return false;
598
+ }
599
+ }
600
+
601
+ getStatus(type) {
602
+ const installed = this.isInstalled(type);
603
+ let running = false;
604
+
605
+ if (installed) {
606
+ try {
607
+ const serviceName = this.getServiceName();
608
+ const result = execSync(`sc query "${serviceName}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
609
+ running = result.includes('RUNNING');
610
+ } catch {
611
+ running = false;
612
+ }
613
+ }
614
+
615
+ return { installed, running };
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Install JM2 as a system service
621
+ * @param {string} type - Service type: 'user' or 'system'
622
+ * @returns {Promise<{success: boolean, message: string}>}
623
+ */
624
+ export async function installService(type = ServiceType.USER) {
625
+ const service = getPlatformService();
626
+ return service.install(type);
627
+ }
628
+
629
+ /**
630
+ * Uninstall JM2 system service
631
+ * @param {string} type - Service type: 'user' or 'system'
632
+ * @returns {Promise<{success: boolean, message: string}>}
633
+ */
634
+ export async function uninstallService(type = ServiceType.USER) {
635
+ const service = getPlatformService();
636
+ return service.uninstall(type);
637
+ }
638
+
639
+ /**
640
+ * Check if service is installed
641
+ * @param {string} type - Service type: 'user' or 'system'
642
+ * @returns {{installed: boolean, running: boolean}}
643
+ */
644
+ export function getServiceStatus(type = ServiceType.USER) {
645
+ const service = getPlatformService();
646
+ return service.getStatus(type);
647
+ }
648
+
649
+ /**
650
+ * Get current platform
651
+ * @returns {string} Platform identifier
652
+ */
653
+ export function getPlatform() {
654
+ return process.platform;
655
+ }
656
+
657
+ /**
658
+ * Check if platform is supported for service installation
659
+ * @returns {boolean}
660
+ */
661
+ export function isPlatformSupported() {
662
+ const platform = process.platform;
663
+ return Object.values(Platform).includes(platform);
664
+ }