@lamalibre/create-portlama 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/index.js +81 -31
- package/src/lib/summary.js +72 -47
- package/src/tasks/panel.js +4 -3
- package/src/tasks/redeploy.js +236 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { nodeTasks } from './tasks/node.js';
|
|
|
11
11
|
import { mtlsTasks } from './tasks/mtls.js';
|
|
12
12
|
import { nginxTasks } from './tasks/nginx.js';
|
|
13
13
|
import { panelTasks } from './tasks/panel.js';
|
|
14
|
+
import { redeployTasks } from './tasks/redeploy.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Parse minimal CLI flags from process.argv.
|
|
@@ -24,6 +25,7 @@ function parseFlags() {
|
|
|
24
25
|
dev: args.includes('--dev'),
|
|
25
26
|
help: args.includes('--help') || args.includes('-h'),
|
|
26
27
|
yes: args.includes('--yes') || args.includes('-y'),
|
|
28
|
+
forceFull: args.includes('--force-full'),
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -92,6 +94,7 @@ ${b('FLAGS')}
|
|
|
92
94
|
${c('--yes')}, ${c('-y')} Skip the confirmation prompt
|
|
93
95
|
${c('--skip-harden')} Skip OS hardening (swap, UFW, fail2ban, SSH)
|
|
94
96
|
${c('--dev')} Allow private/non-routable IP addresses
|
|
97
|
+
${c('--force-full')} Run full installation even on existing installs
|
|
95
98
|
${c('--uninstall')} Show manual removal guide for Portlama
|
|
96
99
|
`);
|
|
97
100
|
process.exit(0);
|
|
@@ -266,10 +269,33 @@ async function detectExistingState() {
|
|
|
266
269
|
/**
|
|
267
270
|
* Print a confirmation banner and optionally wait for user input.
|
|
268
271
|
* @param {{ yes: boolean }} flags - Parsed CLI flags.
|
|
272
|
+
* @param {boolean} isRedeploy - Whether we are in redeploy mode.
|
|
269
273
|
* @param {{ portlamaExists: boolean, onboardingStatus: string | null, existingNginxSites: string[], port3100InUse: boolean, ufwActive: boolean, ufwRuleCount: number }} existingState - Detection results.
|
|
270
274
|
*/
|
|
271
|
-
async function confirmInstallation(flags, existingState) {
|
|
272
|
-
|
|
275
|
+
async function confirmInstallation(flags, isRedeploy, existingState) {
|
|
276
|
+
let banner;
|
|
277
|
+
|
|
278
|
+
if (isRedeploy) {
|
|
279
|
+
banner = `
|
|
280
|
+
${chalk.cyan.bold('┌─────────────────────────────────────────────────────────────┐')}
|
|
281
|
+
${chalk.cyan.bold('│')} ${chalk.white.bold('Portlama Panel Update')} ${chalk.cyan.bold('│')}
|
|
282
|
+
${chalk.cyan.bold('├─────────────────────────────────────────────────────────────┤')}
|
|
283
|
+
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
284
|
+
${chalk.cyan.bold('│')} Existing installation detected. Updating panel only. ${chalk.cyan.bold('│')}
|
|
285
|
+
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
286
|
+
${chalk.cyan.bold('│')} The following changes will be made: ${chalk.cyan.bold('│')}
|
|
287
|
+
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
288
|
+
${chalk.cyan.bold('│')} ${chalk.yellow('•')} Stop panel service ${chalk.cyan.bold('│')}
|
|
289
|
+
${chalk.cyan.bold('│')} ${chalk.yellow('•')} Update panel-server and panel-client files ${chalk.cyan.bold('│')}
|
|
290
|
+
${chalk.cyan.bold('│')} ${chalk.yellow('•')} Install updated dependencies ${chalk.cyan.bold('│')}
|
|
291
|
+
${chalk.cyan.bold('│')} ${chalk.yellow('•')} Restart panel service ${chalk.cyan.bold('│')}
|
|
292
|
+
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
293
|
+
${chalk.cyan.bold('│')} ${chalk.dim('OS, nginx, mTLS certs, and firewall are untouched.')} ${chalk.cyan.bold('│')}
|
|
294
|
+
${chalk.cyan.bold('│')} ${chalk.dim('Use --force-full to run the complete installer.')} ${chalk.cyan.bold('│')}
|
|
295
|
+
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
296
|
+
${chalk.cyan.bold('└─────────────────────────────────────────────────────────────┘')}`;
|
|
297
|
+
} else {
|
|
298
|
+
banner = `
|
|
273
299
|
${chalk.cyan.bold('┌─────────────────────────────────────────────────────────────┐')}
|
|
274
300
|
${chalk.cyan.bold('│')} ${chalk.white.bold('Portlama Installer')} ${chalk.cyan.bold('│')}
|
|
275
301
|
${chalk.cyan.bold('├─────────────────────────────────────────────────────────────┤')}
|
|
@@ -288,39 +314,42 @@ ${chalk.cyan.bold('│')}
|
|
|
288
314
|
${chalk.cyan.bold('│')} ${chalk.dim('Designed for a fresh Ubuntu 24.04 droplet.')} ${chalk.cyan.bold('│')}
|
|
289
315
|
${chalk.cyan.bold('│')} ${chalk.cyan.bold('│')}
|
|
290
316
|
${chalk.cyan.bold('└─────────────────────────────────────────────────────────────┘')}`;
|
|
317
|
+
}
|
|
291
318
|
|
|
292
319
|
console.log(banner);
|
|
293
320
|
|
|
294
|
-
// Display detection warnings below the banner
|
|
295
|
-
|
|
321
|
+
// Display detection warnings below the banner (only for full install)
|
|
322
|
+
if (!isRedeploy) {
|
|
323
|
+
const warnings = [];
|
|
296
324
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
if (existingState.portlamaExists) {
|
|
326
|
+
const status = existingState.onboardingStatus || 'UNKNOWN';
|
|
327
|
+
warnings.push(
|
|
328
|
+
`An existing Portlama installation was detected (onboarding: ${status}). Re-running will update the installation but preserve your configuration.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
303
331
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
332
|
+
if (existingState.existingNginxSites.length > 0) {
|
|
333
|
+
warnings.push(
|
|
334
|
+
`Existing nginx sites will be affected: ${existingState.existingNginxSites.join(', ')}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
309
337
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
if (existingState.port3100InUse) {
|
|
339
|
+
warnings.push('Port 3100 is currently in use. The panel may fail to start.');
|
|
340
|
+
}
|
|
313
341
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
342
|
+
if (existingState.ufwActive && existingState.ufwRuleCount > 0) {
|
|
343
|
+
warnings.push(
|
|
344
|
+
`Existing UFW firewall rules (${existingState.ufwRuleCount} rules) will be reset.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
319
347
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
348
|
+
if (warnings.length > 0) {
|
|
349
|
+
console.log('');
|
|
350
|
+
for (const warning of warnings) {
|
|
351
|
+
console.log(` ${chalk.yellow('!')} ${chalk.yellow(warning)}`);
|
|
352
|
+
}
|
|
324
353
|
}
|
|
325
354
|
}
|
|
326
355
|
|
|
@@ -461,11 +490,32 @@ export async function main() {
|
|
|
461
490
|
// Detect existing system state for the confirmation banner
|
|
462
491
|
const existingState = await detectExistingState();
|
|
463
492
|
|
|
464
|
-
//
|
|
465
|
-
|
|
493
|
+
// Determine mode: redeploy (fast update) or full install
|
|
494
|
+
const isRedeploy = existingState.portlamaExists && !flags.forceFull;
|
|
466
495
|
|
|
467
|
-
//
|
|
468
|
-
await
|
|
496
|
+
// Show confirmation banner and wait for user input
|
|
497
|
+
await confirmInstallation(flags, isRedeploy, existingState);
|
|
498
|
+
|
|
499
|
+
if (isRedeploy) {
|
|
500
|
+
// Fast path: only update panel files and restart
|
|
501
|
+
const redeployTaskList = new Listr(
|
|
502
|
+
[
|
|
503
|
+
{
|
|
504
|
+
title: 'Redeploying Portlama panel',
|
|
505
|
+
task: (_ctx, task) => redeployTasks(ctx, task),
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
{
|
|
509
|
+
renderer: 'default',
|
|
510
|
+
rendererOptions: { collapseSubtasks: false },
|
|
511
|
+
exitOnError: true,
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
await redeployTaskList.run();
|
|
515
|
+
} else {
|
|
516
|
+
// Full install path
|
|
517
|
+
await installTasks.run();
|
|
518
|
+
}
|
|
469
519
|
} catch (error) {
|
|
470
520
|
console.error('\n');
|
|
471
521
|
console.error(' ┌─────────────────────────────────────────────┐');
|
package/src/lib/summary.js
CHANGED
|
@@ -1,21 +1,88 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
|
|
4
|
+
// eslint-disable-next-line no-control-regex
|
|
5
|
+
const ansiPattern = new RegExp('\x1b\\[[0-9;]*m', 'g');
|
|
6
|
+
const stripAnsi = (str) => str.replace(ansiPattern, '');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Print an array of lines inside a bordered box with a title separator
|
|
10
|
+
* after the first 3 lines.
|
|
11
|
+
* @param {string[]} lines
|
|
12
|
+
*/
|
|
13
|
+
function printBox(lines) {
|
|
14
|
+
const maxLineWidth = Math.max(...lines.map((l) => stripAnsi(l).length));
|
|
15
|
+
const boxInnerWidth = Math.max(maxLineWidth + 2, 62);
|
|
16
|
+
|
|
17
|
+
const border = chalk.cyan;
|
|
18
|
+
const topBorder = border(`╔${'═'.repeat(boxInnerWidth)}╗`);
|
|
19
|
+
const bottomBorder = border(`╚${'═'.repeat(boxInnerWidth)}╝`);
|
|
20
|
+
const midBorder = border(`╠${'═'.repeat(boxInnerWidth)}╣`);
|
|
21
|
+
|
|
22
|
+
const padLine = (line) => {
|
|
23
|
+
const visibleLength = stripAnsi(line).length;
|
|
24
|
+
const padding = boxInnerWidth - visibleLength;
|
|
25
|
+
return `${border('║')}${line}${' '.repeat(Math.max(0, padding))}${border('║')}`;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(topBorder);
|
|
30
|
+
|
|
31
|
+
// Title section (first 3 lines: empty, title, empty)
|
|
32
|
+
for (let i = 0; i < 3; i++) {
|
|
33
|
+
console.log(padLine(lines[i]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Separator
|
|
37
|
+
console.log(midBorder);
|
|
38
|
+
|
|
39
|
+
// Remaining content lines
|
|
40
|
+
for (let i = 3; i < lines.length; i++) {
|
|
41
|
+
console.log(padLine(lines[i]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(bottomBorder);
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
|
|
4
48
|
/**
|
|
5
49
|
* Print the post-install summary box with all the information the user needs
|
|
6
50
|
* to download their client certificate, import it, and access the panel.
|
|
7
51
|
*
|
|
8
|
-
*
|
|
52
|
+
* In redeploy mode, prints a shorter summary since the user already has
|
|
53
|
+
* their certificate and knows how to access the panel.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ ip: string, pkiDir: string, installedVersion?: string, vendorVersion?: string }} ctx - Shared installer context.
|
|
9
56
|
*/
|
|
10
57
|
export async function printSummary(ctx) {
|
|
58
|
+
const ip = ctx.ip;
|
|
59
|
+
const panelUrl = `https://${ip}:9292`;
|
|
60
|
+
|
|
61
|
+
// Redeploy mode: short summary
|
|
62
|
+
if (ctx.installedVersion !== undefined) {
|
|
63
|
+
const lines = [
|
|
64
|
+
'',
|
|
65
|
+
` ${chalk.green.bold('Panel updated successfully!')}`,
|
|
66
|
+
'',
|
|
67
|
+
` ${chalk.white.bold('Version:')} ${chalk.dim(ctx.installedVersion || 'unknown')} ${chalk.white('→')} ${chalk.cyan(ctx.vendorVersion || 'unknown')}`,
|
|
68
|
+
'',
|
|
69
|
+
` ${chalk.white.bold('Panel:')} ${chalk.cyan.underline(panelUrl)}`,
|
|
70
|
+
'',
|
|
71
|
+
` ${chalk.dim('Your certificates, nginx config, and OS settings are unchanged.')}`,
|
|
72
|
+
` ${chalk.dim('Refresh your browser to see the updated panel.')}`,
|
|
73
|
+
'',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
printBox(lines);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Full install mode: complete summary with certificate instructions
|
|
11
81
|
const p12PasswordFile = `${ctx.pkiDir}/.p12-password`;
|
|
12
82
|
const p12Password = (await readFile(p12PasswordFile, 'utf8')).trim();
|
|
13
83
|
|
|
14
|
-
const ip = ctx.ip;
|
|
15
84
|
const scpCmd = `scp root@${ip}:${ctx.pkiDir}/client.p12 .`;
|
|
16
|
-
const panelUrl = `https://${ip}:9292`;
|
|
17
85
|
|
|
18
|
-
// Build content lines (without border)
|
|
19
86
|
const lines = [
|
|
20
87
|
'',
|
|
21
88
|
` ${chalk.green.bold('Portlama installed successfully!')}`,
|
|
@@ -55,47 +122,5 @@ export async function printSummary(ctx) {
|
|
|
55
122
|
'',
|
|
56
123
|
];
|
|
57
124
|
|
|
58
|
-
|
|
59
|
-
// We strip ANSI codes to measure actual character width.
|
|
60
|
-
// eslint-disable-next-line no-control-regex
|
|
61
|
-
const ansiPattern = new RegExp('\x1b\\[[0-9;]*m', 'g');
|
|
62
|
-
const stripAnsi = (str) => str.replace(ansiPattern, '');
|
|
63
|
-
const maxLineWidth = Math.max(...lines.map((l) => stripAnsi(l).length));
|
|
64
|
-
const boxInnerWidth = Math.max(maxLineWidth + 2, 62);
|
|
65
|
-
|
|
66
|
-
const border = chalk.cyan;
|
|
67
|
-
const topBorder = border(
|
|
68
|
-
`╔${'═'.repeat(boxInnerWidth)}╗`,
|
|
69
|
-
);
|
|
70
|
-
const bottomBorder = border(
|
|
71
|
-
`╚${'═'.repeat(boxInnerWidth)}╝`,
|
|
72
|
-
);
|
|
73
|
-
const midBorder = border(
|
|
74
|
-
`╠${'═'.repeat(boxInnerWidth)}╣`,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const padLine = (line) => {
|
|
78
|
-
const visibleLength = stripAnsi(line).length;
|
|
79
|
-
const padding = boxInnerWidth - visibleLength;
|
|
80
|
-
return `${border('║')}${line}${' '.repeat(Math.max(0, padding))}${border('║')}`;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
console.log('');
|
|
84
|
-
console.log(topBorder);
|
|
85
|
-
|
|
86
|
-
// Title section (first 3 lines: empty, title, empty)
|
|
87
|
-
for (let i = 0; i < 3; i++) {
|
|
88
|
-
console.log(padLine(lines[i]));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Separator
|
|
92
|
-
console.log(midBorder);
|
|
93
|
-
|
|
94
|
-
// Remaining content lines
|
|
95
|
-
for (let i = 3; i < lines.length; i++) {
|
|
96
|
-
console.log(padLine(lines[i]));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(bottomBorder);
|
|
100
|
-
console.log('');
|
|
125
|
+
printBox(lines);
|
|
101
126
|
}
|
package/src/tasks/panel.js
CHANGED
|
@@ -239,10 +239,11 @@ StandardError=journal
|
|
|
239
239
|
SyslogIdentifier=portlama-panel
|
|
240
240
|
|
|
241
241
|
# Security hardening
|
|
242
|
-
NoNewPrivileges
|
|
243
|
-
|
|
242
|
+
# Note: NoNewPrivileges is intentionally omitted — the panel needs sudo
|
|
243
|
+
# for provisioning (Chisel, Authelia, certbot, nginx, systemctl).
|
|
244
|
+
# Access is restricted via fine-grained sudoers rules in /etc/sudoers.d/portlama.
|
|
244
245
|
ProtectHome=true
|
|
245
|
-
ReadWritePaths=${configDir}
|
|
246
|
+
ReadWritePaths=${configDir} /var/www/portlama
|
|
246
247
|
PrivateTmp=true
|
|
247
248
|
|
|
248
249
|
[Install]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { writeFile, readFile, cp, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read the installed panel-server's package.json version, or null if not found.
|
|
10
|
+
* @param {string} installDir
|
|
11
|
+
* @returns {Promise<string | null>}
|
|
12
|
+
*/
|
|
13
|
+
async function getInstalledVersion(installDir) {
|
|
14
|
+
try {
|
|
15
|
+
const pkgPath = join(installDir, 'panel-server', 'package.json');
|
|
16
|
+
const raw = await readFile(pkgPath, 'utf8');
|
|
17
|
+
return JSON.parse(raw).version || null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the vendor (new) panel-server's package.json version.
|
|
25
|
+
* @param {string} vendorDir
|
|
26
|
+
* @returns {Promise<string | null>}
|
|
27
|
+
*/
|
|
28
|
+
async function getVendorVersion(vendorDir) {
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = join(vendorDir, 'panel-server', 'package.json');
|
|
31
|
+
const raw = await readFile(pkgPath, 'utf8');
|
|
32
|
+
return JSON.parse(raw).version || null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Panel redeployment subtasks. Only updates panel-server and panel-client files,
|
|
40
|
+
* runs npm install, merges config, and restarts the service. Does not touch
|
|
41
|
+
* OS hardening, mTLS certs, nginx, or any other system configuration.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} ctx Shared installer context.
|
|
44
|
+
* @param {object} task Parent Listr2 task reference.
|
|
45
|
+
* @returns {import('listr2').ListrTask[]}
|
|
46
|
+
*/
|
|
47
|
+
export function redeployTasks(ctx, task) {
|
|
48
|
+
const installDir = ctx.installDir;
|
|
49
|
+
const configDir = ctx.configDir;
|
|
50
|
+
|
|
51
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
52
|
+
const thisDir = dirname(thisFile);
|
|
53
|
+
const packageRoot = join(thisDir, '..', '..');
|
|
54
|
+
const vendorDir = join(packageRoot, 'vendor');
|
|
55
|
+
|
|
56
|
+
return task.newListr([
|
|
57
|
+
{
|
|
58
|
+
title: 'Checking versions',
|
|
59
|
+
task: async (_ctx, subtask) => {
|
|
60
|
+
const installed = await getInstalledVersion(installDir);
|
|
61
|
+
const vendor = await getVendorVersion(vendorDir);
|
|
62
|
+
ctx.installedVersion = installed;
|
|
63
|
+
ctx.vendorVersion = vendor;
|
|
64
|
+
subtask.output = `Installed: ${installed || 'unknown'} → New: ${vendor || 'unknown'}`;
|
|
65
|
+
},
|
|
66
|
+
rendererOptions: { persistentOutput: true },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'Stopping panel service',
|
|
70
|
+
task: async (_ctx, subtask) => {
|
|
71
|
+
try {
|
|
72
|
+
const { stdout: status } = await execa('systemctl', [
|
|
73
|
+
'is-active',
|
|
74
|
+
'portlama-panel',
|
|
75
|
+
]);
|
|
76
|
+
if (status.trim() === 'active') {
|
|
77
|
+
await execa('systemctl', ['stop', 'portlama-panel']);
|
|
78
|
+
subtask.output = 'Service stopped';
|
|
79
|
+
} else {
|
|
80
|
+
subtask.output = `Service was not running (${status.trim()})`;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
subtask.output = 'Service was not running';
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
rendererOptions: { persistentOutput: true },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: 'Updating panel-server',
|
|
90
|
+
task: async (_ctx, subtask) => {
|
|
91
|
+
const serverSrc = join(vendorDir, 'panel-server');
|
|
92
|
+
const serverDest = join(installDir, 'panel-server');
|
|
93
|
+
|
|
94
|
+
if (!existsSync(serverSrc)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Panel server source not found at ${serverSrc}. Ensure the package is intact.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
subtask.output = 'Copying panel-server files...';
|
|
101
|
+
await cp(join(serverSrc, 'package.json'), join(serverDest, 'package.json'));
|
|
102
|
+
await cp(join(serverSrc, 'src'), join(serverDest, 'src'), {
|
|
103
|
+
recursive: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
subtask.output = 'Installing production dependencies...';
|
|
107
|
+
try {
|
|
108
|
+
await execa('npm', ['install', '--production'], {
|
|
109
|
+
cwd: serverDest,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Failed to install panel-server dependencies.\n${err.stderr || err.message}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await execa('chown', ['-R', 'portlama:portlama', serverDest]);
|
|
118
|
+
subtask.output = 'Panel server updated';
|
|
119
|
+
},
|
|
120
|
+
rendererOptions: { persistentOutput: true },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
title: 'Updating panel-client',
|
|
124
|
+
task: async (_ctx, subtask) => {
|
|
125
|
+
const clientSrc = join(vendorDir, 'panel-client');
|
|
126
|
+
const clientDest = join(installDir, 'panel-client');
|
|
127
|
+
|
|
128
|
+
if (!existsSync(clientSrc)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Panel client source not found at ${clientSrc}. Ensure the package is intact.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const prebuiltDist = join(clientSrc, 'dist');
|
|
135
|
+
if (!existsSync(join(prebuiltDist, 'index.html'))) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Pre-built panel-client dist not found. The package may be corrupted.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
subtask.output = 'Copying panel-client dist...';
|
|
142
|
+
const distDest = join(clientDest, 'dist');
|
|
143
|
+
await rm(distDest, { recursive: true, force: true });
|
|
144
|
+
await cp(prebuiltDist, distDest, { recursive: true });
|
|
145
|
+
|
|
146
|
+
await execa('chown', ['-R', 'portlama:portlama', clientDest]);
|
|
147
|
+
subtask.output = 'Panel client updated';
|
|
148
|
+
},
|
|
149
|
+
rendererOptions: { persistentOutput: true },
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: 'Updating panel configuration',
|
|
153
|
+
task: async (_ctx, subtask) => {
|
|
154
|
+
const configPath = join(configDir, 'panel.json');
|
|
155
|
+
|
|
156
|
+
if (!existsSync(configPath)) {
|
|
157
|
+
subtask.output = 'No existing config — skipping (full install needed)';
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
subtask.output = 'Merging configuration...';
|
|
162
|
+
const existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
163
|
+
|
|
164
|
+
const config = {
|
|
165
|
+
...existing,
|
|
166
|
+
ip: ctx.ip,
|
|
167
|
+
dataDir: configDir,
|
|
168
|
+
staticDir: join(installDir, 'panel-client', 'dist'),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await writeFile(
|
|
172
|
+
configPath,
|
|
173
|
+
JSON.stringify(config, null, 2) + '\n',
|
|
174
|
+
{ mode: 0o640 },
|
|
175
|
+
);
|
|
176
|
+
await execa('chown', ['portlama:portlama', configPath]);
|
|
177
|
+
|
|
178
|
+
subtask.output = 'Configuration updated';
|
|
179
|
+
},
|
|
180
|
+
rendererOptions: { persistentOutput: true },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
title: 'Reloading systemd and restarting panel',
|
|
184
|
+
task: async (_ctx, subtask) => {
|
|
185
|
+
subtask.output = 'Reloading systemd daemon...';
|
|
186
|
+
await execa('systemctl', ['daemon-reload']);
|
|
187
|
+
|
|
188
|
+
subtask.output = 'Starting portlama-panel...';
|
|
189
|
+
await execa('systemctl', ['start', 'portlama-panel']);
|
|
190
|
+
|
|
191
|
+
subtask.output = 'Waiting for service to start...';
|
|
192
|
+
await sleep(3000);
|
|
193
|
+
|
|
194
|
+
const { stdout: status } = await execa('systemctl', [
|
|
195
|
+
'is-active',
|
|
196
|
+
'portlama-panel',
|
|
197
|
+
]);
|
|
198
|
+
if (status.trim() !== 'active') {
|
|
199
|
+
const { stdout: logs } = await execa('journalctl', [
|
|
200
|
+
'-u',
|
|
201
|
+
'portlama-panel',
|
|
202
|
+
'--no-pager',
|
|
203
|
+
'-n',
|
|
204
|
+
'20',
|
|
205
|
+
]);
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Panel service failed to start. Status: ${status.trim()}\nRecent logs:\n${logs}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
subtask.output = 'Running health check...';
|
|
212
|
+
try {
|
|
213
|
+
const { stdout: healthResponse } = await execa('curl', [
|
|
214
|
+
'-s',
|
|
215
|
+
'--max-time',
|
|
216
|
+
'5',
|
|
217
|
+
'http://127.0.0.1:3100/api/health',
|
|
218
|
+
]);
|
|
219
|
+
subtask.output = `Panel running. Health: ${healthResponse}`;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const { stdout: logs } = await execa('journalctl', [
|
|
222
|
+
'-u',
|
|
223
|
+
'portlama-panel',
|
|
224
|
+
'--no-pager',
|
|
225
|
+
'-n',
|
|
226
|
+
'20',
|
|
227
|
+
]);
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Panel health check failed.\nRecent logs:\n${logs}\n${error.message}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
rendererOptions: { persistentOutput: true },
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
}
|