easy-vps 0.1.1 → 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/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.1",
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",