easy-vps 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,114 @@
1
+ # easy-vps
2
+
3
+ Install dependencies, point domains, deploy apps and run Postgres on a VPS from one UI.
4
+
5
+ easy-vps is a control panel that runs on your server and gives you a web interface for
6
+ managing nginx, pm2, docker, certbot, and PostgreSQL, plus deploying apps and pointing
7
+ domains — all from a single dashboard.
8
+
9
+ ## Requirements
10
+
11
+ - A Linux server (systemd-based distros recommended: Ubuntu, Debian, etc.)
12
+ - **Node.js >= 18** (the `easy-vps` CLI requires it)
13
+ - `npm` (ships with Node.js)
14
+ - `root`/sudo access if you want the panel to run as a systemd service and open its
15
+ firewall port automatically. Without root it still runs as a background process,
16
+ just without service management and firewall changes.
17
+
18
+ Optional, used by their respective features:
19
+
20
+ - `nginx` + `certbot` — domain / HTTPS management
21
+ - `pm2` or `docker` — app deployment
22
+ - `postgres` — managed databases
23
+ - `ufw` — automatic firewall port opening
24
+
25
+ ## Install
26
+
27
+ Install globally from npm:
28
+
29
+ ```bash
30
+ npm install -g easy-vps
31
+ ```
32
+
33
+ The `postinstall` script automatically:
34
+
35
+ 1. Verifies the package payload
36
+ 2. Prepares the web UI
37
+ 3. Opens the panel's port in `ufw` (when run as root with an active firewall)
38
+ 4. Starts the panel as a background service (systemd when available, otherwise a
39
+ detached process)
40
+
41
+ When it finishes, open the printed URL in your browser:
42
+
43
+ ```
44
+ http://<your-server-ip>:41556/
45
+ ```
46
+
47
+ ### Install from source
48
+
49
+ ```bash
50
+ git clone <repo-url> easy-vps
51
+ cd easy-vps
52
+ npm install
53
+ npm run build
54
+ node bin/cli.js start
55
+ ```
56
+
57
+ > The web UI is prebuilt in the published tarball. From a source checkout the UI is
58
+ > built from `client-ui/` during `npm run build`.
59
+
60
+ ## Commands
61
+
62
+ The `easy-vps` command exposes the following subcommands:
63
+
64
+ ```bash
65
+ easy-vps # run the panel in the foreground (default)
66
+ easy-vps start # start the panel in the background
67
+ easy-vps stop # stop the background panel
68
+ easy-vps restart # restart the background panel
69
+ easy-vps status # show whether the panel is running
70
+ easy-vps logs # print the path to the panel log
71
+ easy-vps uninstall # stop the panel and remove its service
72
+ easy-vps --help # show full help
73
+ ```
74
+
75
+ ### Options
76
+
77
+ ```bash
78
+ -p, --port <port> Port to listen on (default: 41556, env: EASY_VPS_PORT)
79
+ --host <host> Address to bind (default: 0.0.0.0, env: EASY_VPS_HOST)
80
+ -v, --version Print version
81
+ -h, --help Show help
82
+ ```
83
+
84
+ Example:
85
+
86
+ ```bash
87
+ easy-vps start --port 8080 --host 127.0.0.1
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ | Variable | Default | Description |
93
+ | -------------------- | ----------- | ------------------------------------ |
94
+ | `EASY_VPS_PORT` | `41556` | Port the panel listens on |
95
+ | `EASY_VPS_HOST` | `0.0.0.0` | Address the panel binds to |
96
+ | `EASY_VPS_NO_AUTOSTART` | _unset_ | Set to skip the postinstall service start |
97
+
98
+ ## Uninstall
99
+
100
+ ```bash
101
+ easy-vps uninstall
102
+ npm rm -g easy-vps
103
+ ```
104
+
105
+ Run `easy-vps uninstall` first so the systemd unit (if created) is removed cleanly
106
+ before removing the package.
107
+
108
+ ## Service behavior
109
+
110
+ - **With systemd + root:** a `easy-vps.service` unit is created, enabled, and
111
+ restarted on boot. Logs: `journalctl -u easy-vps -f`.
112
+ - **Without systemd / not root:** the panel runs as a detached background process.
113
+ Logs are written to `~/.easy-vps/easy-vps.log` (or `/var/lib/easy-vps/easy-vps.log`
114
+ when run as root).
package/bin/cli.js CHANGED
@@ -160,6 +160,29 @@ async function main() {
160
160
  break
161
161
  }
162
162
 
163
+ // `npx easy-vps` / `easy-vps` with no subcommand: show the animated installer,
164
+ // then start the panel in the background so it survives a closed terminal.
165
+ if (!options.command) {
166
+ try {
167
+ await app.runInstaller({ port, host })
168
+ } catch (error) {
169
+ console.error(`installer error (continuing): ${error.message}`)
170
+ }
171
+
172
+ const mode = await daemon.start({ port, host })
173
+ const state = await daemon.status()
174
+ if (!state.running) {
175
+ console.error(`easy-vps did not start — ${state.detail}`)
176
+ process.exit(1)
177
+ }
178
+
179
+ console.log(`\n easy-vps v${app.version()} — started (${mode})\n`)
180
+ printUrls(port)
181
+ console.log('\n The panel is running in the background and will keep running')
182
+ console.log(' after this terminal is closed. Manage it with: status | stop | logs\n')
183
+ process.exit(0)
184
+ }
185
+
163
186
  let server
164
187
  try {
165
188
  server = await app.startServer({ port, host })
package/dist/index.d.ts CHANGED
@@ -20,3 +20,5 @@ export * as instances from './services/instances';
20
20
  export * as logs from './services/logs';
21
21
  export * as firewall from './services/firewall';
22
22
  export * as backupScheduler from './services/backup-scheduler';
23
+ export * as installer from './services/installer';
24
+ export { runInstaller } from './services/installer';
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.backupScheduler = exports.firewall = exports.logs = exports.instances = exports.database = exports.deploy = exports.domain = exports.system = exports.auth = exports.DEFAULT_HOST = exports.DEFAULT_PORT = void 0;
39
+ exports.runInstaller = exports.installer = exports.backupScheduler = exports.firewall = exports.logs = exports.instances = exports.database = exports.deploy = exports.domain = exports.system = exports.auth = exports.DEFAULT_HOST = exports.DEFAULT_PORT = void 0;
40
40
  exports.clientDir = clientDir;
41
41
  exports.createApp = createApp;
42
42
  exports.startServer = startServer;
@@ -141,3 +141,6 @@ exports.instances = __importStar(require("./services/instances"));
141
141
  exports.logs = __importStar(require("./services/logs"));
142
142
  exports.firewall = __importStar(require("./services/firewall"));
143
143
  exports.backupScheduler = __importStar(require("./services/backup-scheduler"));
144
+ exports.installer = __importStar(require("./services/installer"));
145
+ var installer_1 = require("./services/installer");
146
+ Object.defineProperty(exports, "runInstaller", { enumerable: true, get: function () { return installer_1.runInstaller; } });
@@ -74,7 +74,7 @@ After=network.target
74
74
 
75
75
  [Service]
76
76
  Type=simple
77
- ExecStart=${process.execPath} ${cliPath()} --port ${options.port} --host ${options.host}
77
+ ExecStart=${process.execPath} ${cliPath()} run --port ${options.port} --host ${options.host}
78
78
  Restart=on-failure
79
79
  RestartSec=5
80
80
  Environment=NODE_ENV=production
@@ -93,7 +93,7 @@ async function startSystemd(options) {
93
93
  /** Fallback: fully detached child that outlives the parent shell. */
94
94
  function startProcess(options) {
95
95
  const out = (0, node_fs_1.openSync)(logPath(), 'a');
96
- const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath(), '--port', String(options.port), '--host', options.host], { detached: true, stdio: ['ignore', out, out] });
96
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath(), 'run', '--port', String(options.port), '--host', options.host], { detached: true, stdio: ['ignore', out, out] });
97
97
  child.unref();
98
98
  (0, node_fs_1.writeFileSync)(pidPath(), String(child.pid), 'utf8');
99
99
  return child.pid;
@@ -0,0 +1,13 @@
1
+ export interface InstallerOptions {
2
+ port: number;
3
+ host: string;
4
+ }
5
+ /**
6
+ * Runs the animated "installing packages" step. Never throws: a failed install
7
+ * degrades to a warning and the caller still starts the panel.
8
+ */
9
+ export declare function runInstaller(options: InstallerOptions): Promise<void>;
10
+ declare const _default: {
11
+ runInstaller: typeof runInstaller;
12
+ };
13
+ export default _default;
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runInstaller = runInstaller;
7
+ // Animated installer: detects and installs the system packages easy-vps needs to
8
+ // operate as a VPS control panel (nginx, certbot, ufw, docker, postgres, pm2),
9
+ // then hands off to the daemon so the panel keeps running after the terminal closes.
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_fs_1 = require("node:fs");
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_util_1 = require("node:util");
15
+ const spinner_1 = require("./spinner");
16
+ const run = (0, node_util_1.promisify)(node_child_process_1.execFile);
17
+ function isRoot() {
18
+ return typeof process.getuid === 'function' && process.getuid() === 0;
19
+ }
20
+ function stateDir() {
21
+ const dir = isRoot() ? '/var/lib/easy-vps' : node_path_1.default.join(node_os_1.default.homedir(), '.easy-vps');
22
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
23
+ return dir;
24
+ }
25
+ // Packages easy-vps orchestrates. Node is required (already provided by the
26
+ // runtime running this script); the rest enable their respective panel features.
27
+ const PACKAGES = [
28
+ { name: 'Node.js', bin: 'node', manager: 'apt', pkg: 'nodejs', required: true, note: 'runtime' },
29
+ { name: 'nginx', bin: 'nginx', manager: 'apt', pkg: 'nginx', note: 'domains & reverse proxy' },
30
+ { name: 'certbot', bin: 'certbot', manager: 'apt', pkg: 'certbot', note: 'free HTTPS' },
31
+ { name: 'ufw', bin: 'ufw', manager: 'apt', pkg: 'ufw', note: 'firewall' },
32
+ { name: 'docker', bin: 'docker', manager: 'apt', pkg: 'docker.io', note: 'app deployment' },
33
+ { name: 'postgresql', bin: 'psql', manager: 'apt', pkg: 'postgresql', note: 'managed databases' },
34
+ { name: 'pm2', bin: 'pm2', manager: 'npm', pkg: 'pm2', note: 'node deployment' },
35
+ ];
36
+ async function hasBinary(bin) {
37
+ try {
38
+ await run('command', ['-v', bin]);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function sudoPrefix() {
46
+ if (isRoot())
47
+ return [];
48
+ // Only use sudo when it is actually available; otherwise the install steps warn.
49
+ if (await hasBinary('sudo'))
50
+ return ['sudo'];
51
+ return [];
52
+ }
53
+ async function aptUpdate(prefix) {
54
+ await run(prefix[0] ?? 'apt-get', [...(prefix[0] ? [] : []), 'apt-get', 'update', '-y'], {
55
+ shell: false,
56
+ });
57
+ }
58
+ async function installApt(pkg, prefix) {
59
+ const cmd = prefix.concat(['apt-get', 'install', '-y', pkg.pkg]);
60
+ await run(cmd[0], cmd.slice(1), { shell: false });
61
+ }
62
+ async function installNpm(pkg, prefix) {
63
+ const cmd = prefix.concat(['npm', 'install', '-g', pkg.pkg]);
64
+ await run(cmd[0], cmd.slice(1), { shell: false });
65
+ }
66
+ /**
67
+ * Runs the animated "installing packages" step. Never throws: a failed install
68
+ * degrades to a warning and the caller still starts the panel.
69
+ */
70
+ async function runInstaller(options) {
71
+ const marker = node_path_1.default.join(stateDir(), '.installed');
72
+ const skipInstall = process.env.EASY_VPS_NO_INSTALL === '1' || process.env.EASY_VPS_NO_INSTALL === 'true';
73
+ const reinstall = process.env.EASY_VPS_REINSTALL === '1' || process.env.EASY_VPS_REINSTALL === 'true';
74
+ // Header
75
+ process.stdout.write(`\n ${'easy-vps installer'}\n\n`);
76
+ if (skipInstall) {
77
+ process.stdout.write(' install step skipped (EASY_VPS_NO_INSTALL set)\n\n');
78
+ }
79
+ else if ((0, node_fs_1.existsSync)(marker) && !reinstall) {
80
+ process.stdout.write(' dependencies already installed — starting panel\n\n');
81
+ return;
82
+ }
83
+ const prefix = await sudoPrefix();
84
+ const needApt = PACKAGES.filter((p) => p.manager === 'apt' && !skipInstall);
85
+ const needNpm = PACKAGES.filter((p) => p.manager === 'npm' && !skipInstall);
86
+ // Refresh the apt index once if any apt package is missing.
87
+ if (needApt.length) {
88
+ const missingApt = [];
89
+ for (const p of needApt) {
90
+ if (!(await hasBinary(p.bin)))
91
+ missingApt.push(p);
92
+ }
93
+ if (missingApt.length) {
94
+ const spinner = new spinner_1.Spinner('updating package index');
95
+ spinner.start();
96
+ try {
97
+ await aptUpdate(prefix);
98
+ spinner.succeed('package index updated');
99
+ }
100
+ catch (error) {
101
+ spinner.warn(`could not update index (${error.message.split('\n')[0]})`);
102
+ }
103
+ }
104
+ }
105
+ // Install each package, animating as we go.
106
+ for (const pkg of PACKAGES) {
107
+ if (skipInstall)
108
+ break;
109
+ const spinner = new spinner_1.Spinner(`installing ${pkg.name}`);
110
+ spinner.start();
111
+ try {
112
+ if (await hasBinary(pkg.bin)) {
113
+ spinner.succeed(pkg.note ?? 'already installed');
114
+ continue;
115
+ }
116
+ if (pkg.manager === 'apt')
117
+ await installApt(pkg, prefix);
118
+ else
119
+ await installNpm(pkg, prefix);
120
+ spinner.succeed(pkg.note ?? 'installed');
121
+ }
122
+ catch (error) {
123
+ const detail = error.message.split('\n')[0];
124
+ if (pkg.required) {
125
+ spinner.warn(`not available (${detail})`);
126
+ }
127
+ else {
128
+ spinner.warn(`skipped (${detail})`);
129
+ }
130
+ }
131
+ }
132
+ process.stdout.write('\n');
133
+ try {
134
+ (0, node_fs_1.writeFileSync)(marker, `${new Date().toISOString()}\nport=${options.port} host=${options.host}\n`, 'utf8');
135
+ }
136
+ catch {
137
+ // marker is best-effort
138
+ }
139
+ }
140
+ exports.default = { runInstaller };
@@ -0,0 +1,21 @@
1
+ export declare class Spinner {
2
+ private fd;
3
+ private readonly color;
4
+ private readonly bold;
5
+ private readonly dim;
6
+ private readonly green;
7
+ private readonly yellow;
8
+ private readonly reset;
9
+ private readonly clear;
10
+ private timer;
11
+ private frame;
12
+ private readonly label;
13
+ constructor(label: string);
14
+ private write;
15
+ private render;
16
+ start(): this;
17
+ private stop;
18
+ succeed(detail?: string): void;
19
+ warn(detail?: string): void;
20
+ close(): void;
21
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Spinner = void 0;
4
+ // Animated terminal spinner for the install flow.
5
+ //
6
+ // `npx` may pipe the child and hide stdout, so we write to the controlling
7
+ // terminal (/dev/tty) when one exists and fall back to stdout otherwise.
8
+ const node_fs_1 = require("node:fs");
9
+ const ESC = String.fromCharCode(27);
10
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
11
+ function openTty() {
12
+ try {
13
+ return (0, node_fs_1.openSync)('/dev/tty', 'w');
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ class Spinner {
20
+ fd = null;
21
+ color;
22
+ bold;
23
+ dim;
24
+ green;
25
+ yellow;
26
+ reset;
27
+ clear;
28
+ timer = null;
29
+ frame = 0;
30
+ label;
31
+ constructor(label) {
32
+ this.fd = openTty();
33
+ this.color = this.fd !== null && !process.env.NO_COLOR;
34
+ this.bold = this.color ? `${ESC}[1m` : '';
35
+ this.dim = this.color ? `${ESC}[2m` : '';
36
+ this.green = this.color ? `${ESC}[32m` : '';
37
+ this.yellow = this.color ? `${ESC}[33m` : '';
38
+ this.reset = this.color ? `${ESC}[0m` : '';
39
+ this.clear = this.fd !== null ? `${ESC}[K` : '';
40
+ this.label = label;
41
+ }
42
+ write(text) {
43
+ try {
44
+ if (this.fd !== null)
45
+ (0, node_fs_1.writeSync)(this.fd, text);
46
+ else
47
+ process.stdout.write(text);
48
+ }
49
+ catch {
50
+ // terminal went away; nothing to do
51
+ }
52
+ }
53
+ render() {
54
+ const glyph = this.color ? FRAMES[this.frame % FRAMES.length] : '·';
55
+ this.write(`${this.clear}\r${this.dim}${glyph}${this.reset} ${this.label}`);
56
+ this.frame++;
57
+ }
58
+ start() {
59
+ this.render();
60
+ this.timer = setInterval(() => this.render(), 80);
61
+ if (this.timer.unref)
62
+ this.timer.unref();
63
+ return this;
64
+ }
65
+ stop(final) {
66
+ if (this.timer) {
67
+ clearInterval(this.timer);
68
+ this.timer = null;
69
+ }
70
+ this.write(`${this.clear}\r${final}\n`);
71
+ }
72
+ succeed(detail) {
73
+ const extra = detail ? ` ${this.dim}(${detail})${this.reset}` : '';
74
+ this.stop(`${this.green}✔${this.reset} ${this.label}${extra}`);
75
+ }
76
+ warn(detail) {
77
+ const extra = detail ? ` ${this.dim}(${detail})${this.reset}` : '';
78
+ this.stop(`${this.yellow}!${this.reset} ${this.label}${extra}`);
79
+ }
80
+ close() {
81
+ if (this.timer)
82
+ this.stop('');
83
+ if (this.fd !== null) {
84
+ try {
85
+ (0, node_fs_1.closeSync)(this.fd);
86
+ }
87
+ catch {
88
+ // ignore
89
+ }
90
+ this.fd = null;
91
+ }
92
+ }
93
+ }
94
+ exports.Spinner = Spinner;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-vps",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Install dependencies, point domains, deploy apps and run Postgres on a VPS from one UI",
5
5
  "keywords": [
6
6
  "vps",