elestio 1.0.3 → 1.2.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.
@@ -1,294 +1,306 @@
1
- import { apiRequest } from '../api.js';
2
- import { loadConfig } from '../config.js';
3
- import { findTemplate } from './templates.js';
4
- import { formatTable, formatService, formatPrice, colors, log, sleep, validateServerName, outputJson } from '../utils.js';
5
-
6
- async function listProjectsRaw() {
7
- const response = await apiRequest('/api/projects/getList');
8
- if (response.status !== 'OK') return [];
9
- return response.data?.projects || [];
10
- }
11
-
12
- export async function findServiceAcrossProjects(vmID) {
13
- const projects = await listProjectsRaw();
14
- for (const project of projects) {
15
- const pid = project.projectID;
16
- try {
17
- const services = await listServicesRaw(pid);
18
- const svc = services.find(s => String(s.vmID) === String(vmID));
19
- if (svc) return { service: svc, projectId: pid, projectName: project.project_name };
20
- } catch { /* skip */ }
21
- }
22
- return null;
23
- }
24
-
25
- export async function listServicesRaw(projectId = null) {
26
- const config = loadConfig();
27
- const pid = projectId || config.defaultProject;
28
- if (!pid) throw new Error('Project ID required. Use --project or set default with: elestio config --set-default-project <id>');
29
-
30
- const response = await apiRequest('/api/servers/getServices', 'POST', {
31
- appid: 'Cloudxx',
32
- projectId: String(pid),
33
- isActiveService: 'true'
34
- });
35
-
36
- return response.servers || response.data?.services || [];
37
- }
38
-
39
- export async function listServices(projectId = null, json = false) {
40
- const config = loadConfig();
41
- const pid = projectId || config.defaultProject;
42
- if (!pid) throw new Error('Project ID required. Use --project or set default with: elestio config --set-default-project <id>');
43
-
44
- const response = await apiRequest('/api/servers/getServices', 'POST', {
45
- appid: 'Cloudxx',
46
- projectId: String(pid),
47
- isActiveService: 'true'
48
- });
49
-
50
- if (response.status === 'KO' || response.code === 'AccessDenied') {
51
- throw new Error(response.message || 'Access denied.');
52
- }
53
-
54
- const services = response.servers || response.data?.services || [];
55
-
56
- if (json) {
57
- outputJson(services);
58
- return services;
59
- }
60
-
61
- if (services.length === 0) {
62
- log('info', `No services in project ${pid}`);
63
- return [];
64
- }
65
-
66
- const columns = [
67
- { key: 'displayName', label: 'Name' },
68
- { key: 'templateName', label: 'Software' },
69
- { key: 'status', label: 'Status' },
70
- { key: 'deploymentStatus', label: 'Deploy' },
71
- { key: 'vmID', label: 'vmID' },
72
- { key: 'ipv4', label: 'IP' }
73
- ];
74
-
75
- console.log(`\n${colors.bold}Services in project ${pid} (${services.length})${colors.reset}\n`);
76
- console.log(formatTable(services, columns));
77
- console.log('');
78
- return services;
79
- }
80
-
81
- export async function getServiceDetails(vmID, projectId = null) {
82
- const config = loadConfig();
83
- const pid = projectId || config.defaultProject;
84
- if (!pid) throw new Error('Project ID required');
85
-
86
- const response = await apiRequest('/api/servers/getServerDetails', 'POST', {
87
- vmID: String(vmID),
88
- projectID: String(pid)
89
- });
90
-
91
- if (response.serviceInfos && response.serviceInfos.length > 0) return response.serviceInfos[0];
92
- if (response.status === 'OK' && response.data) return response.data;
93
- if (response.status === 'KO' || response.message) throw new Error(response.message || 'Failed to get service details');
94
- return null;
95
- }
96
-
97
- export async function getServiceByVmId(vmID, projectId = null) {
98
- const services = await listServicesRaw(projectId);
99
- return services.find(s => String(s.vmID) === String(vmID));
100
- }
101
-
102
- export async function getServerIdFromVmId(vmID, projectId = null) {
103
- const service = await getServiceByVmId(vmID, projectId);
104
- if (!service) throw new Error(`Service with vmID ${vmID} not found`);
105
- return service.id;
106
- }
107
-
108
- export async function deployService(templateNameOrId, options = {}) {
109
- const config = loadConfig();
110
- const template = await findTemplate(templateNameOrId);
111
- if (!template) throw new Error(`Template "${templateNameOrId}" not found. Use: elestio templates search <name>`);
112
-
113
- const projectId = options.project || config.defaultProject;
114
- if (!projectId) throw new Error('Project ID required. Use --project or set default');
115
-
116
- const serverName = options.name || `${template.title.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Date.now().toString(36)}`;
117
- const serverType = options.size || config.defaults?.serverType || 'MEDIUM-2C-4G';
118
- const datacenter = options.region || config.defaults?.datacenter || 'nbg';
119
- const support = options.support || config.defaults?.support || 'level1';
120
- const adminEmail = options.email || config.email;
121
- const provider = options.provider || config.defaults?.provider || 'netcup';
122
- const version = options.version || template.dockerhub_default_tag || 'latest';
123
-
124
- const nameValidation = validateServerName(serverName);
125
- if (!nameValidation.valid) throw new Error(nameValidation.error);
126
-
127
- const isCicd = template.title?.toLowerCase().includes('ci-cd') || templateNameOrId?.toLowerCase() === 'cicd';
128
- const serviceType = isCicd ? 'CICD' : 'Service';
129
-
130
- if (options.dryRun) {
131
- const preview = {
132
- template: template.title, templateId: template.id, version,
133
- projectId, serverName, provider, datacenter, serverType, support, adminEmail, serviceType
134
- };
135
-
136
- if (options.json) {
137
- outputJson({ dryRun: true, ...preview });
138
- return preview;
139
- }
140
-
141
- console.log(`\n${colors.bold}Deployment Preview (--dry-run)${colors.reset}\n`);
142
- console.log(` Software: ${colors.cyan}${template.title}${colors.reset} (ID: ${template.id})`);
143
- console.log(` Version: ${version}`);
144
- console.log(` Project: ${projectId}`);
145
- console.log(` Name: ${serverName}`);
146
- console.log(` Provider: ${provider}`);
147
- console.log(` Region: ${datacenter}`);
148
- console.log(` Size: ${serverType}`);
149
- console.log(` Support: ${support}`);
150
- console.log(` Admin: ${adminEmail}`);
151
- console.log('');
152
- log('info', 'To deploy, run the same command without --dry-run');
153
- return preview;
154
- }
155
-
156
- log('info', `Deploying ${template.title} (ID: ${template.id})`);
157
- log('info', ` Project: ${projectId} | Name: ${serverName}`);
158
- log('info', ` Provider: ${provider} | Size: ${serverType} @ ${datacenter}`);
159
-
160
- const payload = {
161
- templateID: String(template.id), serverType, datacenter,
162
- providerName: provider, serverName, appid: 'Cloudxx',
163
- data: 'data', support, projectId: String(projectId),
164
- version, adminEmail, deploymentServiceType: 'normal', serviceType
165
- };
166
-
167
- if (isCicd) {
168
- payload.cicdPayload = { pipelineName: options.pipelineName || serverName };
169
- }
170
-
171
- const response = await apiRequest('/api/servers/createServer', 'POST', payload);
172
-
173
- if (!response.providerServerID && !response.action) {
174
- throw new Error(response.message || 'Failed to create service');
175
- }
176
-
177
- log('success', `Deployment started! Provider Server ID: ${response.providerServerID}`);
178
-
179
- if (options.wait !== false) {
180
- log('info', 'Waiting for deployment to complete...');
181
- return await waitForDeployment(response.providerServerID, projectId, options.timeout);
182
- }
183
-
184
- return response;
185
- }
186
-
187
- export async function waitForDeployment(vmID, projectId, timeoutMs = 600000) {
188
- const start = Date.now();
189
- let lastStatus = '';
190
-
191
- while (Date.now() - start < timeoutMs) {
192
- const services = await listServicesRaw(projectId);
193
- const svc = services.find(s =>
194
- String(s.vmID) === String(vmID) || String(s.providerServerID) === String(vmID)
195
- );
196
-
197
- if (!svc) { await sleep(10000); continue; }
198
-
199
- if (svc.deploymentStatus !== lastStatus) {
200
- lastStatus = svc.deploymentStatus;
201
- log('info', `Status: ${svc.deploymentStatus}`);
202
- }
203
-
204
- if (svc.deploymentStatus === 'Deployed' && svc.status === 'running') {
205
- log('success', 'Deployment complete!');
206
- console.log('\n' + formatService(svc) + '\n');
207
- return svc;
208
- }
209
-
210
- await sleep(15000);
211
- }
212
-
213
- throw new Error(`Deployment timed out after ${timeoutMs / 1000}s`);
214
- }
215
-
216
- export async function deleteService(vmID, options = {}) {
217
- if (!options.force) throw new Error('Deleting a service requires --force flag');
218
-
219
- const config = loadConfig();
220
- const projectId = options.project || config.defaultProject;
221
- if (!projectId) throw new Error('Project ID required');
222
-
223
- const response = await apiRequest('/api/servers/deleteServer', 'POST', {
224
- vmID: String(vmID),
225
- projectID: String(projectId),
226
- isDeleteServiceWithBackup: options.withBackups ? 'true' : 'false'
227
- });
228
-
229
- if (response.status !== 'OK' && !response.action) {
230
- throw new Error(response.message || 'Failed to delete service');
231
- }
232
-
233
- log('success', `Service ${vmID} deletion initiated`);
234
- return response;
235
- }
236
-
237
- export async function moveService(vmIDOrServerID, targetProjectId, sourceProjectId = null) {
238
- const config = loadConfig();
239
- const sourcePid = sourceProjectId || config.defaultProject;
240
-
241
- let serverID = vmIDOrServerID;
242
- let foundInProject = null;
243
-
244
- try {
245
- const service = await getServiceByVmId(vmIDOrServerID, sourcePid);
246
- if (service?.id) { serverID = service.id; foundInProject = sourcePid; }
247
- } catch { /* not found */ }
248
-
249
- if (!foundInProject) {
250
- log('info', `Searching across all projects...`);
251
- const found = await findServiceAcrossProjects(vmIDOrServerID);
252
- if (found) {
253
- serverID = found.service.id;
254
- log('info', `Found in project "${found.projectName}" (${found.projectId})`);
255
- } else {
256
- throw new Error(`Service ${vmIDOrServerID} not found in any project`);
257
- }
258
- }
259
-
260
- const response = await apiRequest('/api/servers/moveService', 'PUT', {
261
- serviceId: String(serverID),
262
- projectId: String(targetProjectId)
263
- });
264
-
265
- if (response.status !== 'OK') throw new Error(response.message || 'Failed to move service');
266
-
267
- log('success', `Service moved to project ${targetProjectId}`);
268
- return response;
269
- }
270
-
271
- export async function showService(vmID, projectId = null, json = false) {
272
- const config = loadConfig();
273
- const pid = projectId || config.defaultProject;
274
-
275
- const details = await getServiceDetails(vmID, pid);
276
- if (!details) throw new Error(`Service with vmID ${vmID} not found`);
277
-
278
- if (json) {
279
- outputJson(details);
280
- return details;
281
- }
282
-
283
- console.log('\n' + formatService(details));
284
- console.log(`\n${colors.bold}Extended Details${colors.reset}`);
285
- console.log(` Server ID: ${details.id || 'N/A'}`);
286
- console.log(` Firewall: ${details.isFirewallActivated ? 'enabled' : 'disabled'}`);
287
- console.log(` Alerts: ${details.isAlertsActivated ? 'enabled' : 'disabled'}`);
288
- console.log(` Remote Backup: ${details.remoteBackupsActivated ? 'enabled' : 'disabled'}`);
289
- console.log(` System Auto-Update: ${details.system_AutoUpdate_Enabled ? 'enabled' : 'disabled'}`);
290
- console.log(` App Auto-Update: ${details.app_AutoUpdate_Enabled ? 'enabled' : 'disabled'}`);
291
- console.log(` Price/Hour: $${details.pricePerHour || 'N/A'}`);
292
- console.log('');
293
- return details;
294
- }
1
+ import { apiRequest } from '../api.js';
2
+ import { loadConfig } from '../config.js';
3
+ import { findTemplate } from './templates.js';
4
+ import { formatTable, formatService, colors, log, sleep, validateServerName, outputJson } from '../utils.js';
5
+ import { buildCreateServerPayload, validateClusterOptions, describeCluster } from '../payloads/server.js';
6
+ import { parseVmIDs, deploymentProgress } from '../deployment.js';
7
+
8
+ async function listProjectsRaw() {
9
+ const response = await apiRequest('/api/projects/getList');
10
+ if (response.status !== 'OK') return [];
11
+ return response.data?.projects || [];
12
+ }
13
+
14
+ export async function findServiceAcrossProjects(vmID) {
15
+ const projects = await listProjectsRaw();
16
+ for (const project of projects) {
17
+ const pid = project.projectID;
18
+ try {
19
+ const services = await listServicesRaw(pid);
20
+ const svc = services.find(s => String(s.vmID) === String(vmID));
21
+ if (svc) return { service: svc, projectId: pid, projectName: project.project_name };
22
+ } catch { /* skip */ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ export async function listServicesRaw(projectId = null) {
28
+ const config = loadConfig();
29
+ const pid = projectId || config.defaultProject;
30
+ if (!pid) throw new Error('Project ID required. Use --project or set default with: elestio config --set-default-project <id>');
31
+
32
+ const response = await apiRequest('/api/servers/getServices', 'POST', {
33
+ appid: 'Cloudxx',
34
+ projectId: String(pid),
35
+ isActiveService: 'true'
36
+ });
37
+
38
+ return response.servers || response.data?.services || [];
39
+ }
40
+
41
+ export async function listServices(projectId = null, json = false) {
42
+ const config = loadConfig();
43
+ const pid = projectId || config.defaultProject;
44
+ if (!pid) throw new Error('Project ID required. Use --project or set default with: elestio config --set-default-project <id>');
45
+
46
+ const response = await apiRequest('/api/servers/getServices', 'POST', {
47
+ appid: 'Cloudxx',
48
+ projectId: String(pid),
49
+ isActiveService: 'true'
50
+ });
51
+
52
+ if (response.status === 'KO' || response.code === 'AccessDenied') {
53
+ throw new Error(response.message || 'Access denied.');
54
+ }
55
+
56
+ const services = response.servers || response.data?.services || [];
57
+
58
+ if (json) {
59
+ outputJson(services);
60
+ return services;
61
+ }
62
+
63
+ if (services.length === 0) {
64
+ log('info', `No services in project ${pid}`);
65
+ return [];
66
+ }
67
+
68
+ const columns = [
69
+ { key: 'displayName', label: 'Name' },
70
+ { key: 'templateName', label: 'Software' },
71
+ { key: 'status', label: 'Status' },
72
+ { key: 'deploymentStatus', label: 'Deploy' },
73
+ { key: 'vmID', label: 'vmID' },
74
+ { key: 'ipv4', label: 'IP' }
75
+ ];
76
+
77
+ console.log(`\n${colors.bold}Services in project ${pid} (${services.length})${colors.reset}\n`);
78
+ console.log(formatTable(services, columns));
79
+ console.log('');
80
+ return services;
81
+ }
82
+
83
+ export async function getServiceDetails(vmID, projectId = null) {
84
+ const config = loadConfig();
85
+ const pid = projectId || config.defaultProject;
86
+ if (!pid) throw new Error('Project ID required');
87
+
88
+ const response = await apiRequest('/api/servers/getServerDetails', 'POST', {
89
+ vmID: String(vmID),
90
+ projectID: String(pid)
91
+ });
92
+
93
+ if (response.serviceInfos && response.serviceInfos.length > 0) return response.serviceInfos[0];
94
+ if (response.status === 'OK' && response.data) return response.data;
95
+ if (response.status === 'KO' || response.message) throw new Error(response.message || 'Failed to get service details');
96
+ return null;
97
+ }
98
+
99
+ export async function getServiceByVmId(vmID, projectId = null) {
100
+ const services = await listServicesRaw(projectId);
101
+ return services.find(s => String(s.vmID) === String(vmID));
102
+ }
103
+
104
+ export async function getServerIdFromVmId(vmID, projectId = null) {
105
+ const service = await getServiceByVmId(vmID, projectId);
106
+ if (!service) throw new Error(`Service with vmID ${vmID} not found`);
107
+ return service.id;
108
+ }
109
+
110
+ export async function deployService(templateNameOrId, options = {}) {
111
+ const config = loadConfig();
112
+ const template = await findTemplate(templateNameOrId);
113
+ if (!template) throw new Error(`Template "${templateNameOrId}" not found. Use: elestio templates search <name>`);
114
+
115
+ const projectId = options.project || config.defaultProject;
116
+ if (!projectId) throw new Error('Project ID required. Use --project or set default');
117
+
118
+ const serverName = options.name || `${template.title.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${Date.now().toString(36)}`;
119
+ const serverType = options.size || config.defaults?.serverType || 'MEDIUM-2C-4G';
120
+ const datacenter = options.region || config.defaults?.datacenter || 'nbg';
121
+ const support = options.support || config.defaults?.support || 'level1';
122
+ const adminEmail = options.email || config.email;
123
+ const provider = options.provider || config.defaults?.provider || 'netcup';
124
+ const version = options.version || template.dockerhub_default_tag || 'latest';
125
+
126
+ const nameValidation = validateServerName(serverName);
127
+ if (!nameValidation.valid) throw new Error(nameValidation.error);
128
+
129
+ const isCicd = template.title?.toLowerCase().includes('ci-cd') || templateNameOrId?.toLowerCase() === 'cicd';
130
+ const serviceType = isCicd ? 'CICD' : 'Service';
131
+
132
+ // Validated before anything is created: a rejected cluster still bills the
133
+ // VMs the provider has already started.
134
+ const cluster = options.cluster
135
+ ? validateClusterOptions({ mode: options.clusterMode, nodes: options.nodes }, template)
136
+ : null;
137
+
138
+ if (cluster && isCicd) throw new Error('A CI/CD target cannot be deployed as a cluster');
139
+
140
+ const payload = buildCreateServerPayload({
141
+ template, projectId, serverName, serverType, datacenter, provider,
142
+ support, adminEmail, version, serviceType, cluster,
143
+ pipelineName: options.pipelineName, cicdMode: options.cicdMode
144
+ });
145
+
146
+ if (options.dryRun) {
147
+ const preview = {
148
+ template: template.title, templateId: template.id, version,
149
+ projectId, serverName, provider, datacenter, serverType, support, adminEmail, serviceType,
150
+ cluster: cluster ? { mode: cluster.mode, nodes: cluster.nodes, isReplica: cluster.isReplica } : null,
151
+ topology: describeCluster(cluster)
152
+ };
153
+
154
+ if (options.json) {
155
+ outputJson({ dryRun: true, ...preview, payload });
156
+ return preview;
157
+ }
158
+
159
+ console.log(`\n${colors.bold}Deployment Preview (--dry-run)${colors.reset}\n`);
160
+ console.log(` Software: ${colors.cyan}${template.title}${colors.reset} (ID: ${template.id})`);
161
+ console.log(` Version: ${version}`);
162
+ console.log(` Project: ${projectId}`);
163
+ console.log(` Name: ${serverName}`);
164
+ console.log(` Provider: ${provider}`);
165
+ console.log(` Region: ${datacenter}`);
166
+ console.log(` Size: ${serverType}`);
167
+ console.log(` Support: ${support}`);
168
+ console.log(` Admin: ${adminEmail}`);
169
+ console.log(` Topology: ${describeCluster(cluster)}`);
170
+ if (cluster) console.log(` ${colors.yellow}Billing is per VM: ${cluster.nodes} VMs will be created.${colors.reset}`);
171
+ console.log('');
172
+ log('info', 'To deploy, run the same command without --dry-run');
173
+ return preview;
174
+ }
175
+
176
+ log('info', `Deploying ${template.title} (ID: ${template.id})`);
177
+ log('info', ` Project: ${projectId} | Name: ${serverName}`);
178
+ log('info', ` Provider: ${provider} | Size: ${serverType} @ ${datacenter}`);
179
+ if (cluster) log('info', ` Cluster: ${describeCluster(cluster)}`);
180
+
181
+ const response = await apiRequest('/api/servers/createServer', 'POST', payload);
182
+
183
+ if (!response.providerServerID && !response.action) {
184
+ throw new Error(response.message || 'Failed to create service');
185
+ }
186
+
187
+ log('success', `Deployment started! Provider Server ID: ${response.providerServerID}`);
188
+ if (cluster) log('info', 'Replicas are configured after the primary is up; follow progress with: elestio clusters list');
189
+
190
+ if (options.wait !== false) {
191
+ log('info', 'Waiting for deployment to complete...');
192
+ return await waitForDeployment(response.providerServerID, projectId, options.timeout);
193
+ }
194
+
195
+ return response;
196
+ }
197
+
198
+ export async function waitForDeployment(vmID, projectId, timeoutMs = 600000) {
199
+ const ids = parseVmIDs(vmID);
200
+ if (ids.length === 0) throw new Error('No vmID to wait for');
201
+
202
+ const start = Date.now();
203
+ let lastSummary = '';
204
+
205
+ while (Date.now() - start < timeoutMs) {
206
+ const progress = deploymentProgress(await listServicesRaw(projectId), ids);
207
+
208
+ if (progress.services.length === 0) { await sleep(10000); continue; }
209
+
210
+ if (progress.summary !== lastSummary) {
211
+ lastSummary = progress.summary;
212
+ log('info', `Status: ${progress.summary}`);
213
+ }
214
+
215
+ if (progress.done) {
216
+ log('success', 'Deployment complete!');
217
+ for (const svc of progress.services) console.log('\n' + formatService(svc));
218
+ console.log('');
219
+ return ids.length === 1 ? progress.services[0] : progress.services;
220
+ }
221
+
222
+ await sleep(15000);
223
+ }
224
+
225
+ throw new Error(`Deployment timed out after ${timeoutMs / 1000}s`);
226
+ }
227
+
228
+ export async function deleteService(vmID, options = {}) {
229
+ if (!options.force) throw new Error('Deleting a service requires --force flag');
230
+
231
+ const config = loadConfig();
232
+ const projectId = options.project || config.defaultProject;
233
+ if (!projectId) throw new Error('Project ID required');
234
+
235
+ const response = await apiRequest('/api/servers/deleteServer', 'POST', {
236
+ vmID: String(vmID),
237
+ projectID: String(projectId),
238
+ isDeleteServiceWithBackup: options.withBackups ? 'true' : 'false'
239
+ });
240
+
241
+ if (response.status !== 'OK' && !response.action) {
242
+ throw new Error(response.message || 'Failed to delete service');
243
+ }
244
+
245
+ log('success', `Service ${vmID} deletion initiated`);
246
+ return response;
247
+ }
248
+
249
+ export async function moveService(vmIDOrServerID, targetProjectId, sourceProjectId = null) {
250
+ const config = loadConfig();
251
+ const sourcePid = sourceProjectId || config.defaultProject;
252
+
253
+ let serverID = vmIDOrServerID;
254
+ let foundInProject = null;
255
+
256
+ try {
257
+ const service = await getServiceByVmId(vmIDOrServerID, sourcePid);
258
+ if (service?.id) { serverID = service.id; foundInProject = sourcePid; }
259
+ } catch { /* not found */ }
260
+
261
+ if (!foundInProject) {
262
+ log('info', `Searching across all projects...`);
263
+ const found = await findServiceAcrossProjects(vmIDOrServerID);
264
+ if (found) {
265
+ serverID = found.service.id;
266
+ log('info', `Found in project "${found.projectName}" (${found.projectId})`);
267
+ } else {
268
+ throw new Error(`Service ${vmIDOrServerID} not found in any project`);
269
+ }
270
+ }
271
+
272
+ const response = await apiRequest('/api/servers/moveService', 'PUT', {
273
+ serviceId: String(serverID),
274
+ projectId: String(targetProjectId)
275
+ });
276
+
277
+ if (response.status !== 'OK') throw new Error(response.message || 'Failed to move service');
278
+
279
+ log('success', `Service moved to project ${targetProjectId}`);
280
+ return response;
281
+ }
282
+
283
+ export async function showService(vmID, projectId = null, json = false) {
284
+ const config = loadConfig();
285
+ const pid = projectId || config.defaultProject;
286
+
287
+ const details = await getServiceDetails(vmID, pid);
288
+ if (!details) throw new Error(`Service with vmID ${vmID} not found`);
289
+
290
+ if (json) {
291
+ outputJson(details);
292
+ return details;
293
+ }
294
+
295
+ console.log('\n' + formatService(details));
296
+ console.log(`\n${colors.bold}Extended Details${colors.reset}`);
297
+ console.log(` Server ID: ${details.id || 'N/A'}`);
298
+ console.log(` Firewall: ${details.isFirewallActivated ? 'enabled' : 'disabled'}`);
299
+ console.log(` Alerts: ${details.isAlertsActivated ? 'enabled' : 'disabled'}`);
300
+ console.log(` Remote Backup: ${details.remoteBackupsActivated ? 'enabled' : 'disabled'}`);
301
+ console.log(` System Auto-Update: ${details.system_AutoUpdate_Enabled ? 'enabled' : 'disabled'}`);
302
+ console.log(` App Auto-Update: ${details.app_AutoUpdate_Enabled ? 'enabled' : 'disabled'}`);
303
+ console.log(` Price/Hour: $${details.pricePerHour || 'N/A'}`);
304
+ console.log('');
305
+ return details;
306
+ }