@celilo/cli 0.5.0-alpha.8 → 0.5.0-alpha.9

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/api-clients/proxmox.test.ts +78 -0
  3. package/src/api-clients/proxmox.ts +96 -1
  4. package/src/cli/command-registry.ts +32 -3
  5. package/src/cli/commands/backup-delete.ts +10 -7
  6. package/src/cli/commands/backup-import.ts +11 -8
  7. package/src/cli/commands/backup-restore.ts +11 -8
  8. package/src/cli/commands/events.ts +8 -3
  9. package/src/cli/commands/machine-add.ts +178 -163
  10. package/src/cli/commands/machine-remove.ts +10 -7
  11. package/src/cli/commands/module-config.test.ts +78 -0
  12. package/src/cli/commands/module-config.ts +18 -3
  13. package/src/cli/commands/module-import.ts +9 -5
  14. package/src/cli/commands/module-remove.ts +20 -9
  15. package/src/cli/commands/module-status.ts +15 -0
  16. package/src/cli/commands/module-upgrade.ts +10 -6
  17. package/src/cli/commands/proxmox-node-list.ts +101 -0
  18. package/src/cli/commands/proxmox-template-selection.ts +16 -15
  19. package/src/cli/commands/service-add-digitalocean.ts +120 -109
  20. package/src/cli/commands/service-add-proxmox.ts +275 -260
  21. package/src/cli/commands/service-reconfigure.ts +171 -153
  22. package/src/cli/commands/service-remove.ts +19 -13
  23. package/src/cli/commands/service-verify.ts +9 -10
  24. package/src/cli/commands/storage-add-local.ts +120 -107
  25. package/src/cli/commands/storage-add-s3.ts +145 -131
  26. package/src/cli/commands/storage-remove.ts +11 -8
  27. package/src/cli/commands/system-init.ts +119 -128
  28. package/src/cli/completion.ts +15 -0
  29. package/src/cli/index.ts +25 -0
  30. package/src/cli/service-credential.ts +54 -0
  31. package/src/services/bus-interview.ts +232 -0
  32. package/src/services/module-config.ts +12 -0
  33. package/src/services/module-deploy.ts +6 -1
  34. package/src/services/placement-reconcile.test.ts +86 -0
  35. package/src/services/placement-reconcile.ts +108 -0
  36. package/src/services/programmatic-responder.ts +34 -0
  37. package/src/services/terminal-responder.ts +113 -0
  38. package/src/templates/generator.test.ts +30 -0
  39. package/src/templates/generator.ts +86 -31
@@ -3,7 +3,6 @@
3
3
  * Configure a Proxmox container service
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
6
  import {
8
7
  buildProxmoxApiUrl,
9
8
  buildTemplatePath,
@@ -11,20 +10,32 @@ import {
11
10
  listNodeStorage,
12
11
  } from '../../api-clients/proxmox';
13
12
  import type { NetworkZone } from '../../db/schema';
13
+ import {
14
+ askConfirm,
15
+ askMultiselect,
16
+ askSelect,
17
+ askText,
18
+ withInterviewSession,
19
+ } from '../../services/bus-interview';
14
20
  import {
15
21
  addContainerService,
16
22
  testConnection as testServiceConnection,
17
23
  updateVerificationStatus,
18
24
  } from '../../services/container-service';
19
- import { celiloIntro, celiloOutro, promptPassword, promptText } from '../prompts';
25
+ import { celiloIntro, celiloOutro } from '../prompts';
26
+ import { resolveServiceCredential } from '../service-credential';
20
27
  import type { CommandResult } from '../types';
21
- import { validateRequired } from '../validators';
22
28
  import {
23
29
  runApplianceDownload,
24
30
  selectUbuntuApplianceFromCatalog,
25
31
  } from './proxmox-template-selection';
26
32
  import { buildCloudInitTemplate, detectExistingVmTemplates } from './proxmox-vm-template-build';
27
33
 
34
+ // Sentinel VM-template choices returned by the select. Any other value is an
35
+ // existing template name.
36
+ const VM_BUILD = '__build__';
37
+ const VM_SKIP = '__skip__';
38
+
28
39
  /**
29
40
  * Handle service add proxmox command
30
41
  *
@@ -33,291 +44,295 @@ import { buildCloudInitTemplate, detectExistingVmTemplates } from './proxmox-vm-
33
44
  */
34
45
  export async function handleServiceAddProxmox(
35
46
  _args: string[],
36
- _flags: Record<string, boolean | string> = {},
47
+ flags: Record<string, boolean | string> = {},
37
48
  ): Promise<CommandResult> {
38
- try {
39
- celiloIntro('Add Proxmox Container Service');
40
-
41
- // Prompt for service configuration
42
- const name = await promptText({
43
- message: 'Human-readable name:',
44
- defaultValue: 'Proxmox Home Lab',
45
- placeholder: 'Proxmox Home Lab',
46
- validate: validateRequired('Service name'),
47
- });
48
-
49
- // Prompt for zones (multi-select)
50
- const zones = await p.multiselect<NetworkZone>({
51
- message: 'Zones this service can provision to:',
52
- options: [
53
- { value: 'internal' as NetworkZone, label: 'internal' },
54
- { value: 'dmz' as NetworkZone, label: 'dmz' },
55
- { value: 'app' as NetworkZone, label: 'app' },
56
- { value: 'secure' as NetworkZone, label: 'secure' },
57
- { value: 'external' as NetworkZone, label: 'external' },
58
- ],
59
- required: true,
60
- initialValues: ['internal', 'dmz', 'app', 'secure'] as NetworkZone[], // Common defaults
61
- });
62
-
63
- if (p.isCancel(zones)) {
64
- p.cancel('Operation cancelled');
65
- return { success: false, error: 'Cancelled by user' };
66
- }
49
+ // Non-secret prompts route through the bus interview (ISS-0127); the API
50
+ // token id + secret are service credentials that travel by flag/env only
51
+ // (D7). `withInterviewSession` renders bus questions locally on a TTY.
52
+ const scope = 'service-add:proxmox';
53
+
54
+ return withInterviewSession(async () => {
55
+ try {
56
+ celiloIntro('Add Proxmox Container Service');
57
+
58
+ const name = await askText({
59
+ scope,
60
+ key: 'name',
61
+ message: 'Human-readable name',
62
+ defaultValue: 'Proxmox Home Lab',
63
+ placeholder: 'Proxmox Home Lab',
64
+ required: true,
65
+ });
67
66
 
68
- console.log('\nProxmox Configuration');
69
- console.log('─────────────────────');
70
-
71
- const ipAddress = await promptText({
72
- message: 'Proxmox IP address:',
73
- placeholder: 'e.g., 192.168.1.100 or proxmox.local',
74
- validate: validateRequired('IP address'),
75
- });
76
-
77
- const port = await promptText({
78
- message: 'Proxmox API port:',
79
- defaultValue: '8006',
80
- placeholder: '8006',
81
- validate: (value): string | Error | undefined => {
82
- // validateRequired returns undefined for valid inputs (including undefined with defaults)
83
- const requiredError = validateRequired('Port')(value);
84
- if (requiredError !== undefined) return requiredError;
85
-
86
- // If value is undefined, default will be used - that's valid
87
- if (value === undefined) return undefined;
88
-
89
- const portNum = Number(value);
90
- if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
91
- return 'Port must be a number between 1 and 65535';
92
- }
93
- return undefined;
94
- },
95
- });
96
-
97
- // Build the full API URL
98
- const apiUrl = buildProxmoxApiUrl(ipAddress, Number(port));
99
-
100
- const apiTokenId = await promptText({
101
- message: 'API Token ID:',
102
- defaultValue: 'root@pam!celilo',
103
- placeholder: 'root@pam!celilo',
104
- validate: validateRequired('API token ID'),
105
- });
106
-
107
- const apiTokenSecret = await promptPassword({
108
- message: 'API Token Secret:',
109
- validate: validateRequired('API token secret'),
110
- });
111
-
112
- const targetNode = await promptText({
113
- message: 'Default target node:',
114
- defaultValue: 'pve',
115
- placeholder: 'pve',
116
- validate: validateRequired('Target node'),
117
- });
118
-
119
- const storage = await promptText({
120
- message: 'Default storage:',
121
- defaultValue: 'local-lvm',
122
- placeholder: 'local-lvm',
123
- validate: validateRequired('Storage'),
124
- });
125
-
126
- // Build credentials here — needed for VM template detection below and
127
- // for LXC template catalog lookup further down.
128
- const credentials = {
129
- api_url: apiUrl,
130
- api_token_id: apiTokenId,
131
- api_token_secret: apiTokenSecret,
132
- };
133
-
134
- // VM template for `requires.system.type: vm` modules.
135
- // Auto-detect existing templates on the node; offer to build if none found.
136
- // See v2/PROXMOX_VM_TEMPLATE.md.
137
- let vmTemplate: string | undefined;
138
- const existingTemplates = await detectExistingVmTemplates(credentials, targetNode);
139
- if (existingTemplates.length > 0) {
140
- const templateChoice = await p.select({
141
- message: 'VM template for VM-type modules:',
67
+ const zones = await askMultiselect({
68
+ scope,
69
+ key: 'zones',
70
+ message: 'Zones this service can provision to',
142
71
  options: [
143
- ...existingTemplates.map((t) => ({
144
- value: t.name,
145
- label: `${t.name} (VMID ${t.vmid})`,
146
- })),
147
- { value: '__build__', label: 'Build a new Ubuntu 24.04 cloud-init template now' },
148
- { value: '__skip__', label: 'Skip — I only need LXC modules' },
72
+ { value: 'internal', label: 'internal' },
73
+ { value: 'dmz', label: 'dmz' },
74
+ { value: 'app', label: 'app' },
75
+ { value: 'secure', label: 'secure' },
76
+ { value: 'external', label: 'external' },
149
77
  ],
78
+ required: true,
150
79
  });
151
- if (p.isCancel(templateChoice)) {
152
- p.cancel('Operation cancelled');
153
- return { success: false, error: 'Cancelled by user' };
154
- }
155
- if (templateChoice === '__build__') {
156
- vmTemplate = await buildCloudInitTemplate({
157
- credentials,
158
- nodeName: targetNode,
159
- diskStorage: storage,
160
- });
161
- } else if (templateChoice !== '__skip__') {
162
- vmTemplate = templateChoice;
163
- }
164
- } else {
165
- const shouldBuild = await p.confirm({
166
- message: 'No VM templates found. Build a Ubuntu 24.04 cloud-init template now? (~2–10 min)',
167
- initialValue: false,
80
+
81
+ console.log('\nProxmox Configuration');
82
+ console.log('─────────────────────');
83
+
84
+ const ipAddress = await askText({
85
+ scope,
86
+ key: 'ip_address',
87
+ message: 'Proxmox IP address',
88
+ placeholder: 'e.g., 192.168.1.100 or proxmox.local',
89
+ required: true,
168
90
  });
169
- if (p.isCancel(shouldBuild)) {
170
- p.cancel('Operation cancelled');
171
- return { success: false, error: 'Cancelled by user' };
172
- }
173
- if (shouldBuild) {
174
- vmTemplate = await buildCloudInitTemplate({
175
- credentials,
176
- nodeName: targetNode,
177
- diskStorage: storage,
91
+
92
+ const port = await askText({
93
+ scope,
94
+ key: 'port',
95
+ message: 'Proxmox API port',
96
+ defaultValue: '8006',
97
+ placeholder: '8006',
98
+ required: true,
99
+ type: 'integer',
100
+ pattern: '^[0-9]+$',
101
+ });
102
+
103
+ // Build the full API URL
104
+ const apiUrl = buildProxmoxApiUrl(ipAddress, Number(port));
105
+
106
+ // API token ID + secret are service credentials — flag/env only (D7).
107
+ const apiTokenId = await resolveServiceCredential({
108
+ field: 'API token ID',
109
+ flag: 'api-token-id',
110
+ envVar: 'PROXMOX_API_TOKEN_ID',
111
+ flagValue: flags['api-token-id'],
112
+ });
113
+
114
+ const apiTokenSecret = await resolveServiceCredential({
115
+ field: 'API token secret',
116
+ flag: 'api-token-secret',
117
+ envVar: 'PROXMOX_API_TOKEN_SECRET',
118
+ flagValue: flags['api-token-secret'],
119
+ });
120
+
121
+ const targetNode = await askText({
122
+ scope,
123
+ key: 'default_target_node',
124
+ message: 'Default target node',
125
+ defaultValue: 'pve',
126
+ placeholder: 'pve',
127
+ required: true,
128
+ });
129
+
130
+ const storage = await askText({
131
+ scope,
132
+ key: 'storage',
133
+ message: 'Default storage',
134
+ defaultValue: 'local-lvm',
135
+ placeholder: 'local-lvm',
136
+ required: true,
137
+ });
138
+
139
+ // Build credentials here — needed for VM template detection below and
140
+ // for LXC template catalog lookup further down.
141
+ const credentials = {
142
+ api_url: apiUrl,
143
+ api_token_id: apiTokenId,
144
+ api_token_secret: apiTokenSecret,
145
+ };
146
+
147
+ // VM template for `requires.system.type: vm` modules.
148
+ // Auto-detect existing templates on the node; offer to build if none found.
149
+ // See v2/PROXMOX_VM_TEMPLATE.md.
150
+ let vmTemplate: string | undefined;
151
+ const existingTemplates = await detectExistingVmTemplates(credentials, targetNode);
152
+ if (existingTemplates.length > 0) {
153
+ const templateChoice = await askSelect({
154
+ scope,
155
+ key: 'vm_template',
156
+ message: 'VM template for VM-type modules',
157
+ options: [
158
+ ...existingTemplates.map((t) => ({
159
+ value: t.name,
160
+ label: `${t.name} (VMID ${t.vmid})`,
161
+ })),
162
+ { value: VM_BUILD, label: 'Build a new Ubuntu 24.04 cloud-init template now' },
163
+ { value: VM_SKIP, label: 'Skip — I only need LXC modules' },
164
+ ],
178
165
  });
166
+ if (templateChoice === VM_BUILD) {
167
+ vmTemplate = await buildCloudInitTemplate({
168
+ credentials,
169
+ nodeName: targetNode,
170
+ diskStorage: storage,
171
+ });
172
+ } else if (templateChoice !== VM_SKIP) {
173
+ vmTemplate = templateChoice;
174
+ }
179
175
  } else {
180
- const manualEntry = await promptText({
181
- message: 'VM template name (optional — leave blank to skip):',
182
- placeholder: 'e.g., ubuntu-2404-cloudinit',
176
+ const shouldBuild = await askConfirm({
177
+ scope,
178
+ key: 'vm_template_build',
179
+ message:
180
+ 'No VM templates found. Build a Ubuntu 24.04 cloud-init template now? (~2–10 min)',
181
+ defaultValue: false,
183
182
  });
184
- vmTemplate = manualEntry?.trim() || undefined;
183
+ if (shouldBuild) {
184
+ vmTemplate = await buildCloudInitTemplate({
185
+ credentials,
186
+ nodeName: targetNode,
187
+ diskStorage: storage,
188
+ });
189
+ } else {
190
+ const manualEntry = await askText({
191
+ scope,
192
+ key: 'vm_template_manual',
193
+ message: 'VM template name (optional — leave blank to skip)',
194
+ placeholder: 'e.g., ubuntu-2404-cloudinit',
195
+ });
196
+ vmTemplate = manualEntry.trim() || undefined;
197
+ }
185
198
  }
186
- }
187
199
 
188
- // Find storage that supports vztmpl content. We do this BEFORE prompting
189
- // for a template so the user only ever sees one storage in subsequent
190
- // messages, and so the saved volid uses the right storage.
191
- console.log('\nFinding storage for templates...');
192
- const storageListResult = await listNodeStorage(credentials, targetNode);
200
+ // Find storage that supports vztmpl content. We do this BEFORE prompting
201
+ // for a template so the user only ever sees one storage in subsequent
202
+ // messages, and so the saved volid uses the right storage.
203
+ console.log('\nFinding storage for templates...');
204
+ const storageListResult = await listNodeStorage(credentials, targetNode);
193
205
 
194
- let templateStorage = 'local';
195
- if (storageListResult.success) {
196
- const vztmplStorage = storageListResult.data.find(
197
- (s) => s.active && s.enabled && s.content.includes('vztmpl'),
198
- );
199
- if (vztmplStorage) {
200
- templateStorage = vztmplStorage.storage;
201
- console.log(`✓ Using storage '${templateStorage}' for templates`);
202
- } else {
203
- console.log(`⚠ No storage found with 'vztmpl' support, using '${templateStorage}'`);
206
+ let templateStorage = 'local';
207
+ if (storageListResult.success) {
208
+ const vztmplStorage = storageListResult.data.find(
209
+ (s) => s.active && s.enabled && s.content.includes('vztmpl'),
210
+ );
211
+ if (vztmplStorage) {
212
+ templateStorage = vztmplStorage.storage;
213
+ console.log(`✓ Using storage '${templateStorage}' for templates`);
214
+ } else {
215
+ console.log(`⚠ No storage found with 'vztmpl' support, using '${templateStorage}'`);
216
+ }
204
217
  }
205
- }
206
218
 
207
- // Pick a template from Proxmox's live catalog (replaces the old hardcoded
208
- // version select that built `-1`-revision URLs by hand).
209
- const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode);
210
- if (selectionResult.kind === 'cancelled') {
211
- p.cancel('Operation cancelled');
212
- return { success: false, error: 'Cancelled by user' };
213
- }
214
- if (selectionResult.kind === 'error') {
215
- return { success: false, error: selectionResult.message };
216
- }
217
- const templateFilename = selectionResult.choice.template;
218
- const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
219
-
220
- // Check if the template is already cached in the chosen storage.
221
- console.log(`\nChecking if template '${templateFilename}' exists...`);
222
- const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
219
+ // Pick a template from Proxmox's live catalog (replaces the old hardcoded
220
+ // version select that built `-1`-revision URLs by hand).
221
+ const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
222
+ scope,
223
+ });
224
+ if (selectionResult.kind === 'cancelled') {
225
+ return { success: false, error: 'Cancelled by user' };
226
+ }
227
+ if (selectionResult.kind === 'error') {
228
+ return { success: false, error: selectionResult.message };
229
+ }
230
+ const templateFilename = selectionResult.choice.template;
231
+ const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
232
+
233
+ // Check if the template is already cached in the chosen storage.
234
+ console.log(`\nChecking if template '${templateFilename}' exists...`);
235
+ const templatesResult = await listAvailableTemplates(
236
+ credentials,
237
+ targetNode,
238
+ templateStorage,
239
+ );
223
240
 
224
- let templateExists = false;
225
- if (templatesResult.success) {
226
- templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
227
- }
241
+ let templateExists = false;
242
+ if (templatesResult.success) {
243
+ templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
244
+ }
228
245
 
229
- // Save the service FIRST so credentials aren't lost if the download fails.
230
- const service = await addContainerService({
231
- name,
232
- providerName: 'proxmox',
233
- zones: zones as unknown as NetworkZone[],
234
- apiCredentials: credentials,
235
- providerConfig: {
236
- default_target_node: targetNode,
237
- lxc_template: lxcTemplate,
238
- storage,
239
- ...(vmTemplate ? { vm_template: vmTemplate } : {}),
240
- },
241
- });
242
-
243
- console.log(`✓ Service '${service.serviceId}' saved\n`);
244
-
245
- let templateReady = templateExists;
246
-
247
- if (!templateExists) {
248
- console.log(`✗ Template '${templateFilename}' not found in storage '${templateStorage}'`);
249
-
250
- const shouldDownload = await p.confirm({
251
- message: 'Download template now?',
252
- initialValue: true,
246
+ // Save the service FIRST so credentials aren't lost if the download fails.
247
+ const service = await addContainerService({
248
+ name,
249
+ providerName: 'proxmox',
250
+ zones: zones as unknown as NetworkZone[],
251
+ apiCredentials: credentials,
252
+ providerConfig: {
253
+ default_target_node: targetNode,
254
+ lxc_template: lxcTemplate,
255
+ storage,
256
+ ...(vmTemplate ? { vm_template: vmTemplate } : {}),
257
+ },
253
258
  });
254
259
 
255
- if (p.isCancel(shouldDownload)) {
256
- p.cancel('Operation cancelled');
257
- return { success: false, error: 'Cancelled by user' };
258
- }
260
+ console.log(`✓ Service '${service.serviceId}' saved\n`);
261
+
262
+ let templateReady = templateExists;
263
+
264
+ if (!templateExists) {
265
+ console.log(`✗ Template '${templateFilename}' not found in storage '${templateStorage}'`);
259
266
 
260
- if (shouldDownload) {
261
- const downloadOutcome = await runApplianceDownload({
262
- credentials,
263
- targetNode,
264
- templateStorage,
265
- templateFilename,
267
+ const shouldDownload = await askConfirm({
268
+ scope,
269
+ key: 'download_template',
270
+ message: 'Download template now?',
271
+ defaultValue: true,
266
272
  });
267
- templateReady = downloadOutcome.ready;
273
+
274
+ if (shouldDownload) {
275
+ const downloadOutcome = await runApplianceDownload({
276
+ credentials,
277
+ targetNode,
278
+ templateStorage,
279
+ templateFilename,
280
+ });
281
+ templateReady = downloadOutcome.ready;
282
+ } else {
283
+ console.log(
284
+ '\nTemplate not downloaded. Service saved but will need a template before deployment.',
285
+ );
286
+ }
287
+
288
+ if (!templateReady) {
289
+ console.log(
290
+ '\nTemplate is not available. The service has been saved but needs a template.\n',
291
+ );
292
+ console.log('Next steps:');
293
+ console.log(
294
+ ` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
295
+ );
296
+ console.log(
297
+ ` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
298
+ );
299
+ console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
300
+ }
268
301
  } else {
269
- console.log(
270
- '\nTemplate not downloaded. Service saved but will need a template before deployment.',
271
- );
302
+ console.log(`✓ Template '${templateFilename}' found`);
272
303
  }
273
304
 
274
- if (!templateReady) {
305
+ // Test connection
306
+ console.log('\nTesting connection...');
307
+ const testResult = await testServiceConnection(service);
308
+ await updateVerificationStatus(service.id, testResult);
309
+
310
+ if (!testResult.success) {
311
+ console.log(`✗ Connection test failed: ${testResult.message}`);
275
312
  console.log(
276
- '\nTemplate is not available. The service has been saved but needs a template.\n',
313
+ '\nService saved but not verified. The service will not be used until verification succeeds.',
277
314
  );
278
- console.log('Next steps:');
279
- console.log(
280
- ` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
315
+
316
+ celiloOutro(
317
+ `Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
281
318
  );
282
- console.log(
283
- ` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
319
+ } else {
320
+ console.log(`✓ ${testResult.message}`);
321
+
322
+ celiloOutro(
323
+ `Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
284
324
  );
285
- console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
286
325
  }
287
- } else {
288
- console.log(`✓ Template '${templateFilename}' found`);
289
- }
290
-
291
- // Test connection
292
- console.log('\nTesting connection...');
293
- const testResult = await testServiceConnection(service);
294
- await updateVerificationStatus(service.id, testResult);
295
-
296
- if (!testResult.success) {
297
- console.log(`✗ Connection test failed: ${testResult.message}`);
298
- console.log(
299
- '\nService saved but not verified. The service will not be used until verification succeeds.',
300
- );
301
326
 
302
- celiloOutro(
303
- `Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
304
- );
305
- } else {
306
- console.log(`✓ ${testResult.message}`);
307
-
308
- celiloOutro(
309
- `Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
310
- );
327
+ return {
328
+ success: true,
329
+ message: `Added Proxmox service: ${service.id}`,
330
+ };
331
+ } catch (error) {
332
+ return {
333
+ success: false,
334
+ error: `Failed to add Proxmox service: ${error instanceof Error ? error.message : String(error)}`,
335
+ };
311
336
  }
312
-
313
- return {
314
- success: true,
315
- message: `Added Proxmox service: ${service.id}`,
316
- };
317
- } catch (error) {
318
- return {
319
- success: false,
320
- error: `Failed to add Proxmox service: ${error instanceof Error ? error.message : String(error)}`,
321
- };
322
- }
337
+ });
323
338
  }