@fastybird/smart-panel 0.1.0-alpha.5

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 (42) hide show
  1. package/README.md +98 -0
  2. package/bin/smart-panel-service.js +1074 -0
  3. package/bin/smart-panel.js +43 -0
  4. package/dist/index.d.ts +6 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +6 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/installers/base.d.ts +78 -0
  9. package/dist/installers/base.d.ts.map +1 -0
  10. package/dist/installers/base.js +5 -0
  11. package/dist/installers/base.js.map +1 -0
  12. package/dist/installers/index.d.ts +8 -0
  13. package/dist/installers/index.d.ts.map +1 -0
  14. package/dist/installers/index.js +16 -0
  15. package/dist/installers/index.js.map +1 -0
  16. package/dist/installers/linux.d.ts +32 -0
  17. package/dist/installers/linux.d.ts.map +1 -0
  18. package/dist/installers/linux.js +406 -0
  19. package/dist/installers/linux.js.map +1 -0
  20. package/dist/utils/index.d.ts +5 -0
  21. package/dist/utils/index.d.ts.map +1 -0
  22. package/dist/utils/index.js +5 -0
  23. package/dist/utils/index.js.map +1 -0
  24. package/dist/utils/logger.d.ts +17 -0
  25. package/dist/utils/logger.d.ts.map +1 -0
  26. package/dist/utils/logger.js +55 -0
  27. package/dist/utils/logger.js.map +1 -0
  28. package/dist/utils/paths.d.ts +26 -0
  29. package/dist/utils/paths.d.ts.map +1 -0
  30. package/dist/utils/paths.js +57 -0
  31. package/dist/utils/paths.js.map +1 -0
  32. package/dist/utils/system.d.ts +87 -0
  33. package/dist/utils/system.d.ts.map +1 -0
  34. package/dist/utils/system.js +211 -0
  35. package/dist/utils/system.js.map +1 -0
  36. package/dist/utils/version.d.ts +11 -0
  37. package/dist/utils/version.d.ts.map +1 -0
  38. package/dist/utils/version.js +75 -0
  39. package/dist/utils/version.js.map +1 -0
  40. package/package.json +72 -0
  41. package/templates/environment.template +28 -0
  42. package/templates/systemd/smart-panel.service +31 -0
@@ -0,0 +1,1074 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Smart Panel Service CLI - Service management for Linux
5
+ *
6
+ * Commands:
7
+ * install - Install Smart Panel as a systemd service
8
+ * uninstall - Remove the systemd service
9
+ * start - Start the service
10
+ * stop - Stop the service
11
+ * restart - Restart the service
12
+ * status - Show service status
13
+ * logs - View service logs
14
+ * update - Update to latest version
15
+ */
16
+
17
+ import { Command } from 'commander';
18
+ import chalk from 'chalk';
19
+ import ora from 'ora';
20
+ import { createRequire } from 'node:module';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { existsSync, readFileSync } from 'node:fs';
24
+ import { execFileSync } from 'node:child_process';
25
+
26
+ const __filename = fileURLToPath(import.meta.url);
27
+ const __dirname = dirname(__filename);
28
+
29
+ // Import from compiled dist
30
+ const distPath = join(__dirname, '..', 'dist');
31
+ const { getInstaller } = await import(join(distPath, 'installers', 'index.js'));
32
+ const { logger, isRoot, hasSystemd, getArch, getDistroInfo, compareSemver } = await import(join(distPath, 'utils', 'index.js'));
33
+
34
+ // Get package version
35
+ const packageJsonPath = join(__dirname, '..', 'package.json');
36
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
37
+ const version = packageJson.version;
38
+
39
+ const program = new Command();
40
+
41
+ program
42
+ .name('smart-panel-service')
43
+ .description('FastyBird Smart Panel service management CLI')
44
+ .version(version);
45
+
46
+ // ============================================================================
47
+ // Install Command
48
+ // ============================================================================
49
+ program
50
+ .command('install')
51
+ .description('Install Smart Panel as a systemd service')
52
+ .option('-p, --port <port>', 'HTTP port for the backend', '3000')
53
+ .option('-u, --user <user>', 'System user for the service', 'smart-panel')
54
+ .option('-d, --data-dir <path>', 'Data directory path', '/var/lib/smart-panel')
55
+ .option('--admin-username <username>', 'Create admin user with this username')
56
+ .option('--admin-password <password>', 'Admin user password (requires --admin-username)')
57
+ .option('--no-start', 'Do not start the service after installation')
58
+ .action(async (options) => {
59
+ console.log();
60
+ console.log(chalk.bold.cyan(' FastyBird Smart Panel Installer'));
61
+ console.log(chalk.gray(' ─────────────────────────────────'));
62
+ console.log();
63
+
64
+ // Check root
65
+ if (!isRoot()) {
66
+ logger.error('This command must be run as root. Please use sudo.');
67
+ process.exit(1);
68
+ }
69
+
70
+ // Check systemd
71
+ if (!hasSystemd()) {
72
+ logger.error('systemd is required but not detected on this system.');
73
+ process.exit(1);
74
+ }
75
+
76
+ const installer = getInstaller();
77
+ const spinner = ora();
78
+
79
+ try {
80
+ // Show system info
81
+ const distro = getDistroInfo();
82
+ const arch = getArch();
83
+ logger.info(`System: ${distro?.name || 'Linux'} ${distro?.version || ''} (${arch})`);
84
+ logger.info(`Node.js: ${process.version}`);
85
+ console.log();
86
+
87
+ // Check prerequisites
88
+ spinner.start('Checking prerequisites...');
89
+ const errors = await installer.checkPrerequisites();
90
+ if (errors.length > 0) {
91
+ spinner.fail('Prerequisites check failed');
92
+ errors.forEach((err) => logger.error(err));
93
+ process.exit(1);
94
+ }
95
+ spinner.succeed('Prerequisites check passed');
96
+
97
+ // Check for port conflicts
98
+ const targetPort = parseInt(options.port, 10);
99
+ try {
100
+ const ssOutput = execFileSync('ss', ['-tlnp'], { encoding: 'utf-8' });
101
+ const portInUse = ssOutput.split('\n').some((line) => line.includes(`:${targetPort} `));
102
+ if (portInUse) {
103
+ logger.warning(`Port ${targetPort} is already in use by another process`);
104
+ logger.info('Use --port to specify a different port, or stop the conflicting service');
105
+ }
106
+ } catch {
107
+ // Ignore - ss may not be available
108
+ }
109
+
110
+ // Validate admin options
111
+ if (options.adminUsername && !options.adminPassword) {
112
+ spinner.fail('--admin-password is required when using --admin-username');
113
+ process.exit(1);
114
+ }
115
+ if (options.adminPassword && !options.adminUsername) {
116
+ spinner.fail('--admin-username is required when using --admin-password');
117
+ process.exit(1);
118
+ }
119
+
120
+ // Install
121
+ spinner.start('Installing Smart Panel service...');
122
+
123
+ await installer.install({
124
+ user: options.user,
125
+ dataDir: options.dataDir,
126
+ port: parseInt(options.port, 10),
127
+ noStart: !options.start,
128
+ adminUsername: options.adminUsername,
129
+ adminPassword: options.adminPassword,
130
+ });
131
+
132
+ spinner.succeed('Smart Panel service installed');
133
+
134
+ // Show admin user creation status
135
+ if (options.adminUsername) {
136
+ logger.success(`Admin user '${options.adminUsername}' created`);
137
+ }
138
+
139
+ // Show success message
140
+ console.log();
141
+ logger.success('Installation complete!');
142
+ console.log();
143
+
144
+ if (options.start) {
145
+ logger.info(`Smart Panel is now running on port ${options.port}`);
146
+ logger.info(`Access the admin UI at: ${chalk.cyan(`http://localhost:${options.port}`)}`);
147
+ } else {
148
+ logger.info('Service installed but not started. Run:');
149
+ logger.info(` ${chalk.cyan('sudo smart-panel-service start')}`);
150
+ }
151
+
152
+ // Show first-time setup hint if admin user wasn't created
153
+ if (!options.adminUsername) {
154
+ console.log();
155
+ logger.info('First-time setup:');
156
+ console.log(chalk.gray(' Open the admin UI in your browser to create an admin account.'));
157
+ console.log(chalk.gray(' Or run: sudo smart-panel auth:onboarding <username> <password>'));
158
+ }
159
+
160
+ console.log();
161
+ logger.info('Useful commands:');
162
+ console.log(chalk.gray(' sudo smart-panel-service status - Check service status'));
163
+ console.log(chalk.gray(' sudo smart-panel-service logs -f - View live logs'));
164
+ console.log(chalk.gray(' sudo smart-panel-service restart - Restart the service'));
165
+ console.log();
166
+ } catch (error) {
167
+ spinner.fail('Installation failed');
168
+ logger.error(error instanceof Error ? error.message : String(error));
169
+ process.exit(1);
170
+ }
171
+ });
172
+
173
+ // ============================================================================
174
+ // Uninstall Command
175
+ // ============================================================================
176
+ program
177
+ .command('uninstall')
178
+ .description('Remove the Smart Panel systemd service')
179
+ .option('--keep-data', 'Keep the data directory')
180
+ .option('-f, --force', 'Skip confirmation prompts')
181
+ .action(async (options) => {
182
+ console.log();
183
+
184
+ if (!isRoot()) {
185
+ logger.error('This command must be run as root. Please use sudo.');
186
+ process.exit(1);
187
+ }
188
+
189
+ const installer = getInstaller();
190
+ const spinner = ora();
191
+
192
+ // Get installed config to show correct paths in warning
193
+ const config = installer.getInstalledConfig();
194
+ const dataDir = config.dataDir || '/var/lib/smart-panel';
195
+
196
+ // Confirmation
197
+ if (!options.force) {
198
+ logger.warning('This will remove the Smart Panel service.');
199
+ if (!options.keepData) {
200
+ logger.warning(`All data in ${dataDir} will be deleted!`);
201
+ }
202
+ console.log();
203
+ logger.info('Use --force to skip this confirmation, or --keep-data to preserve data.');
204
+ console.log();
205
+
206
+ // Simple confirmation via readline
207
+ const readline = await import('node:readline');
208
+ const rl = readline.createInterface({
209
+ input: process.stdin,
210
+ output: process.stdout,
211
+ });
212
+
213
+ const answer = await new Promise((resolve) => {
214
+ rl.question(chalk.yellow('Are you sure you want to continue? (yes/no): '), resolve);
215
+ });
216
+ rl.close();
217
+
218
+ if (answer !== 'yes' && answer !== 'y') {
219
+ logger.info('Uninstall cancelled.');
220
+ process.exit(0);
221
+ }
222
+ }
223
+
224
+ try {
225
+ spinner.start('Uninstalling Smart Panel service...');
226
+
227
+ await installer.uninstall({
228
+ keepData: options.keepData || false,
229
+ force: options.force || false,
230
+ });
231
+
232
+ spinner.succeed('Smart Panel service uninstalled');
233
+ console.log();
234
+
235
+ if (options.keepData) {
236
+ logger.info(`Data directory preserved at ${dataDir}`);
237
+ }
238
+
239
+ logger.success('Uninstall complete!');
240
+ console.log();
241
+ } catch (error) {
242
+ spinner.fail('Uninstall failed');
243
+ logger.error(error instanceof Error ? error.message : String(error));
244
+ process.exit(1);
245
+ }
246
+ });
247
+
248
+ // ============================================================================
249
+ // Start Command
250
+ // ============================================================================
251
+ program
252
+ .command('start')
253
+ .description('Start the Smart Panel service')
254
+ .action(async () => {
255
+ if (!isRoot()) {
256
+ logger.error('This command must be run as root. Please use sudo.');
257
+ process.exit(1);
258
+ }
259
+
260
+ const installer = getInstaller();
261
+ const spinner = ora('Starting Smart Panel service...').start();
262
+
263
+ try {
264
+ await installer.start();
265
+ spinner.succeed('Smart Panel service started');
266
+ } catch (error) {
267
+ spinner.fail('Failed to start service');
268
+ logger.error(error instanceof Error ? error.message : String(error));
269
+ process.exit(1);
270
+ }
271
+ });
272
+
273
+ // ============================================================================
274
+ // Stop Command
275
+ // ============================================================================
276
+ program
277
+ .command('stop')
278
+ .description('Stop the Smart Panel service')
279
+ .action(async () => {
280
+ if (!isRoot()) {
281
+ logger.error('This command must be run as root. Please use sudo.');
282
+ process.exit(1);
283
+ }
284
+
285
+ const installer = getInstaller();
286
+ const spinner = ora('Stopping Smart Panel service...').start();
287
+
288
+ try {
289
+ await installer.stop();
290
+ spinner.succeed('Smart Panel service stopped');
291
+ } catch (error) {
292
+ spinner.fail('Failed to stop service');
293
+ logger.error(error instanceof Error ? error.message : String(error));
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ // ============================================================================
299
+ // Restart Command
300
+ // ============================================================================
301
+ program
302
+ .command('restart')
303
+ .description('Restart the Smart Panel service')
304
+ .action(async () => {
305
+ if (!isRoot()) {
306
+ logger.error('This command must be run as root. Please use sudo.');
307
+ process.exit(1);
308
+ }
309
+
310
+ const installer = getInstaller();
311
+ const spinner = ora('Restarting Smart Panel service...').start();
312
+
313
+ try {
314
+ await installer.restart();
315
+ spinner.succeed('Smart Panel service restarted');
316
+ } catch (error) {
317
+ spinner.fail('Failed to restart service');
318
+ logger.error(error instanceof Error ? error.message : String(error));
319
+ process.exit(1);
320
+ }
321
+ });
322
+
323
+ // ============================================================================
324
+ // Status Command
325
+ // ============================================================================
326
+ program
327
+ .command('status')
328
+ .description('Show Smart Panel service status')
329
+ .option('--json', 'Output as JSON')
330
+ .action(async (options) => {
331
+ const installer = getInstaller();
332
+
333
+ try {
334
+ const status = await installer.status();
335
+
336
+ if (options.json) {
337
+ console.log(JSON.stringify(status, null, 2));
338
+ return;
339
+ }
340
+
341
+ console.log();
342
+ console.log(chalk.bold(' Smart Panel Service Status'));
343
+ console.log(chalk.gray(' ──────────────────────────'));
344
+ console.log();
345
+
346
+ // Installed
347
+ console.log(
348
+ ' Installed:',
349
+ status.installed ? chalk.green('Yes') : chalk.red('No')
350
+ );
351
+
352
+ if (!status.installed) {
353
+ console.log();
354
+ logger.info('Service is not installed. Run: sudo smart-panel-service install');
355
+ console.log();
356
+ return;
357
+ }
358
+
359
+ // Running
360
+ console.log(
361
+ ' Running: ',
362
+ status.running ? chalk.green('Yes') : chalk.red('No')
363
+ );
364
+
365
+ // Enabled
366
+ console.log(
367
+ ' Enabled: ',
368
+ status.enabled ? chalk.green('Yes') : chalk.yellow('No')
369
+ );
370
+
371
+ // PID
372
+ if (status.pid) {
373
+ console.log(' PID: ', chalk.white(status.pid));
374
+ }
375
+
376
+ // Uptime
377
+ if (status.uptime !== undefined) {
378
+ const hours = Math.floor(status.uptime / 3600);
379
+ const minutes = Math.floor((status.uptime % 3600) / 60);
380
+ const seconds = status.uptime % 60;
381
+ console.log(
382
+ ' Uptime: ',
383
+ chalk.white(`${hours}h ${minutes}m ${seconds}s`)
384
+ );
385
+ }
386
+
387
+ // Memory
388
+ if (status.memoryMB !== undefined) {
389
+ console.log(' Memory: ', chalk.white(`${status.memoryMB} MB`));
390
+ }
391
+
392
+ console.log();
393
+
394
+ if (!status.running) {
395
+ logger.info('Service is not running. Start with: sudo smart-panel-service start');
396
+ }
397
+ console.log();
398
+ } catch (error) {
399
+ logger.error(error instanceof Error ? error.message : String(error));
400
+ process.exit(1);
401
+ }
402
+ });
403
+
404
+ // ============================================================================
405
+ // Logs Command
406
+ // ============================================================================
407
+ program
408
+ .command('logs')
409
+ .description('View Smart Panel service logs')
410
+ .option('-f, --follow', 'Follow log output')
411
+ .option('-n, --lines <n>', 'Number of lines to show', '50')
412
+ .option('--since <time>', 'Show logs since time (e.g., "1h", "2024-01-01")')
413
+ .action(async (options) => {
414
+ const installer = getInstaller();
415
+
416
+ try {
417
+ await installer.logs({
418
+ follow: options.follow || false,
419
+ lines: parseInt(options.lines, 10),
420
+ since: options.since,
421
+ });
422
+ } catch (error) {
423
+ logger.error(error instanceof Error ? error.message : String(error));
424
+ process.exit(1);
425
+ }
426
+ });
427
+
428
+ // ============================================================================
429
+ // Update Command
430
+ // ============================================================================
431
+ program
432
+ .command('update')
433
+ .description('Update Smart Panel server to the latest version')
434
+ .option('--version <version>', 'Update to a specific version')
435
+ .option('--beta', 'Update to the latest beta version')
436
+ .option('--check', 'Only check for updates without installing')
437
+ .option('-y, --yes', 'Skip confirmation prompts')
438
+ .action(async (options) => {
439
+ console.log();
440
+
441
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@fastybird/smart-panel';
442
+
443
+ // --check mode: just display version info
444
+ if (options.check) {
445
+ const spinner = ora('Checking for updates...').start();
446
+
447
+ try {
448
+ const currentVersion = packageJson.version;
449
+ const channel = options.beta ? 'beta' : 'latest';
450
+
451
+ const response = await fetch(NPM_REGISTRY_URL);
452
+ const data = await response.json();
453
+ const latestVersion = data['dist-tags']?.[channel];
454
+
455
+ spinner.stop();
456
+
457
+ console.log(chalk.bold(' Smart Panel Update Check'));
458
+ console.log(chalk.gray(' ────────────────────────'));
459
+ console.log();
460
+ console.log(' Current version:', chalk.white(currentVersion));
461
+ console.log(' Latest version: ', latestVersion ? chalk.white(latestVersion) : chalk.yellow('unknown'));
462
+
463
+ if (latestVersion && compareSemver(currentVersion, latestVersion) < 0) {
464
+ console.log();
465
+ logger.info(`Update available! Run ${chalk.cyan('sudo smart-panel-service update')} to install.`);
466
+ } else if (latestVersion) {
467
+ console.log();
468
+ logger.success('Server is up to date.');
469
+ }
470
+
471
+ console.log();
472
+ } catch (error) {
473
+ spinner.fail('Failed to check for updates');
474
+ logger.error(error instanceof Error ? error.message : String(error));
475
+ process.exit(1);
476
+ }
477
+
478
+ return;
479
+ }
480
+
481
+ if (!isRoot()) {
482
+ logger.error('This command must be run as root. Please use sudo.');
483
+ process.exit(1);
484
+ }
485
+
486
+ const installer = getInstaller();
487
+ const spinner = ora();
488
+ let wasRunning = false;
489
+
490
+ try {
491
+ // Check current status
492
+ const status = await installer.status();
493
+ wasRunning = status.running;
494
+
495
+ if (!status.installed) {
496
+ logger.error('Smart Panel is not installed. Run: sudo smart-panel-service install');
497
+ process.exit(1);
498
+ }
499
+
500
+ // Determine package tag
501
+ let tag = 'latest';
502
+ let versionArg = '';
503
+
504
+ if (options.version) {
505
+ versionArg = `@${options.version}`;
506
+ } else if (options.beta) {
507
+ tag = 'beta';
508
+ versionArg = '@beta';
509
+ }
510
+
511
+ // Check what version we'd update to
512
+ const currentVersion = packageJson.version;
513
+ let targetVersion = options.version || null;
514
+ const isExplicitVersion = !!targetVersion;
515
+
516
+ if (!targetVersion) {
517
+ try {
518
+ const response = await fetch(NPM_REGISTRY_URL);
519
+ const data = await response.json();
520
+ targetVersion = data['dist-tags']?.[tag];
521
+ } catch {
522
+ // Continue anyway
523
+ }
524
+ }
525
+
526
+ if (!isExplicitVersion && targetVersion && compareSemver(currentVersion, targetVersion) >= 0) {
527
+ logger.success('Server is already up to date.');
528
+ console.log();
529
+ return;
530
+ }
531
+
532
+ logger.info(`Updating Smart Panel${targetVersion ? ` to ${targetVersion}` : ''}...`);
533
+
534
+ // Confirmation
535
+ if (!options.yes) {
536
+ const readline = await import('node:readline');
537
+ const rl = readline.createInterface({
538
+ input: process.stdin,
539
+ output: process.stdout,
540
+ });
541
+
542
+ const answer = await new Promise((resolve) => {
543
+ rl.question(chalk.yellow('Do you want to proceed? (yes/no): '), resolve);
544
+ });
545
+ rl.close();
546
+
547
+ if (answer !== 'yes' && answer !== 'y') {
548
+ logger.info('Update cancelled.');
549
+ return;
550
+ }
551
+ }
552
+
553
+ console.log();
554
+
555
+ // Stop service if running
556
+ if (status.running) {
557
+ spinner.start('Stopping service...');
558
+ await installer.stop();
559
+ spinner.succeed('Service stopped');
560
+ }
561
+
562
+ // Update package
563
+ spinner.start('Updating packages...');
564
+ try {
565
+ const packageSpec = `@fastybird/smart-panel${versionArg}`;
566
+ // Use 'install' when a specific version/tag is requested, 'update' for latest
567
+ const npmCommand = versionArg ? 'install' : 'update';
568
+ execFileSync('npm', [npmCommand, packageSpec, '-g'], {
569
+ stdio: 'inherit',
570
+ });
571
+ spinner.succeed('Packages updated');
572
+ } catch {
573
+ spinner.fail('Failed to update packages');
574
+ throw new Error('npm update failed');
575
+ }
576
+
577
+ // Run migrations
578
+ spinner.start('Running database migrations...');
579
+ const config = installer.getInstalledConfig();
580
+ const dataDir = config.dataDir || '/var/lib/smart-panel';
581
+ await installer.runMigrations(dataDir);
582
+ spinner.succeed('Migrations complete');
583
+
584
+ // Start service if it was running before update
585
+ if (wasRunning) {
586
+ spinner.start('Starting service...');
587
+ await installer.start();
588
+ spinner.succeed('Service started');
589
+ }
590
+
591
+ console.log();
592
+ logger.success('Update complete!');
593
+
594
+ // Show new version
595
+ try {
596
+ const newPackageJson = JSON.parse(
597
+ readFileSync(packageJsonPath, 'utf-8')
598
+ );
599
+ logger.info(`Current version: ${newPackageJson.version}`);
600
+ } catch {
601
+ // Ignore
602
+ }
603
+
604
+ console.log();
605
+ } catch (error) {
606
+ spinner.fail('Update failed');
607
+ logger.error(error instanceof Error ? error.message : String(error));
608
+
609
+ // Try to restart service if it was running before update
610
+ if (wasRunning) {
611
+ try {
612
+ await installer.start();
613
+ logger.info('Service restarted');
614
+ } catch {
615
+ logger.warning('Failed to restart service. Please start manually.');
616
+ }
617
+ }
618
+
619
+ process.exit(1);
620
+ }
621
+ });
622
+
623
+ // ============================================================================
624
+ // Update Panel Command
625
+ // ============================================================================
626
+ program
627
+ .command('update-panel')
628
+ .description('Update the Smart Panel display app')
629
+ .option('--platform <platform>', 'Panel platform: flutter-pi-armv7, flutter-pi-arm64, elinux, linux, android')
630
+ .option('--version <version>', 'Install specific version')
631
+ .option('--beta', 'Install latest beta release')
632
+ .option('-d, --install-dir <dir>', 'Installation directory', '/opt/smart-panel-display')
633
+ .option('-y, --yes', 'Skip confirmation prompts')
634
+ .action(async (options) => {
635
+ console.log();
636
+
637
+ const GITHUB_API_URL = 'https://api.github.com/repos/FastyBird/smart-panel/releases';
638
+ const DISPLAY_SERVICE = 'smart-panel-display';
639
+ const installDir = options.installDir || '/opt/smart-panel-display';
640
+
641
+ // Detect platform
642
+ let platform = options.platform;
643
+
644
+ if (!platform) {
645
+ const arch = getArch();
646
+
647
+ if (existsSync('/proc/device-tree/model')) {
648
+ try {
649
+ const model = readFileSync('/proc/device-tree/model', 'utf-8').toLowerCase();
650
+ if (model.includes('raspberry')) {
651
+ platform = arch === 'arm64' ? 'flutter-pi-arm64' : 'flutter-pi-armv7';
652
+ }
653
+ } catch {
654
+ // Ignore
655
+ }
656
+ }
657
+
658
+ if (!platform) {
659
+ if (arch === 'arm64') {
660
+ platform = 'flutter-pi-arm64';
661
+ } else if (arch === 'armv7') {
662
+ platform = 'flutter-pi-armv7';
663
+ } else if (arch === 'x64') {
664
+ platform = 'elinux';
665
+ } else {
666
+ logger.error('Could not detect platform. Use --platform to specify.');
667
+ process.exit(1);
668
+ }
669
+ }
670
+ }
671
+
672
+ logger.info(`Platform: ${platform}`);
673
+
674
+ // Get release info
675
+ const spinner = ora('Checking for panel releases...').start();
676
+
677
+ try {
678
+ let releaseUrl = options.beta
679
+ ? `${GITHUB_API_URL}?per_page=10`
680
+ : options.version
681
+ ? `${GITHUB_API_URL}/tags/v${options.version.replace(/^v/, '')}`
682
+ : `${GITHUB_API_URL}/latest`;
683
+
684
+ const response = await fetch(releaseUrl, {
685
+ headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'FastyBird-SmartPanel' },
686
+ });
687
+
688
+ if (!response.ok) {
689
+ throw new Error(`GitHub API returned ${response.status}`);
690
+ }
691
+
692
+ let release;
693
+
694
+ if (options.beta) {
695
+ const releases = await response.json();
696
+ release = releases.find((r) => r.prerelease);
697
+
698
+ if (!release) {
699
+ spinner.fail('No beta release found');
700
+ process.exit(1);
701
+ }
702
+ } else {
703
+ release = await response.json();
704
+ }
705
+
706
+ // Asset pattern matching
707
+ const assetPatterns = {
708
+ 'flutter-pi-armv7': /smart-panel-display-armv7\.tar\.gz/,
709
+ 'flutter-pi-arm64': /smart-panel-display-arm64\.tar\.gz/,
710
+ elinux: /smart-panel-display-elinux-x64\.tar\.gz/,
711
+ linux: /smart-panel-display-linux-x64\.tar\.gz/,
712
+ android: /smart-panel-display\.apk/,
713
+ };
714
+
715
+ const pattern = assetPatterns[platform];
716
+
717
+ if (!pattern) {
718
+ spinner.fail(`Unsupported platform: ${platform}`);
719
+ process.exit(1);
720
+ }
721
+
722
+ const asset = release.assets?.find((a) => pattern.test(a.name));
723
+
724
+ if (!asset) {
725
+ spinner.fail(`No build found for platform '${platform}' in release ${release.tag_name}`);
726
+ process.exit(1);
727
+ }
728
+
729
+ const sizeMB = (asset.size / (1024 * 1024)).toFixed(1);
730
+ spinner.succeed(`Found: ${release.tag_name} - ${asset.name} (${sizeMB} MB)`);
731
+
732
+ // Confirmation
733
+ if (!options.yes) {
734
+ const readline = await import('node:readline');
735
+ const rl = readline.createInterface({
736
+ input: process.stdin,
737
+ output: process.stdout,
738
+ });
739
+
740
+ const answer = await new Promise((resolve) => {
741
+ rl.question(chalk.yellow(`Update panel (${platform}) to ${release.tag_name}? (yes/no): `), resolve);
742
+ });
743
+ rl.close();
744
+
745
+ if (answer !== 'yes' && answer !== 'y') {
746
+ logger.info('Update cancelled.');
747
+ return;
748
+ }
749
+ }
750
+
751
+ let serviceStopped = false;
752
+ if (platform === 'android') {
753
+ // Android: download and install via ADB
754
+ try {
755
+ execFileSync('which', ['adb'], { stdio: 'pipe' });
756
+ } catch {
757
+ logger.error('ADB is required. Install with: apt-get install android-tools-adb');
758
+ process.exit(1);
759
+ }
760
+
761
+ const tmpFile = '/tmp/smart-panel-display.apk';
762
+ const dlSpinner = ora('Downloading APK...').start();
763
+
764
+ const dlResponse = await fetch(asset.browser_download_url, {
765
+ headers: { 'User-Agent': 'FastyBird-SmartPanel' },
766
+ redirect: 'follow',
767
+ });
768
+
769
+ if (!dlResponse.ok || !dlResponse.body) {
770
+ dlSpinner.fail('Download failed');
771
+ process.exit(1);
772
+ }
773
+
774
+ const { createWriteStream: cws } = await import('node:fs');
775
+ const { Readable } = await import('node:stream');
776
+ const { pipeline } = await import('node:stream/promises');
777
+ const fileStream = cws(tmpFile);
778
+ const nodeStream = Readable.fromWeb(dlResponse.body);
779
+ await pipeline(nodeStream, fileStream);
780
+ dlSpinner.succeed('Downloaded');
781
+
782
+ const installSpinner = ora('Installing via ADB...').start();
783
+ execFileSync('adb', ['install', '-r', tmpFile], { stdio: 'inherit' });
784
+ installSpinner.succeed('APK installed');
785
+
786
+ try { execFileSync('rm', ['-f', tmpFile], { stdio: 'pipe' }); } catch {}
787
+ } else {
788
+ // Linux platforms: stop service, download, extract, restart
789
+ if (!isRoot()) {
790
+ logger.error('This command must be run as root. Please use sudo.');
791
+ process.exit(1);
792
+ }
793
+
794
+ // Stop display service
795
+ const stopSpinner = ora('Stopping display service...').start();
796
+ try {
797
+ execFileSync('systemctl', ['stop', DISPLAY_SERVICE], { stdio: 'pipe' });
798
+ stopSpinner.succeed('Display service stopped');
799
+ } catch {
800
+ stopSpinner.warn('Display service not running');
801
+ }
802
+ serviceStopped = true;
803
+
804
+ // Download
805
+ const dlSpinner = ora(`Downloading ${asset.name}...`).start();
806
+ const tmpFile = `/tmp/${asset.name}`;
807
+
808
+ const dlResponse = await fetch(asset.browser_download_url, {
809
+ headers: { 'User-Agent': 'FastyBird-SmartPanel' },
810
+ redirect: 'follow',
811
+ });
812
+
813
+ if (!dlResponse.ok || !dlResponse.body) {
814
+ dlSpinner.fail('Download failed');
815
+ // Try to restart
816
+ try { execFileSync('systemctl', ['start', DISPLAY_SERVICE], { stdio: 'pipe' }); } catch {}
817
+ process.exit(1);
818
+ }
819
+
820
+ const { createWriteStream: cws } = await import('node:fs');
821
+ const { Readable } = await import('node:stream');
822
+ const { pipeline } = await import('node:stream/promises');
823
+ const fileStream = cws(tmpFile);
824
+ const nodeStream = Readable.fromWeb(dlResponse.body);
825
+ await pipeline(nodeStream, fileStream);
826
+ dlSpinner.succeed('Downloaded');
827
+
828
+ // Extract
829
+ const extractSpinner = ora('Extracting...').start();
830
+ execFileSync('mkdir', ['-p', installDir], { stdio: 'pipe' });
831
+ execFileSync('tar', ['-xzf', tmpFile, '-C', installDir], { stdio: 'pipe' });
832
+
833
+ // Make binary executable
834
+ try {
835
+ execFileSync('chmod', ['+x', join(installDir, 'fastybird_smart_panel')], { stdio: 'pipe' });
836
+ } catch {}
837
+
838
+ extractSpinner.succeed('Extracted');
839
+
840
+ // Cleanup
841
+ try { execFileSync('rm', ['-f', tmpFile], { stdio: 'pipe' }); } catch {}
842
+
843
+ // Start display service
844
+ const startSpinner = ora('Starting display service...').start();
845
+ try {
846
+ execFileSync('systemctl', ['start', DISPLAY_SERVICE], { stdio: 'pipe' });
847
+ startSpinner.succeed('Display service started');
848
+ } catch {
849
+ startSpinner.warn('Could not start display service. Start manually: sudo systemctl start ' + DISPLAY_SERVICE);
850
+ }
851
+ }
852
+
853
+ console.log();
854
+ logger.success(`Panel (${platform}) updated to ${release.tag_name}!`);
855
+ console.log();
856
+ } catch (error) {
857
+ spinner.fail('Update failed');
858
+ logger.error(error instanceof Error ? error.message : String(error));
859
+ // Try to restart display service if it was stopped before the failure
860
+ if (serviceStopped) { try { execFileSync('systemctl', ['start', DISPLAY_SERVICE], { stdio: 'pipe' }); } catch {} }
861
+ process.exit(1);
862
+ }
863
+ });
864
+
865
+ // ============================================================================
866
+ // Doctor Command
867
+ // ============================================================================
868
+ program
869
+ .command('doctor')
870
+ .description('Diagnose system health and check for common issues')
871
+ .action(async () => {
872
+ console.log();
873
+ console.log(chalk.bold.cyan(' Smart Panel Doctor'));
874
+ console.log(chalk.gray(' ──────────────────'));
875
+ console.log();
876
+
877
+ const installer = getInstaller();
878
+ let issues = 0;
879
+ let warnings = 0;
880
+
881
+ const ok = (msg) => console.log(` ${chalk.green('✓')} ${msg}`);
882
+ const warn = (msg) => { console.log(` ${chalk.yellow('!')} ${msg}`); warnings++; };
883
+ const fail = (msg) => { console.log(` ${chalk.red('✗')} ${msg}`); issues++; };
884
+ const info = (msg) => console.log(` ${chalk.gray(msg)}`);
885
+
886
+ // 1. OS & Architecture
887
+ console.log(chalk.bold(' System'));
888
+ const distro = getDistroInfo();
889
+ const arch = getArch();
890
+ ok(`OS: ${distro?.name || 'Linux'} ${distro?.version || ''} (${arch})`);
891
+
892
+ if (process.platform !== 'linux') {
893
+ fail('Only Linux is supported');
894
+ } else {
895
+ ok('Platform: Linux');
896
+ }
897
+
898
+ // 2. Systemd
899
+ if (hasSystemd()) {
900
+ ok('systemd: available');
901
+ } else {
902
+ fail('systemd: not detected');
903
+ info('Smart Panel requires systemd for service management');
904
+ }
905
+
906
+ // 3. Node.js version
907
+ console.log();
908
+ console.log(chalk.bold(' Node.js'));
909
+ const nodeVersion = process.versions.node;
910
+ const nodeMajor = parseInt(nodeVersion.split('.')[0], 10);
911
+
912
+ if (nodeMajor >= 24) {
913
+ ok(`Node.js: v${nodeVersion} (>= 24 required)`);
914
+ } else {
915
+ fail(`Node.js: v${nodeVersion} (>= 24 required)`);
916
+ info('Update Node.js: https://nodejs.org/en/download/');
917
+ }
918
+
919
+ // 4. Disk space
920
+ console.log();
921
+ console.log(chalk.bold(' Disk Space'));
922
+ try {
923
+ const dfOutput = execFileSync('df', ['-BM', '/var/lib'], { encoding: 'utf-8' });
924
+ const lines = dfOutput.trim().split('\n');
925
+ if (lines.length >= 2) {
926
+ const parts = lines[1].split(/\s+/);
927
+ const availMB = parseInt(parts[3], 10);
928
+ if (availMB >= 500) {
929
+ ok(`Available: ${availMB} MB (>= 500 MB recommended)`);
930
+ } else if (availMB >= 200) {
931
+ warn(`Available: ${availMB} MB (>= 500 MB recommended)`);
932
+ info('Low disk space may cause issues during updates');
933
+ } else {
934
+ fail(`Available: ${availMB} MB (>= 500 MB recommended)`);
935
+ info('Insufficient disk space for reliable operation');
936
+ }
937
+ }
938
+ } catch {
939
+ warn('Could not check disk space');
940
+ }
941
+
942
+ // 5. Port check
943
+ console.log();
944
+ console.log(chalk.bold(' Network'));
945
+ const config = installer.getInstalledConfig();
946
+ const port = 3000; // Default port
947
+
948
+ try {
949
+ const envFile = '/etc/smart-panel/environment';
950
+ if (existsSync(envFile)) {
951
+ const envContent = readFileSync(envFile, 'utf-8');
952
+ const portMatch = envContent.match(/FB_BACKEND_PORT=(\d+)/);
953
+ if (portMatch) {
954
+ const configuredPort = parseInt(portMatch[1], 10);
955
+ try {
956
+ const ssOutput = execFileSync('ss', ['-tlnp'], { encoding: 'utf-8' });
957
+ const portInUse = ssOutput.split('\n').some((line) =>
958
+ line.includes(`:${configuredPort} `) && !line.includes('smart-panel') && !line.includes('node')
959
+ );
960
+ if (portInUse) {
961
+ warn(`Port ${configuredPort}: in use by another process`);
962
+ info('Another application may conflict with Smart Panel');
963
+ } else {
964
+ ok(`Port ${configuredPort}: available`);
965
+ }
966
+ } catch {
967
+ warn(`Port ${configuredPort}: could not verify`);
968
+ }
969
+ }
970
+ } else {
971
+ // Check default port
972
+ try {
973
+ const ssOutput = execFileSync('ss', ['-tlnp'], { encoding: 'utf-8' });
974
+ const portInUse = ssOutput.split('\n').some((line) => line.includes(`:${port} `));
975
+ if (portInUse) {
976
+ warn(`Port ${port} (default): already in use`);
977
+ info('Use --port flag during install to use a different port');
978
+ } else {
979
+ ok(`Port ${port} (default): available`);
980
+ }
981
+ } catch {
982
+ warn('Could not check port availability');
983
+ }
984
+ }
985
+ } catch {
986
+ warn('Could not check network configuration');
987
+ }
988
+
989
+ // 6. Service status
990
+ console.log();
991
+ console.log(chalk.bold(' Service'));
992
+ try {
993
+ const status = await installer.status();
994
+
995
+ if (status.installed) {
996
+ ok('Installed: yes');
997
+
998
+ if (status.running) {
999
+ ok('Running: yes');
1000
+ if (status.uptime !== undefined) {
1001
+ const hours = Math.floor(status.uptime / 3600);
1002
+ const minutes = Math.floor((status.uptime % 3600) / 60);
1003
+ ok(`Uptime: ${hours}h ${minutes}m`);
1004
+ }
1005
+ if (status.memoryMB !== undefined) {
1006
+ if (status.memoryMB > 512) {
1007
+ warn(`Memory: ${status.memoryMB} MB (high usage)`);
1008
+ } else {
1009
+ ok(`Memory: ${status.memoryMB} MB`);
1010
+ }
1011
+ }
1012
+ } else {
1013
+ warn('Running: no');
1014
+ info('Start with: sudo smart-panel-service start');
1015
+ }
1016
+
1017
+ if (status.enabled) {
1018
+ ok('Auto-start: enabled');
1019
+ } else {
1020
+ warn('Auto-start: disabled');
1021
+ info('Enable with: sudo systemctl enable smart-panel');
1022
+ }
1023
+ } else {
1024
+ warn('Service not installed');
1025
+ info('Install with: sudo smart-panel-service install');
1026
+ }
1027
+ } catch {
1028
+ warn('Could not check service status');
1029
+ }
1030
+
1031
+ // 7. Data directory permissions
1032
+ console.log();
1033
+ console.log(chalk.bold(' Data'));
1034
+ const dataDir = config?.dataDir || '/var/lib/smart-panel';
1035
+ if (existsSync(dataDir)) {
1036
+ ok(`Data directory: ${dataDir}`);
1037
+
1038
+ const dbPath = join(dataDir, 'data');
1039
+ if (existsSync(dbPath)) {
1040
+ ok('Database directory exists');
1041
+ } else {
1042
+ warn('Database directory missing');
1043
+ }
1044
+
1045
+ const configPath = join(dataDir, 'config');
1046
+ if (existsSync(configPath)) {
1047
+ ok('Config directory exists');
1048
+ } else {
1049
+ warn('Config directory missing');
1050
+ }
1051
+ } else {
1052
+ warn(`Data directory not found: ${dataDir}`);
1053
+ info('This is normal before first installation');
1054
+ }
1055
+
1056
+ // Summary
1057
+ console.log();
1058
+ console.log(chalk.gray(' ──────────────────'));
1059
+
1060
+ if (issues === 0 && warnings === 0) {
1061
+ console.log(` ${chalk.green.bold('All checks passed!')}`);
1062
+ } else if (issues === 0) {
1063
+ console.log(` ${chalk.yellow.bold(`${warnings} warning(s), no critical issues`)}`);
1064
+ } else {
1065
+ console.log(` ${chalk.red.bold(`${issues} issue(s)`)}${warnings > 0 ? chalk.yellow(`, ${warnings} warning(s)`) : ''}`);
1066
+ }
1067
+
1068
+ console.log();
1069
+
1070
+ process.exit(issues > 0 ? 1 : 0);
1071
+ });
1072
+
1073
+ // Parse and run
1074
+ program.parse();