@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.
- package/README.md +98 -0
- package/bin/smart-panel-service.js +1074 -0
- package/bin/smart-panel.js +43 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/installers/base.d.ts +78 -0
- package/dist/installers/base.d.ts.map +1 -0
- package/dist/installers/base.js +5 -0
- package/dist/installers/base.js.map +1 -0
- package/dist/installers/index.d.ts +8 -0
- package/dist/installers/index.d.ts.map +1 -0
- package/dist/installers/index.js +16 -0
- package/dist/installers/index.js.map +1 -0
- package/dist/installers/linux.d.ts +32 -0
- package/dist/installers/linux.d.ts.map +1 -0
- package/dist/installers/linux.js +406 -0
- package/dist/installers/linux.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +55 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +26 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +57 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/system.d.ts +87 -0
- package/dist/utils/system.d.ts.map +1 -0
- package/dist/utils/system.js +211 -0
- package/dist/utils/system.js.map +1 -0
- package/dist/utils/version.d.ts +11 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +75 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +72 -0
- package/templates/environment.template +28 -0
- 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();
|