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,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createS3Client = createS3Client;
4
+ exports.backupS3Key = backupS3Key;
5
+ exports.backupPrefix = backupPrefix;
6
+ exports.uploadToS3 = uploadToS3;
7
+ exports.listS3Backups = listS3Backups;
8
+ exports.downloadFromS3 = downloadFromS3;
9
+ exports.deleteFromS3 = deleteFromS3;
10
+ exports.enforceRetention = enforceRetention;
11
+ // S3-compatible storage operations for backup uploads/downloads.
12
+ const client_s3_1 = require("@aws-sdk/client-s3");
13
+ const node_fs_1 = require("node:fs");
14
+ const BACKUP_PREFIX = 'easy-vps/backups/database';
15
+ function createS3Client(config) {
16
+ return new client_s3_1.S3Client({
17
+ region: config.region,
18
+ endpoint: config.region.includes('.') ? undefined : `https://s3.${config.region}.amazonaws.com`,
19
+ credentials: {
20
+ accessKeyId: config.accessKey,
21
+ secretAccessKey: config.secretKey,
22
+ },
23
+ forcePathStyle: false,
24
+ });
25
+ }
26
+ function backupS3Key(engine, dbName, timestamp) {
27
+ return `${BACKUP_PREFIX}/${engine}/${dbName}/${timestamp}/backup.dmp`;
28
+ }
29
+ function backupPrefix(engine, dbName) {
30
+ return `${BACKUP_PREFIX}/${engine}/${dbName}/`;
31
+ }
32
+ async function uploadToS3(s3Config, localPath, s3Key) {
33
+ const client = createS3Client(s3Config);
34
+ const body = (0, node_fs_1.readFileSync)(localPath);
35
+ await client.send(new client_s3_1.PutObjectCommand({
36
+ Bucket: s3Config.bucket,
37
+ Key: s3Key,
38
+ Body: body,
39
+ }));
40
+ return { key: s3Key };
41
+ }
42
+ async function listS3Backups(s3Config, engine, dbName) {
43
+ const client = createS3Client(s3Config);
44
+ const prefix = backupPrefix(engine, dbName);
45
+ const result = await client.send(new client_s3_1.ListObjectsV2Command({
46
+ Bucket: s3Config.bucket,
47
+ Prefix: prefix,
48
+ }));
49
+ return (result.Contents ?? [])
50
+ .filter((obj) => obj.Key && obj.Key.endsWith('backup.dmp'))
51
+ .map((obj) => ({
52
+ key: obj.Key,
53
+ size: obj.Size ?? 0,
54
+ lastModified: obj.LastModified ?? new Date(),
55
+ }))
56
+ .sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
57
+ }
58
+ async function downloadFromS3(s3Config, s3Key, localPath) {
59
+ const client = createS3Client(s3Config);
60
+ const result = await client.send(new client_s3_1.GetObjectCommand({
61
+ Bucket: s3Config.bucket,
62
+ Key: s3Key,
63
+ }));
64
+ const stream = result.Body;
65
+ if (!stream)
66
+ throw new Error('Empty response from S3');
67
+ const chunks = [];
68
+ const reader = stream.transformToWebStream().getReader();
69
+ let done = false;
70
+ while (!done) {
71
+ const { value, done: readerDone } = await reader.read();
72
+ done = readerDone;
73
+ if (value)
74
+ chunks.push(value);
75
+ }
76
+ (0, node_fs_1.writeFileSync)(localPath, Buffer.concat(chunks));
77
+ }
78
+ async function deleteFromS3(s3Config, s3Key) {
79
+ const client = createS3Client(s3Config);
80
+ await client.send(new client_s3_1.DeleteObjectCommand({
81
+ Bucket: s3Config.bucket,
82
+ Key: s3Key,
83
+ }));
84
+ }
85
+ async function enforceRetention(s3Config, engine, dbName, retention) {
86
+ const backups = await listS3Backups(s3Config, engine, dbName);
87
+ if (backups.length <= retention)
88
+ return;
89
+ const toDelete = backups.slice(retention);
90
+ for (const backup of toDelete) {
91
+ await deleteFromS3(s3Config, backup.key);
92
+ }
93
+ }
@@ -0,0 +1,78 @@
1
+ export declare class RemoteError extends Error {
2
+ }
3
+ export interface Credentials {
4
+ host: string;
5
+ port: number;
6
+ username: string;
7
+ password?: string;
8
+ privateKey?: string;
9
+ passphrase?: string;
10
+ }
11
+ export interface ExecResult {
12
+ stdout: string;
13
+ stderr: string;
14
+ code: number;
15
+ }
16
+ export interface StreamHandlers {
17
+ onOutput: (line: string) => void;
18
+ onDone: () => void;
19
+ onError: (message: string) => void;
20
+ }
21
+ /** Wraps a value for safe interpolation into a remote /bin/sh command line. */
22
+ export declare function quote(value: string): string;
23
+ /**
24
+ * Runs a command through a login shell so it sees the PATH the server's own
25
+ * profile sets up. Without this, tools installed outside the system prefix —
26
+ * nvm's node/npm, and pm2 alongside them — are invisible to `ssh host cmd`,
27
+ * because that runs a non-interactive, non-login shell.
28
+ */
29
+ export declare function loginShell(command: string): string;
30
+ /**
31
+ * One managed VPS, bound to a logged-in session. Holds a single SSH connection
32
+ * and reopens it transparently if the server drops it while the session is idle.
33
+ */
34
+ export declare class Remote {
35
+ private readonly credentials;
36
+ private client;
37
+ private pending;
38
+ private cachedUid;
39
+ private active;
40
+ private readonly waiting;
41
+ constructor(credentials: Credentials);
42
+ get host(): string;
43
+ get username(): string;
44
+ get creds(): Credentials;
45
+ private open;
46
+ /** A ready connection, reused across calls and shared by concurrent ones. */
47
+ private connection;
48
+ /** Waits for a free channel slot; returns the function that releases it. */
49
+ private acquire;
50
+ private channel;
51
+ /**
52
+ * Runs a command to completion. Always resolves with the exit code — callers
53
+ * decide what a non-zero status means. The timeout keeps a wedged binary on
54
+ * the server from hanging an HTTP request indefinitely.
55
+ */
56
+ exec(command: string, timeoutMs?: number): Promise<ExecResult>;
57
+ /** Like exec(), but throws when the command fails. */
58
+ execOrThrow(command: string, timeoutMs?: number): Promise<string>;
59
+ /** Streams a long-running command line by line. Returns a cancel function. */
60
+ execStream(command: string, handlers: StreamHandlers): () => void;
61
+ /** Numeric uid of the logged-in remote user; 0 means the panel can act unprivileged. */
62
+ uid(): Promise<number>;
63
+ isRoot(): Promise<boolean>;
64
+ /** Prefixes a command with non-interactive sudo unless already root. */
65
+ privileged(command: string): Promise<string>;
66
+ readFile(remotePath: string): Promise<string>;
67
+ /** Writes via `tee` so the same sudo path covers root-owned destinations. */
68
+ writeFile(remotePath: string, contents: string): Promise<void>;
69
+ exists(remotePath: string): Promise<boolean>;
70
+ /** Directory entries, or an empty list when the directory is absent. */
71
+ listDir(remotePath: string): Promise<string[]>;
72
+ remove(remotePath: string): Promise<void>;
73
+ symlink(target: string, linkPath: string): Promise<void>;
74
+ close(): void;
75
+ }
76
+ export declare function createSession(id: string, credentials: Credentials): Remote;
77
+ export declare function getSession(id: string): Remote | null;
78
+ export declare function destroySession(id: string): void;
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Remote = exports.RemoteError = void 0;
4
+ exports.quote = quote;
5
+ exports.loginShell = loginShell;
6
+ exports.createSession = createSession;
7
+ exports.getSession = getSession;
8
+ exports.destroySession = destroySession;
9
+ // Runs commands on the managed VPS over SSH. Every operation the panel performs
10
+ // on the server goes through here — the panel process itself never installs
11
+ // packages or writes config on the machine it happens to be running on.
12
+ const ssh2_1 = require("ssh2");
13
+ class RemoteError extends Error {
14
+ }
15
+ exports.RemoteError = RemoteError;
16
+ const CONNECT_TIMEOUT_MS = 8000;
17
+ const DEFAULT_EXEC_TIMEOUT_MS = 30_000;
18
+ // sshd allows a limited number of concurrent channels per connection
19
+ // (MaxSessions, 10 by default) and rejects the rest with "open failed".
20
+ // Short-lived exec() calls queue behind this so a wide Promise.all — ten
21
+ // dependency probes at once — cannot exhaust the connection.
22
+ const MAX_CONCURRENT_EXECS = 4;
23
+ /** Wraps a value for safe interpolation into a remote /bin/sh command line. */
24
+ function quote(value) {
25
+ return `'${value.replace(/'/g, `'\\''`)}'`;
26
+ }
27
+ /**
28
+ * Runs a command through a login shell so it sees the PATH the server's own
29
+ * profile sets up. Without this, tools installed outside the system prefix —
30
+ * nvm's node/npm, and pm2 alongside them — are invisible to `ssh host cmd`,
31
+ * because that runs a non-interactive, non-login shell.
32
+ */
33
+ function loginShell(command) {
34
+ return `bash -lc ${quote(command)}`;
35
+ }
36
+ function emitLines(chunk, onOutput) {
37
+ for (const line of chunk.toString('utf8').split('\n')) {
38
+ if (line.trim())
39
+ onOutput(line);
40
+ }
41
+ }
42
+ /** `sudo -n` fails loudly rather than hanging on a password prompt; say so usefully. */
43
+ function sudoFailure(stderr, username) {
44
+ if (/sudo: a (password|terminal) is required|sudo: no tty/i.test(stderr)) {
45
+ return `Passwordless sudo is not enabled for "${username}" on this server. Enable it, or log in as root.`;
46
+ }
47
+ if (/is not in the sudoers file/i.test(stderr)) {
48
+ return `"${username}" is not permitted to use sudo on this server. Log in as root instead.`;
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * One managed VPS, bound to a logged-in session. Holds a single SSH connection
54
+ * and reopens it transparently if the server drops it while the session is idle.
55
+ */
56
+ class Remote {
57
+ credentials;
58
+ client = null;
59
+ pending = null;
60
+ cachedUid = null;
61
+ active = 0;
62
+ waiting = [];
63
+ constructor(credentials) {
64
+ this.credentials = credentials;
65
+ }
66
+ get host() {
67
+ return this.credentials.host;
68
+ }
69
+ get username() {
70
+ return this.credentials.username;
71
+ }
72
+ get creds() {
73
+ return this.credentials;
74
+ }
75
+ open() {
76
+ return new Promise((resolve, reject) => {
77
+ const client = new ssh2_1.Client();
78
+ const timer = setTimeout(() => {
79
+ client.end();
80
+ reject(new RemoteError('SSH handshake timed out'));
81
+ }, CONNECT_TIMEOUT_MS);
82
+ client
83
+ .on('ready', () => {
84
+ clearTimeout(timer);
85
+ this.client = client;
86
+ resolve(client);
87
+ })
88
+ .on('error', (error) => {
89
+ clearTimeout(timer);
90
+ this.client = null;
91
+ reject(new RemoteError(error.message));
92
+ })
93
+ .on('close', () => {
94
+ // Next call reconnects rather than writing to a dead channel.
95
+ this.client = null;
96
+ })
97
+ .connect({
98
+ host: this.credentials.host,
99
+ port: this.credentials.port,
100
+ username: this.credentials.username,
101
+ password: this.credentials.password,
102
+ privateKey: this.credentials.privateKey,
103
+ passphrase: this.credentials.passphrase,
104
+ readyTimeout: CONNECT_TIMEOUT_MS,
105
+ keepaliveInterval: 15_000,
106
+ });
107
+ });
108
+ }
109
+ /** A ready connection, reused across calls and shared by concurrent ones. */
110
+ async connection() {
111
+ if (this.client)
112
+ return this.client;
113
+ this.pending ??= this.open().finally(() => {
114
+ this.pending = null;
115
+ });
116
+ return this.pending;
117
+ }
118
+ /** Waits for a free channel slot; returns the function that releases it. */
119
+ async acquire() {
120
+ if (this.active >= MAX_CONCURRENT_EXECS) {
121
+ await new Promise((resolve) => this.waiting.push(resolve));
122
+ }
123
+ this.active += 1;
124
+ let released = false;
125
+ return () => {
126
+ if (released)
127
+ return;
128
+ released = true;
129
+ this.active -= 1;
130
+ this.waiting.shift()?.();
131
+ };
132
+ }
133
+ async channel(command) {
134
+ const client = await this.connection();
135
+ return new Promise((resolve, reject) => {
136
+ client.exec(command, (error, stream) => {
137
+ if (error)
138
+ reject(new RemoteError(error.message));
139
+ else
140
+ resolve(stream);
141
+ });
142
+ });
143
+ }
144
+ /**
145
+ * Runs a command to completion. Always resolves with the exit code — callers
146
+ * decide what a non-zero status means. The timeout keeps a wedged binary on
147
+ * the server from hanging an HTTP request indefinitely.
148
+ */
149
+ async exec(command, timeoutMs = DEFAULT_EXEC_TIMEOUT_MS) {
150
+ const release = await this.acquire();
151
+ try {
152
+ const stream = await this.channel(command);
153
+ return await new Promise((resolve, reject) => {
154
+ let stdout = '';
155
+ let stderr = '';
156
+ const timer = setTimeout(() => {
157
+ stream.close();
158
+ reject(new RemoteError(`Timed out after ${Math.round(timeoutMs / 1000)}s: ${command}`));
159
+ }, timeoutMs);
160
+ stream.on('data', (chunk) => {
161
+ stdout += chunk.toString('utf8');
162
+ });
163
+ stream.stderr.on('data', (chunk) => {
164
+ stderr += chunk.toString('utf8');
165
+ });
166
+ stream.on('close', (code) => {
167
+ clearTimeout(timer);
168
+ resolve({ stdout, stderr, code: code ?? 0 });
169
+ });
170
+ stream.on('error', (error) => {
171
+ clearTimeout(timer);
172
+ reject(new RemoteError(error.message));
173
+ });
174
+ });
175
+ }
176
+ finally {
177
+ release();
178
+ }
179
+ }
180
+ /** Like exec(), but throws when the command fails. */
181
+ async execOrThrow(command, timeoutMs) {
182
+ const result = await this.exec(command, timeoutMs);
183
+ if (result.code !== 0) {
184
+ const detail = result.stderr.trim() || `Command failed: ${command}`;
185
+ throw new RemoteError(sudoFailure(result.stderr, this.username) ?? detail);
186
+ }
187
+ return result.stdout;
188
+ }
189
+ /** Streams a long-running command line by line. Returns a cancel function. */
190
+ execStream(command, handlers) {
191
+ let cancelled = false;
192
+ let stream = null;
193
+ void this.channel(command)
194
+ .then((channel) => {
195
+ if (cancelled) {
196
+ channel.close();
197
+ return;
198
+ }
199
+ stream = channel;
200
+ channel.on('data', (chunk) => emitLines(chunk, handlers.onOutput));
201
+ channel.stderr.on('data', (chunk) => emitLines(chunk, handlers.onOutput));
202
+ channel.on('close', (code) => {
203
+ if (cancelled)
204
+ return;
205
+ if (code === 0)
206
+ handlers.onDone();
207
+ else
208
+ handlers.onError(`Command exited with code ${code ?? 'unknown'}`);
209
+ });
210
+ channel.on('error', (error) => {
211
+ if (!cancelled)
212
+ handlers.onError(error.message);
213
+ });
214
+ })
215
+ .catch((error) => {
216
+ if (!cancelled)
217
+ handlers.onError(error.message);
218
+ });
219
+ return () => {
220
+ cancelled = true;
221
+ stream?.close();
222
+ };
223
+ }
224
+ /** Numeric uid of the logged-in remote user; 0 means the panel can act unprivileged. */
225
+ async uid() {
226
+ if (this.cachedUid !== null)
227
+ return this.cachedUid;
228
+ const { stdout } = await this.exec('id -u', 10_000);
229
+ const uid = Number(stdout.trim());
230
+ this.cachedUid = Number.isInteger(uid) ? uid : -1;
231
+ return this.cachedUid;
232
+ }
233
+ async isRoot() {
234
+ return (await this.uid()) === 0;
235
+ }
236
+ /** Prefixes a command with non-interactive sudo unless already root. */
237
+ async privileged(command) {
238
+ return (await this.isRoot()) ? command : `sudo -n ${command}`;
239
+ }
240
+ async readFile(remotePath) {
241
+ return this.execOrThrow(await this.privileged(`cat ${quote(remotePath)}`));
242
+ }
243
+ /** Writes via `tee` so the same sudo path covers root-owned destinations. */
244
+ async writeFile(remotePath, contents) {
245
+ const command = await this.privileged(`tee ${quote(remotePath)} > /dev/null`);
246
+ const release = await this.acquire();
247
+ const stream = await this.channel(command).catch((error) => {
248
+ release();
249
+ throw error;
250
+ });
251
+ await new Promise((resolve, reject) => {
252
+ let stderr = '';
253
+ // Both pipes must be drained or the channel never closes and this hangs.
254
+ stream.on('data', () => undefined);
255
+ stream.stderr.on('data', (chunk) => {
256
+ stderr += chunk.toString('utf8');
257
+ });
258
+ stream.on('close', (code) => {
259
+ if (code === 0)
260
+ resolve();
261
+ else {
262
+ const detail = stderr.trim() || `Could not write ${remotePath}`;
263
+ reject(new RemoteError(sudoFailure(stderr, this.username) ?? detail));
264
+ }
265
+ });
266
+ stream.on('error', (error) => reject(new RemoteError(error.message)));
267
+ stream.end(contents);
268
+ }).finally(release);
269
+ }
270
+ async exists(remotePath) {
271
+ const command = await this.privileged(`test -e ${quote(remotePath)}`);
272
+ return (await this.exec(command, 10_000)).code === 0;
273
+ }
274
+ /** Directory entries, or an empty list when the directory is absent. */
275
+ async listDir(remotePath) {
276
+ const command = await this.privileged(`ls -1 ${quote(remotePath)} 2>/dev/null`);
277
+ const { stdout, code } = await this.exec(command, 10_000);
278
+ if (code !== 0)
279
+ return [];
280
+ return stdout.split('\n').map((line) => line.trim()).filter(Boolean);
281
+ }
282
+ async remove(remotePath) {
283
+ await this.exec(await this.privileged(`rm -f ${quote(remotePath)}`), 10_000);
284
+ }
285
+ async symlink(target, linkPath) {
286
+ await this.execOrThrow(await this.privileged(`ln -sfn ${quote(target)} ${quote(linkPath)}`), 10_000);
287
+ }
288
+ close() {
289
+ this.client?.end();
290
+ this.client = null;
291
+ }
292
+ }
293
+ exports.Remote = Remote;
294
+ // Live sessions, keyed by the id carried in the session cookie. Credentials stay
295
+ // in this process only: a restart empties the map and everyone logs in again.
296
+ const sessions = new Map();
297
+ function createSession(id, credentials) {
298
+ sessions.get(id)?.close();
299
+ const remote = new Remote(credentials);
300
+ sessions.set(id, remote);
301
+ return remote;
302
+ }
303
+ function getSession(id) {
304
+ return sessions.get(id) ?? null;
305
+ }
306
+ function destroySession(id) {
307
+ sessions.get(id)?.close();
308
+ sessions.delete(id);
309
+ }
@@ -0,0 +1,74 @@
1
+ import { type DependencyName } from './packages';
2
+ import { type Remote, type StreamHandlers } from './ssh';
3
+ export type { DependencyName } from './packages';
4
+ export { PACKAGES } from './packages';
5
+ export type PackageAction = 'install' | 'uninstall';
6
+ export interface DependencyStatus {
7
+ name: DependencyName;
8
+ installed: boolean;
9
+ version: string | null;
10
+ /** Paths its uninstall deletes, so the UI can warn before running it. */
11
+ removes: string[];
12
+ }
13
+ export interface CpuInfo {
14
+ model: string;
15
+ cores: number;
16
+ usagePercent: number;
17
+ }
18
+ export interface MemoryInfo {
19
+ totalMb: number;
20
+ usedMb: number;
21
+ freeMb: number;
22
+ availableMb: number;
23
+ usagePercent: number;
24
+ }
25
+ export interface DiskInfo {
26
+ totalGb: number;
27
+ usedGb: number;
28
+ freeGb: number;
29
+ usagePercent: number;
30
+ }
31
+ export interface SwapInfo {
32
+ totalMb: number;
33
+ usedMb: number;
34
+ freeMb: number;
35
+ usagePercent: number;
36
+ }
37
+ export interface LoadAverage {
38
+ one: number;
39
+ five: number;
40
+ fifteen: number;
41
+ }
42
+ export interface NetworkInfo {
43
+ rxMb: number;
44
+ txMb: number;
45
+ }
46
+ export interface TopProcess {
47
+ name: string;
48
+ cpu: number;
49
+ memory: number;
50
+ pid: number;
51
+ }
52
+ export interface SystemMetrics {
53
+ cpu: CpuInfo;
54
+ memory: MemoryInfo;
55
+ disk: DiskInfo;
56
+ swap: SwapInfo;
57
+ uptime: string;
58
+ uptimeSeconds: number;
59
+ loadAverage: LoadAverage;
60
+ network: NetworkInfo;
61
+ topProcesses: TopProcess[];
62
+ }
63
+ /** Collect comprehensive system metrics from the VPS. */
64
+ export declare function getMetrics(remote: Remote): Promise<SystemMetrics>;
65
+ /** Detect whether a dependency is present on the VPS. */
66
+ export declare function detect(remote: Remote, name: DependencyName): Promise<DependencyStatus>;
67
+ export declare function detectAll(remote: Remote): Promise<DependencyStatus[]>;
68
+ /** True when the logged-in remote user can run privileged commands unattended. */
69
+ export declare function canAdminister(remote: Remote): Promise<boolean>;
70
+ /**
71
+ * Installs or removes a dependency on the VPS, streaming stdout/stderr line by
72
+ * line. Returns a function that cancels the run in progress.
73
+ */
74
+ export declare function packageStream(remote: Remote, name: DependencyName, action: PackageAction, handlers: StreamHandlers): () => void;