easy-vps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/bin/cli.js +180 -0
  2. package/client/assets/index-C4GZ_uMC.js +104 -0
  3. package/client/assets/index-Dq-W19hL.css +2 -0
  4. package/client/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  5. package/client/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  6. package/client/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  7. package/client/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  8. package/client/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  9. package/client/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  10. package/client/favicon.svg +1 -0
  11. package/client/icons.svg +24 -0
  12. package/client/index.html +14 -0
  13. package/dist/index.d.ts +22 -0
  14. package/dist/index.js +143 -0
  15. package/dist/routes/async-route.d.ts +3 -0
  16. package/dist/routes/async-route.js +9 -0
  17. package/dist/routes/auth.d.ts +5 -0
  18. package/dist/routes/auth.js +101 -0
  19. package/dist/routes/database.d.ts +2 -0
  20. package/dist/routes/database.js +192 -0
  21. package/dist/routes/deploy.d.ts +2 -0
  22. package/dist/routes/deploy.js +161 -0
  23. package/dist/routes/domain.d.ts +2 -0
  24. package/dist/routes/domain.js +54 -0
  25. package/dist/routes/firewall.d.ts +2 -0
  26. package/dist/routes/firewall.js +63 -0
  27. package/dist/routes/instances.d.ts +2 -0
  28. package/dist/routes/instances.js +77 -0
  29. package/dist/routes/logs.d.ts +2 -0
  30. package/dist/routes/logs.js +67 -0
  31. package/dist/routes/system.d.ts +2 -0
  32. package/dist/routes/system.js +83 -0
  33. package/dist/services/auth.d.ts +25 -0
  34. package/dist/services/auth.js +128 -0
  35. package/dist/services/backup-config.d.ts +16 -0
  36. package/dist/services/backup-config.js +51 -0
  37. package/dist/services/backup-scheduler.d.ts +2 -0
  38. package/dist/services/backup-scheduler.js +104 -0
  39. package/dist/services/daemon.d.ts +28 -0
  40. package/dist/services/daemon.js +180 -0
  41. package/dist/services/database.d.ts +60 -0
  42. package/dist/services/database.js +540 -0
  43. package/dist/services/deploy.d.ts +102 -0
  44. package/dist/services/deploy.js +540 -0
  45. package/dist/services/domain.d.ts +26 -0
  46. package/dist/services/domain.js +170 -0
  47. package/dist/services/firewall.d.ts +24 -0
  48. package/dist/services/firewall.js +64 -0
  49. package/dist/services/instances.d.ts +17 -0
  50. package/dist/services/instances.js +67 -0
  51. package/dist/services/logs.d.ts +8 -0
  52. package/dist/services/logs.js +61 -0
  53. package/dist/services/metrics-history.d.ts +13 -0
  54. package/dist/services/metrics-history.js +36 -0
  55. package/dist/services/packages.d.ts +22 -0
  56. package/dist/services/packages.js +124 -0
  57. package/dist/services/s3.d.ts +17 -0
  58. package/dist/services/s3.js +93 -0
  59. package/dist/services/ssh.d.ts +78 -0
  60. package/dist/services/ssh.js +309 -0
  61. package/dist/services/system.d.ts +74 -0
  62. package/dist/services/system.js +286 -0
  63. package/package.json +55 -0
  64. package/scripts/dev.js +88 -0
  65. package/scripts/postinstall.js +123 -0
  66. package/scripts/preuninstall.js +21 -0
  67. package/scripts/try-install.sh +65 -0
  68. package/scripts/ui.js +54 -0
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PACKAGES = void 0;
4
+ exports.getMetrics = getMetrics;
5
+ exports.detect = detect;
6
+ exports.detectAll = detectAll;
7
+ exports.canAdminister = canAdminister;
8
+ exports.packageStream = packageStream;
9
+ // Dependency detection, installation and removal on the managed VPS, over SSH.
10
+ const packages_1 = require("./packages");
11
+ const ssh_1 = require("./ssh");
12
+ var packages_2 = require("./packages");
13
+ Object.defineProperty(exports, "PACKAGES", { enumerable: true, get: function () { return packages_2.PACKAGES; } });
14
+ function parseBytesToMb(bytes) {
15
+ return Math.round((bytes / 1024 / 1024) * 100) / 100;
16
+ }
17
+ function parseBytesToGb(bytes) {
18
+ return Math.round((bytes / 1024 / 1024 / 1024) * 100) / 100;
19
+ }
20
+ /** Collect comprehensive system metrics from the VPS. */
21
+ async function getMetrics(remote) {
22
+ const metricsScript = `#!/bin/bash
23
+ # CPU info
24
+ cpu_model=$(grep -m1 'model name' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | xargs || echo "Unknown")
25
+ cpu_cores=$(nproc 2>/dev/null || echo 1)
26
+ cpu_usage=$(top -bn1 2>/dev/null | grep '^%Cpu' | awk '{print 100 - $8}' || echo 0)
27
+
28
+ # Memory info
29
+ mem_info=$(free -b | grep Mem)
30
+ mem_total=$(echo "$mem_info" | awk '{print $2}')
31
+ mem_used=$(echo "$mem_info" | awk '{print $3}')
32
+ mem_free=$(echo "$mem_info" | awk '{print $4}')
33
+ mem_available=$(echo "$mem_info" | awk '{print $7}')
34
+
35
+ # Swap info
36
+ swap_info=$(free -b | grep Swap)
37
+ swap_total=$(echo "$swap_info" | awk '{print $2}')
38
+ swap_used=$(echo "$swap_info" | awk '{print $3}')
39
+ swap_free=$(echo "$swap_info" | awk '{print $4}')
40
+
41
+ # Disk info
42
+ disk_info=$(df -B1 / | tail -1)
43
+ disk_total=$(echo "$disk_info" | awk '{print $2}')
44
+ disk_used=$(echo "$disk_info" | awk '{print $3}')
45
+ disk_free=$(echo "$disk_info" | awk '{print $4}')
46
+
47
+ # Uptime
48
+ uptime_seconds=$(awk '{print int($1)}' /proc/uptime)
49
+ uptime_human=$(uptime -p 2>/dev/null || echo "up $((uptime_seconds/86400)) days")
50
+
51
+ # Load average
52
+ load_avg=$(cat /proc/loadavg | awk '{print $1, $2, $3}')
53
+
54
+ # Network (first non-loopback interface)
55
+ net_rx=0
56
+ net_tx=0
57
+ for iface in /sys/class/net/*/statistics; do
58
+ iface_name=$(echo "$iface" | cut -d/ -f5)
59
+ if [ "$iface_name" != "lo" ] && [ -d "$iface" ]; then
60
+ net_rx=$(cat "$iface/rx_bytes" 2>/dev/null || echo 0)
61
+ net_tx=$(cat "$iface/tx_bytes" 2>/dev/null || echo 0)
62
+ break
63
+ fi
64
+ done
65
+
66
+ # Top processes by CPU
67
+ top_procs=$(ps aux --sort=-%cpu 2>/dev/null | head -6 | tail -5 | awk '{print $11, $3, $4, $2}')
68
+
69
+ echo "CPU_MODEL:$cpu_model"
70
+ echo "CPU_CORES:$cpu_cores"
71
+ echo "CPU_USAGE:$cpu_usage"
72
+ echo "MEM_TOTAL:$mem_total"
73
+ echo "MEM_USED:$mem_used"
74
+ echo "MEM_FREE:$mem_free"
75
+ echo "MEM_AVAILABLE:$mem_available"
76
+ echo "SWAP_TOTAL:$swap_total"
77
+ echo "SWAP_USED:$swap_used"
78
+ echo "SWAP_FREE:$swap_free"
79
+ echo "DISK_TOTAL:$disk_total"
80
+ echo "DISK_USED:$disk_used"
81
+ echo "DISK_FREE:$disk_free"
82
+ echo "UPTIME_SECONDS:$uptime_seconds"
83
+ echo "UPTIME_HUMAN:$uptime_human"
84
+ echo "LOAD_AVG:$load_avg"
85
+ echo "NET_RX:$net_rx"
86
+ echo "NET_TX:$net_tx"
87
+ echo "TOP_PROCS:$top_procs"
88
+ `;
89
+ const { stdout, code } = await remote.exec((0, ssh_1.loginShell)(metricsScript), 15_000);
90
+ if (code !== 0) {
91
+ return getDefaultMetrics();
92
+ }
93
+ const lines = stdout.split('\n').filter((l) => l.includes(':'));
94
+ const get = (key) => {
95
+ const line = lines.find((l) => l.startsWith(`${key}:`));
96
+ return line ? line.slice(key.length + 1).trim() : '';
97
+ };
98
+ const cpuCores = parseInt(get('CPU_CORES')) || 1;
99
+ const cpuUsage = Math.min(100, Math.max(0, parseFloat(get('CPU_USAGE')) || 0));
100
+ const memTotal = parseInt(get('MEM_TOTAL')) || 0;
101
+ const memUsed = parseInt(get('MEM_USED')) || 0;
102
+ const memFree = parseInt(get('MEM_FREE')) || 0;
103
+ const memAvailable = parseInt(get('MEM_AVAILABLE')) || 0;
104
+ const memUsagePercent = memTotal > 0 ? Math.round((memUsed / memTotal) * 10000) / 100 : 0;
105
+ const swapTotal = parseInt(get('SWAP_TOTAL')) || 0;
106
+ const swapUsed = parseInt(get('SWAP_USED')) || 0;
107
+ const swapFree = parseInt(get('SWAP_FREE')) || 0;
108
+ const swapUsagePercent = swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 10000) / 100 : 0;
109
+ const diskTotal = parseInt(get('DISK_TOTAL')) || 0;
110
+ const diskUsed = parseInt(get('DISK_USED')) || 0;
111
+ const diskFree = parseInt(get('DISK_FREE')) || 0;
112
+ const diskUsagePercent = diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 10000) / 100 : 0;
113
+ const uptimeSeconds = parseInt(get('UPTIME_SECONDS')) || 0;
114
+ const loadParts = get('LOAD_AVG').split(/\s+/);
115
+ const loadOne = parseFloat(loadParts[0]) || 0;
116
+ const loadFive = parseFloat(loadParts[1]) || 0;
117
+ const loadFifteen = parseFloat(loadParts[2]) || 0;
118
+ const netRx = parseInt(get('NET_RX')) || 0;
119
+ const netTx = parseInt(get('NET_TX')) || 0;
120
+ const topProcsRaw = get('TOP_PROCS');
121
+ const topProcesses = topProcsRaw
122
+ ? topProcsRaw.split('\n').map((line) => {
123
+ const parts = line.trim().split(/\s+/);
124
+ return {
125
+ name: parts[0] || 'unknown',
126
+ cpu: parseFloat(parts[1]) || 0,
127
+ memory: parseFloat(parts[2]) || 0,
128
+ pid: parseInt(parts[3]) || 0,
129
+ };
130
+ })
131
+ : [];
132
+ return {
133
+ cpu: {
134
+ model: get('CPU_MODEL') || 'Unknown',
135
+ cores: cpuCores,
136
+ usagePercent: Math.round(cpuUsage * 100) / 100,
137
+ },
138
+ memory: {
139
+ totalMb: parseBytesToMb(memTotal),
140
+ usedMb: parseBytesToMb(memUsed),
141
+ freeMb: parseBytesToMb(memFree),
142
+ availableMb: parseBytesToMb(memAvailable),
143
+ usagePercent: Math.round(memUsagePercent * 100) / 100,
144
+ },
145
+ disk: {
146
+ totalGb: parseBytesToGb(diskTotal),
147
+ usedGb: parseBytesToGb(diskUsed),
148
+ freeGb: parseBytesToGb(diskFree),
149
+ usagePercent: Math.round(diskUsagePercent * 100) / 100,
150
+ },
151
+ swap: {
152
+ totalMb: parseBytesToMb(swapTotal),
153
+ usedMb: parseBytesToMb(swapUsed),
154
+ freeMb: parseBytesToMb(swapFree),
155
+ usagePercent: Math.round(swapUsagePercent * 100) / 100,
156
+ },
157
+ uptime: get('UPTIME_HUMAN') || 'unknown',
158
+ uptimeSeconds,
159
+ loadAverage: { one: loadOne, five: loadFive, fifteen: loadFifteen },
160
+ network: { rxMb: parseBytesToMb(netRx), txMb: parseBytesToMb(netTx) },
161
+ topProcesses,
162
+ };
163
+ }
164
+ function getDefaultMetrics() {
165
+ return {
166
+ cpu: { model: 'Unknown', cores: 0, usagePercent: 0 },
167
+ memory: { totalMb: 0, usedMb: 0, freeMb: 0, availableMb: 0, usagePercent: 0 },
168
+ disk: { totalGb: 0, usedGb: 0, freeGb: 0, usagePercent: 0 },
169
+ swap: { totalMb: 0, usedMb: 0, freeMb: 0, usagePercent: 0 },
170
+ uptime: 'unknown',
171
+ uptimeSeconds: 0,
172
+ loadAverage: { one: 0, five: 0, fifteen: 0 },
173
+ network: { rxMb: 0, txMb: 0 },
174
+ topProcesses: [],
175
+ };
176
+ }
177
+ const DETECT_TIMEOUT_MS = 10_000;
178
+ // A non-interactive shell misses tools installed outside the system prefix.
179
+ // System paths stay first so a distro binary wins; every installed nvm version
180
+ // is appended, because the one holding the binary is not necessarily the
181
+ // newest — picking just one silently misses tools under the others.
182
+ const PATH_PRIMER = 'export PATH="/usr/local/bin:/usr/bin:/snap/bin:$PATH"; ' +
183
+ 'for nvm_bin in "$HOME"/.nvm/versions/node/*/bin; do ' +
184
+ '[ -d "$nvm_bin" ] && PATH="$PATH:$nvm_bin"; done; export PATH;';
185
+ /** Detect whether a dependency is present on the VPS. */
186
+ async function detect(remote, name) {
187
+ try {
188
+ // nginx and certbot print their version banner on stderr
189
+ const { stdout, stderr, code } = await remote.exec((0, ssh_1.loginShell)(`${PATH_PRIMER} ${packages_1.PACKAGES[name].versionCommand} 2>&1`), DETECT_TIMEOUT_MS);
190
+ const removes = packages_1.PACKAGES[name].removes;
191
+ if (code !== 0)
192
+ return { name, installed: false, version: null, removes };
193
+ const version = (stdout || stderr).trim().split('\n')[0] || null;
194
+ return { name, installed: true, version, removes };
195
+ }
196
+ catch {
197
+ // A timeout or a dropped channel is reported as "not installed" rather than
198
+ // failing the whole status request over one wedged binary.
199
+ return { name, installed: false, version: null, removes: packages_1.PACKAGES[name].removes };
200
+ }
201
+ }
202
+ async function detectAll(remote) {
203
+ return Promise.all(packages_1.DEPENDENCY_NAMES.map((name) => detect(remote, name)));
204
+ }
205
+ /** True when the logged-in remote user can run privileged commands unattended. */
206
+ async function canAdminister(remote) {
207
+ if (await remote.isRoot())
208
+ return true;
209
+ return (await remote.exec('sudo -n true', DETECT_TIMEOUT_MS)).code === 0;
210
+ }
211
+ // System locations first: pm2 installed by an nvm npm lands inside that nvm
212
+ // prefix, which a non-interactive shell cannot see afterwards, so detection
213
+ // would keep reporting pm2 as missing even after a successful install.
214
+ const NPM_CANDIDATES = ['/usr/local/bin/npm', '/usr/bin/npm', '/snap/bin/npm'];
215
+ /** Locates npm on the VPS, or returns null when Node is not installed at all. */
216
+ async function resolveNpm(remote) {
217
+ const probe = [
218
+ ...NPM_CANDIDATES.map((candidate) => `[ -x ${(0, ssh_1.quote)(candidate)} ] && echo ${(0, ssh_1.quote)(candidate)} && exit 0`),
219
+ // PATH_PRIMER has already added every nvm bin, so this covers those too.
220
+ 'command -v npm 2>/dev/null && exit 0',
221
+ ].join('\n');
222
+ const { stdout } = await remote.exec((0, ssh_1.loginShell)(`${PATH_PRIMER}\n${probe}`), DETECT_TIMEOUT_MS);
223
+ return stdout.trim().split('\n')[0]?.trim() || null;
224
+ }
225
+ /**
226
+ * Turns a definition's command into something runnable on the server: apt gets
227
+ * a refreshed index and a non-interactive frontend, and {{npm}} is replaced
228
+ * with a real path. npm is not reliably on a non-interactive PATH, and on a
229
+ * fresh VPS Node may be missing altogether — in which case installing pm2
230
+ * bootstraps Node from the distro first.
231
+ */
232
+ async function buildScript(remote, name, action) {
233
+ const definition = packages_1.PACKAGES[name];
234
+ const command = action === 'install' ? definition.installCommand : definition.uninstallCommand;
235
+ if (command.includes(packages_1.NPM_PLACEHOLDER)) {
236
+ const npm = await resolveNpm(remote);
237
+ if (!npm) {
238
+ if (action === 'uninstall') {
239
+ // Nothing to uninstall through npm, but its leftovers still go.
240
+ return command.split(' && ').filter((part) => !part.includes(packages_1.NPM_PLACEHOLDER)).join(' && ');
241
+ }
242
+ return ('export DEBIAN_FRONTEND=noninteractive; apt-get update && ' +
243
+ 'apt-get install -y nodejs npm && ' +
244
+ command.replaceAll(packages_1.NPM_PLACEHOLDER, 'npm'));
245
+ }
246
+ // Put npm's own directory first so the node binary beside it is found too.
247
+ return (`export PATH="$(dirname ${(0, ssh_1.quote)(npm)}):$PATH"; ` +
248
+ command.replaceAll(packages_1.NPM_PLACEHOLDER, (0, ssh_1.quote)(npm)));
249
+ }
250
+ // apt needs a refreshed index first; chain it so both halves stream into the
251
+ // same channel and the exit code reflects the real work, not the refresh.
252
+ return action === 'install'
253
+ ? `export DEBIAN_FRONTEND=noninteractive; apt-get update && ${command}`
254
+ : `export DEBIAN_FRONTEND=noninteractive; ${command}`;
255
+ }
256
+ /**
257
+ * Installs or removes a dependency on the VPS, streaming stdout/stderr line by
258
+ * line. Returns a function that cancels the run in progress.
259
+ */
260
+ function packageStream(remote, name, action, handlers) {
261
+ if (!(name in packages_1.PACKAGES)) {
262
+ handlers.onError(`Unknown dependency "${name}"`);
263
+ return () => undefined;
264
+ }
265
+ let cancel = () => undefined;
266
+ let cancelled = false;
267
+ void (async () => {
268
+ if (!(await canAdminister(remote))) {
269
+ handlers.onError(`"${remote.username}" cannot run privileged commands on ${remote.host}. ` +
270
+ 'Log in as root, or enable passwordless sudo.');
271
+ return;
272
+ }
273
+ const script = await buildScript(remote, name, action);
274
+ if (cancelled)
275
+ return;
276
+ handlers.onOutput(`$ ${script}`);
277
+ cancel = remote.execStream(await remote.privileged((0, ssh_1.loginShell)(script)), handlers);
278
+ })().catch((error) => {
279
+ if (!cancelled)
280
+ handlers.onError(error.message);
281
+ });
282
+ return () => {
283
+ cancelled = true;
284
+ cancel();
285
+ };
286
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "easy-vps",
3
+ "version": "0.1.0",
4
+ "description": "Install dependencies, point domains, deploy apps and run Postgres on a VPS from one UI",
5
+ "keywords": [
6
+ "vps",
7
+ "nginx",
8
+ "pm2",
9
+ "docker",
10
+ "certbot",
11
+ "postgres",
12
+ "deploy"
13
+ ],
14
+ "license": "MIT",
15
+ "bin": {
16
+ "easy-vps": "bin/cli.js"
17
+ },
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "dist",
26
+ "client",
27
+ "scripts"
28
+ ],
29
+ "scripts": {
30
+ "build": "npm run build:server && npm run build:client",
31
+ "build:dev": "npm run build:server && npm run build:client",
32
+ "build:server": "tsc -p tsconfig.json",
33
+ "build:client": "npm --prefix client-ui run build",
34
+ "dev": "node scripts/dev.js",
35
+ "dev:server": "tsc -p tsconfig.json --watch",
36
+ "dev:client": "npm --prefix client-ui run dev",
37
+ "start": "node bin/cli.js",
38
+ "prepublishOnly": "npm run build",
39
+ "postinstall": "node scripts/postinstall.js",
40
+ "preuninstall": "node scripts/preuninstall.js"
41
+ },
42
+ "dependencies": {
43
+ "@aws-sdk/client-s3": "^3.1118.0",
44
+ "express": "^4.21.2",
45
+ "node-cron": "^4.6.0",
46
+ "ssh2": "^1.17.0"
47
+ },
48
+ "devDependencies": {
49
+ "@types/express": "^4.17.21",
50
+ "@types/node": "^24.13.3",
51
+ "@types/node-cron": "^3.0.11",
52
+ "@types/ssh2": "^1.15.5",
53
+ "typescript": "~5.9.3"
54
+ }
55
+ }
package/scripts/dev.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // One command for local development: compiles the server, runs it on 41556,
5
+ // and starts the Vite dev server on 41557 which proxies /api back to it.
6
+ //
7
+ // Running Vite alone leaves the proxy with nothing to talk to, which shows up
8
+ // as `ECONNREFUSED 127.0.0.1:41556` in the Vite log.
9
+
10
+ const { spawn, spawnSync } = require('node:child_process')
11
+ const path = require('node:path')
12
+
13
+ const ROOT = path.resolve(__dirname, '..')
14
+ const PANEL_PORT = Number(process.env.EASY_VPS_PORT) || 41556
15
+ const UI_PORT = Number(process.env.EASY_VPS_UI_PORT) || PANEL_PORT + 1
16
+ const ESC = String.fromCharCode(27)
17
+ const color = process.stdout.isTTY && !process.env.NO_COLOR
18
+
19
+ const paint = (code, text) => (color ? `${ESC}[${code}m${text}${ESC}[0m` : text)
20
+ const LABEL = {
21
+ tsc: paint(35, 'tsc '),
22
+ api: paint(36, 'api '),
23
+ ui: paint(32, 'ui '),
24
+ }
25
+
26
+ const children = []
27
+
28
+ /** Prefix every line of a child's output so the three streams stay readable. */
29
+ function prefix(label, stream) {
30
+ let buffer = ''
31
+ stream.on('data', (chunk) => {
32
+ buffer += chunk.toString()
33
+ const lines = buffer.split('\n')
34
+ buffer = lines.pop() ?? ''
35
+ for (const line of lines) {
36
+ if (line.trim()) console.log(`${label} ${paint(2, '|')} ${line}`)
37
+ }
38
+ })
39
+ }
40
+
41
+ function start(label, command, args, options = {}) {
42
+ const child = spawn(command, args, { cwd: ROOT, ...options })
43
+ prefix(label, child.stdout)
44
+ prefix(label, child.stderr)
45
+ child.on('exit', (code) => {
46
+ if (code !== 0 && code !== null) {
47
+ console.log(`${label} ${paint(2, '|')} exited with code ${code}`)
48
+ }
49
+ })
50
+ children.push(child)
51
+ return child
52
+ }
53
+
54
+ function shutdown() {
55
+ for (const child of children) {
56
+ child.kill('SIGTERM')
57
+ }
58
+ process.exit(0)
59
+ }
60
+
61
+ process.on('SIGINT', shutdown)
62
+ process.on('SIGTERM', shutdown)
63
+
64
+ // A first compile must finish before the server can boot.
65
+ console.log(`${LABEL.tsc} ${paint(2, '|')} building server…`)
66
+ const build = spawnSync('npx', ['tsc', '-p', 'tsconfig.json'], { cwd: ROOT, encoding: 'utf8' })
67
+
68
+ if (build.status !== 0) {
69
+ console.error(build.stdout || build.stderr)
70
+ console.error('server build failed — fix the errors above and rerun')
71
+ process.exit(1)
72
+ }
73
+
74
+ // Recompile on change; `node --watch` restarts the server when dist/ updates.
75
+ start(LABEL.tsc, 'npx', ['tsc', '-p', 'tsconfig.json', '--watch', '--preserveWatchOutput'])
76
+ const env = {
77
+ ...process.env,
78
+ EASY_VPS_HOST: '127.0.0.1',
79
+ EASY_VPS_PORT: String(PANEL_PORT),
80
+ EASY_VPS_UI_PORT: String(UI_PORT),
81
+ }
82
+
83
+ start(LABEL.api, process.execPath, ['--watch', 'bin/cli.js'], { env })
84
+ start(LABEL.ui, 'npm', ['--prefix', 'client-ui', 'run', 'dev'], { env })
85
+
86
+ console.log(
87
+ `${LABEL.ui} ${paint(2, '|')} open http://localhost:${UI_PORT} — the UI proxies /api to ${PANEL_PORT}`,
88
+ )
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // Runs after `npm i -g easy-vps`: verifies the payload, prepares the UI,
5
+ // opens the port and starts the panel as a background service.
6
+ //
7
+ // Never throws — a failing postinstall aborts the whole npm install, so every
8
+ // step degrades to a warning and the user is told how to finish by hand.
9
+
10
+ const { execFileSync } = require('node:child_process')
11
+ const { existsSync } = require('node:fs')
12
+ const path = require('node:path')
13
+
14
+ const ui = require('./ui')
15
+
16
+ const ROOT = path.resolve(__dirname, '..')
17
+ const PORT = Number(process.env.EASY_VPS_PORT) || 41556
18
+ const HOST = process.env.EASY_VPS_HOST || '0.0.0.0'
19
+
20
+ /** Skip the whole hook for local/dev installs and opt-outs. */
21
+ function shouldSkip() {
22
+ if (process.env.EASY_VPS_NO_AUTOSTART) return 'EASY_VPS_NO_AUTOSTART is set'
23
+ if (process.env.CI) return 'running in CI'
24
+ // npm sets this for `npm i -g`; a local dependency install must not start a server
25
+ if (process.env.npm_config_global !== 'true') return 'not a global install'
26
+ return null
27
+ }
28
+
29
+ function verifyPayload() {
30
+ const missing = ['dist/index.js', 'bin/cli.js'].filter((f) => !existsSync(path.join(ROOT, f)))
31
+ if (missing.length) throw new Error(`missing ${missing.join(', ')}`)
32
+ }
33
+
34
+ /**
35
+ * The UI ships prebuilt in the tarball. It is only absent when running from a
36
+ * source checkout, where client-ui/ is present and can be built.
37
+ */
38
+ function prepareUI() {
39
+ if (existsSync(path.join(ROOT, 'client', 'index.html'))) return 'prebuilt'
40
+ if (!existsSync(path.join(ROOT, 'client-ui', 'package.json'))) throw new Error('no UI payload')
41
+
42
+ execFileSync('npm', ['--prefix', path.join(ROOT, 'client-ui'), 'run', 'build'], {
43
+ stdio: 'ignore',
44
+ })
45
+ return 'built'
46
+ }
47
+
48
+ async function main() {
49
+ const skip = shouldSkip()
50
+ if (skip) return
51
+
52
+ let daemon
53
+ ui.blank()
54
+
55
+ ui.step('installing easy vps')
56
+ try {
57
+ verifyPayload()
58
+ daemon = require(path.join(ROOT, 'dist', 'services', 'daemon.js'))
59
+ ui.done('easy vps installed')
60
+ } catch (error) {
61
+ ui.warn(`easy vps installed, but the payload looks incomplete (${error.message})`)
62
+ return
63
+ }
64
+
65
+ ui.step('compiling user interface')
66
+ try {
67
+ prepareUI()
68
+ ui.done('user interface ready')
69
+ } catch (error) {
70
+ ui.warn(`user interface unavailable (${error.message})`)
71
+ }
72
+
73
+ ui.step('enabling port')
74
+ try {
75
+ const opened = await daemon.openFirewall(PORT)
76
+ if (opened) ui.done(`port ${PORT} enabled`)
77
+ else if (!daemon.isRoot()) ui.done(`port ${PORT} ready ${ui.dim('(firewall not changed: not root)')}`)
78
+ else ui.done(`port ${PORT} ready ${ui.dim('(no active ufw firewall to update)')}`)
79
+ } catch (error) {
80
+ ui.warn(`could not enable port ${PORT} (${error.message})`)
81
+ }
82
+
83
+ ui.step('starting service')
84
+ let running = false
85
+ try {
86
+ const mode = await daemon.start({ port: PORT, host: HOST })
87
+ const state = await daemon.status()
88
+ running = state.running
89
+ if (running) {
90
+ ui.done(
91
+ mode === 'systemd'
92
+ ? 'service started (systemd, restarts on reboot)'
93
+ : `service started ${ui.dim('(background process)')}`,
94
+ )
95
+ } else {
96
+ ui.warn(`service did not start (${state.detail})`)
97
+ }
98
+ } catch (error) {
99
+ ui.warn(`service did not start (${error.message})`)
100
+ }
101
+
102
+ ui.blank()
103
+
104
+ if (!running) {
105
+ ui.warn('Setup finished with warnings. Start it by hand with: easy-vps start')
106
+ ui.blank()
107
+ return
108
+ }
109
+
110
+ ui.done(ui.bold('All process are completed'))
111
+ const hosts = daemon.addresses()
112
+ const shown = hosts.length ? hosts : ['[vps-ip]']
113
+ for (const address of shown) {
114
+ ui.line(` in your browser visit ${ui.bold(`http://${address}:${PORT}`)} to see the graphical interface`)
115
+ }
116
+ ui.blank()
117
+ ui.line(ui.dim(` easy-vps status | easy-vps stop | easy-vps logs`))
118
+ ui.blank()
119
+ }
120
+
121
+ main()
122
+ .catch(() => undefined)
123
+ .finally(() => ui.close())
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // Stops the panel and removes its systemd unit before the package is deleted.
5
+ const path = require('node:path')
6
+ const ui = require('./ui')
7
+
8
+ async function main() {
9
+ if (process.env.npm_config_global !== 'true') return
10
+ try {
11
+ const daemon = require(path.resolve(__dirname, '..', 'dist', 'services', 'daemon.js'))
12
+ await daemon.uninstall()
13
+ ui.done('easy vps service stopped and removed')
14
+ } catch {
15
+ // nothing installed, or no permission — leave the uninstall itself alone
16
+ }
17
+ }
18
+
19
+ main()
20
+ .catch(() => undefined)
21
+ .finally(() => ui.close())
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # Rehearse `npm i -g easy-vps` locally, without touching the real global prefix.
3
+ #
4
+ # ./scripts/try-install.sh install into a sandbox prefix and check it
5
+ # ./scripts/try-install.sh clean remove the sandbox and stop the panel
6
+ set -euo pipefail
7
+
8
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ SANDBOX="${TMPDIR:-/tmp}/easy-vps-sandbox"
10
+ PORT="${EASY_VPS_PORT:-41556}"
11
+
12
+ cleanup() {
13
+ if [ -x "$SANDBOX/bin/easy-vps" ]; then
14
+ "$SANDBOX/bin/easy-vps" uninstall >/dev/null 2>&1 || true
15
+ fi
16
+ rm -rf "$SANDBOX"
17
+ rm -f "$ROOT"/easy-vps-*.tgz
18
+ echo "sandbox removed"
19
+ }
20
+
21
+ if [ "${1:-}" = "clean" ]; then
22
+ cleanup
23
+ exit 0
24
+ fi
25
+
26
+ trap 'echo "FAILED"; exit 1' ERR
27
+
28
+ echo "==> building"
29
+ npm --prefix "$ROOT" run build >/dev/null
30
+
31
+ echo "==> packing"
32
+ rm -f "$ROOT"/easy-vps-*.tgz
33
+ TARBALL="$ROOT/$(cd "$ROOT" && npm pack --silent)"
34
+
35
+ echo "==> installing globally into $SANDBOX"
36
+ rm -rf "$SANDBOX"
37
+ mkdir -p "$SANDBOX"
38
+ export NPM_CONFIG_PREFIX="$SANDBOX"
39
+ # a pty makes the postinstall banner visible, exactly as a real terminal would
40
+ if command -v script >/dev/null 2>&1; then
41
+ script -qec "npm i -g '$TARBALL'" /dev/null
42
+ else
43
+ npm i -g "$TARBALL"
44
+ fi
45
+
46
+ export PATH="$SANDBOX/bin:$PATH"
47
+
48
+ echo
49
+ echo "==> checking"
50
+ for _ in $(seq 1 30); do
51
+ code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/api/health" || true)
52
+ [ "$code" = "200" ] && break
53
+ sleep 0.5
54
+ done
55
+
56
+ [ "$code" = "200" ] || { echo "panel did not answer on port $PORT"; exit 1; }
57
+
58
+ echo " health $(curl -s "http://localhost:$PORT/api/health")"
59
+ echo " ui HTTP $(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$PORT/")"
60
+ echo " deps $(curl -s "http://localhost:$PORT/api/system/status")"
61
+ easy-vps status
62
+
63
+ echo
64
+ echo "PASS — open http://localhost:$PORT/"
65
+ echo "When done: $0 clean"