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.
- package/bin/cli.js +180 -0
- package/client/assets/index-C4GZ_uMC.js +104 -0
- package/client/assets/index-Dq-W19hL.css +2 -0
- package/client/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/client/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/client/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/client/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/client/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/client/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/client/favicon.svg +1 -0
- package/client/icons.svg +24 -0
- package/client/index.html +14 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +143 -0
- package/dist/routes/async-route.d.ts +3 -0
- package/dist/routes/async-route.js +9 -0
- package/dist/routes/auth.d.ts +5 -0
- package/dist/routes/auth.js +101 -0
- package/dist/routes/database.d.ts +2 -0
- package/dist/routes/database.js +192 -0
- package/dist/routes/deploy.d.ts +2 -0
- package/dist/routes/deploy.js +161 -0
- package/dist/routes/domain.d.ts +2 -0
- package/dist/routes/domain.js +54 -0
- package/dist/routes/firewall.d.ts +2 -0
- package/dist/routes/firewall.js +63 -0
- package/dist/routes/instances.d.ts +2 -0
- package/dist/routes/instances.js +77 -0
- package/dist/routes/logs.d.ts +2 -0
- package/dist/routes/logs.js +67 -0
- package/dist/routes/system.d.ts +2 -0
- package/dist/routes/system.js +83 -0
- package/dist/services/auth.d.ts +25 -0
- package/dist/services/auth.js +128 -0
- package/dist/services/backup-config.d.ts +16 -0
- package/dist/services/backup-config.js +51 -0
- package/dist/services/backup-scheduler.d.ts +2 -0
- package/dist/services/backup-scheduler.js +104 -0
- package/dist/services/daemon.d.ts +28 -0
- package/dist/services/daemon.js +180 -0
- package/dist/services/database.d.ts +60 -0
- package/dist/services/database.js +540 -0
- package/dist/services/deploy.d.ts +102 -0
- package/dist/services/deploy.js +540 -0
- package/dist/services/domain.d.ts +26 -0
- package/dist/services/domain.js +170 -0
- package/dist/services/firewall.d.ts +24 -0
- package/dist/services/firewall.js +64 -0
- package/dist/services/instances.d.ts +17 -0
- package/dist/services/instances.js +67 -0
- package/dist/services/logs.d.ts +8 -0
- package/dist/services/logs.js +61 -0
- package/dist/services/metrics-history.d.ts +13 -0
- package/dist/services/metrics-history.js +36 -0
- package/dist/services/packages.d.ts +22 -0
- package/dist/services/packages.js +124 -0
- package/dist/services/s3.d.ts +17 -0
- package/dist/services/s3.js +93 -0
- package/dist/services/ssh.d.ts +78 -0
- package/dist/services/ssh.js +309 -0
- package/dist/services/system.d.ts +74 -0
- package/dist/services/system.js +286 -0
- package/package.json +55 -0
- package/scripts/dev.js +88 -0
- package/scripts/postinstall.js +123 -0
- package/scripts/preuninstall.js +21 -0
- package/scripts/try-install.sh +65 -0
- 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"
|