@celilo/cli 0.3.16 → 0.3.17

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/api-clients/proxmox.ts +77 -45
  3. package/src/cli/command-registry.ts +12 -12
  4. package/src/cli/commands/completion.ts +12 -11
  5. package/src/cli/commands/module-check.ts +158 -0
  6. package/src/cli/commands/module-import.ts +5 -5
  7. package/src/cli/commands/module-publish.test.ts +3 -90
  8. package/src/cli/commands/module-publish.ts +14 -118
  9. package/src/cli/commands/proxmox-template-selection.test.ts +150 -0
  10. package/src/cli/commands/proxmox-template-selection.ts +258 -0
  11. package/src/cli/commands/service-add-proxmox.ts +49 -127
  12. package/src/cli/commands/service-reconfigure.ts +36 -79
  13. package/src/cli/commands/service-verify.ts +20 -79
  14. package/src/cli/commands/{doctor.test.ts → system-doctor.test.ts} +1 -1
  15. package/src/cli/commands/{doctor.ts → system-doctor.ts} +93 -18
  16. package/src/cli/completion.ts +29 -2
  17. package/src/cli/index.ts +16 -7
  18. package/src/module/import.ts +4 -2
  19. package/src/registry/client.ts +14 -1
  20. package/src/services/module-deploy.ts +19 -1
  21. package/src/services/module-validator/capability-versions.test.ts +90 -0
  22. package/src/services/module-validator/capability-versions.ts +115 -0
  23. package/src/services/module-validator/contract-version.test.ts +24 -0
  24. package/src/services/module-validator/contract-version.ts +69 -0
  25. package/src/services/module-validator/git-hygiene.test.ts +141 -0
  26. package/src/services/module-validator/git-hygiene.ts +144 -0
  27. package/src/services/module-validator/index.test.ts +67 -0
  28. package/src/services/module-validator/index.ts +74 -0
  29. package/src/services/module-validator/manifest-schema.ts +42 -0
  30. package/src/services/module-validator/types.ts +43 -0
  31. package/src/services/module-validator/typescript-build.test.ts +58 -0
  32. package/src/services/module-validator/typescript-build.ts +115 -0
  33. package/src/services/module-validator/workspace-deps.test.ts +137 -0
  34. package/src/services/module-validator/workspace-deps.ts +187 -0
  35. package/src/system/prereqs.test.ts +374 -0
  36. package/src/system/prereqs.ts +377 -0
@@ -7,10 +7,6 @@ import * as p from '@clack/prompts';
7
7
  import {
8
8
  buildProxmoxApiUrl,
9
9
  buildTemplatePath,
10
- buildTemplateUrl,
11
- checkTaskStatus,
12
- downloadTemplate,
13
- extractTemplateFilename,
14
10
  listAvailableTemplates,
15
11
  listNodeStorage,
16
12
  } from '../../api-clients/proxmox';
@@ -20,10 +16,13 @@ import {
20
16
  testConnection as testServiceConnection,
21
17
  updateVerificationStatus,
22
18
  } from '../../services/container-service';
23
- import { FuelGauge } from '../fuel-gauge';
24
19
  import { celiloIntro, celiloOutro, promptPassword, promptText } from '../prompts';
25
20
  import type { CommandResult } from '../types';
26
21
  import { validateRequired } from '../validators';
22
+ import {
23
+ runApplianceDownload,
24
+ selectUbuntuApplianceFromCatalog,
25
+ } from './proxmox-template-selection';
27
26
 
28
27
  /**
29
28
  * Handle service add proxmox command
@@ -123,35 +122,19 @@ export async function handleServiceAddProxmox(
123
122
  validate: validateRequired('Storage'),
124
123
  });
125
124
 
126
- const ubuntuVersion = await p.select({
127
- message: 'Ubuntu LTS version for default template:',
128
- options: [
129
- { value: '24.04', label: 'Ubuntu 24.04 LTS (Noble Numbat)' },
130
- { value: '22.04', label: 'Ubuntu 22.04 LTS (Jammy Jellyfish)', hint: 'Recommended' },
131
- { value: '20.04', label: 'Ubuntu 20.04 LTS (Focal Fossa)' },
132
- ],
133
- initialValue: '22.04',
134
- });
135
-
136
- if (p.isCancel(ubuntuVersion)) {
137
- p.cancel('Operation cancelled');
138
- return { success: false, error: 'Cancelled by user' };
139
- }
140
-
141
- // Find storage that supports vztmpl content
125
+ // Find storage that supports vztmpl content. We do this BEFORE prompting
126
+ // for a template so the user only ever sees one storage in subsequent
127
+ // messages, and so the saved volid uses the right storage.
128
+ const credentials = {
129
+ api_url: apiUrl,
130
+ api_token_id: apiTokenId,
131
+ api_token_secret: apiTokenSecret,
132
+ };
142
133
  console.log('\nFinding storage for templates...');
143
- const storageListResult = await listNodeStorage(
144
- {
145
- api_url: apiUrl,
146
- api_token_id: apiTokenId,
147
- api_token_secret: apiTokenSecret,
148
- },
149
- targetNode,
150
- );
134
+ const storageListResult = await listNodeStorage(credentials, targetNode);
151
135
 
152
- let templateStorage = 'local'; // Default fallback
136
+ let templateStorage = 'local';
153
137
  if (storageListResult.success) {
154
- // Find first active storage that supports 'vztmpl' content
155
138
  const vztmplStorage = storageListResult.data.find(
156
139
  (s) => s.active && s.enabled && s.content.includes('vztmpl'),
157
140
  );
@@ -163,37 +146,34 @@ export async function handleServiceAddProxmox(
163
146
  }
164
147
  }
165
148
 
166
- // Build template path
167
- const lxcTemplate = buildTemplatePath(templateStorage, ubuntuVersion as string);
168
- const templateFilename = extractTemplateFilename(lxcTemplate);
149
+ // Pick a template from Proxmox's live catalog (replaces the old hardcoded
150
+ // version select that built `-1`-revision URLs by hand).
151
+ const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode);
152
+ if (selectionResult.kind === 'cancelled') {
153
+ p.cancel('Operation cancelled');
154
+ return { success: false, error: 'Cancelled by user' };
155
+ }
156
+ if (selectionResult.kind === 'error') {
157
+ return { success: false, error: selectionResult.message };
158
+ }
159
+ const templateFilename = selectionResult.choice.template;
160
+ const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
169
161
 
170
- // Check if template exists
162
+ // Check if the template is already cached in the chosen storage.
171
163
  console.log(`\nChecking if template '${templateFilename}' exists...`);
172
- const templatesResult = await listAvailableTemplates(
173
- {
174
- api_url: apiUrl,
175
- api_token_id: apiTokenId,
176
- api_token_secret: apiTokenSecret,
177
- },
178
- targetNode,
179
- templateStorage,
180
- );
164
+ const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
181
165
 
182
166
  let templateExists = false;
183
167
  if (templatesResult.success) {
184
168
  templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
185
169
  }
186
170
 
187
- // Save the service FIRST so credentials aren't lost if template download fails
171
+ // Save the service FIRST so credentials aren't lost if the download fails.
188
172
  const service = await addContainerService({
189
173
  name,
190
174
  providerName: 'proxmox',
191
175
  zones: zones as unknown as NetworkZone[],
192
- apiCredentials: {
193
- api_url: apiUrl,
194
- api_token_id: apiTokenId,
195
- api_token_secret: apiTokenSecret,
196
- },
176
+ apiCredentials: credentials,
197
177
  providerConfig: {
198
178
  default_target_node: targetNode,
199
179
  lxc_template: lxcTemplate,
@@ -203,7 +183,6 @@ export async function handleServiceAddProxmox(
203
183
 
204
184
  console.log(`✓ Service '${service.serviceId}' saved\n`);
205
185
 
206
- // Now attempt template download (service is already saved)
207
186
  let templateReady = templateExists;
208
187
 
209
188
  if (!templateExists) {
@@ -220,89 +199,32 @@ export async function handleServiceAddProxmox(
220
199
  }
221
200
 
222
201
  if (shouldDownload) {
223
- const templateUrl = buildTemplateUrl(ubuntuVersion as string);
224
-
225
- const downloadResult = await downloadTemplate(
226
- {
227
- api_url: apiUrl,
228
- api_token_id: apiTokenId,
229
- api_token_secret: apiTokenSecret,
230
- },
202
+ const downloadOutcome = await runApplianceDownload({
203
+ credentials,
231
204
  targetNode,
232
205
  templateStorage,
233
- templateUrl,
234
- );
235
-
236
- if (!downloadResult.success) {
237
- console.log(`\n✗ Failed to start download: ${downloadResult.message}`);
238
- } else {
239
- // Wait for download to complete with fuel-gauge progress
240
- const upid = downloadResult.data;
241
- const gauge = new FuelGauge(`Downloading ${templateFilename}`);
242
- gauge.start();
243
-
244
- let downloadComplete = false;
245
- let attempts = 0;
246
- const maxAttempts = 60;
247
-
248
- while (!downloadComplete && attempts < maxAttempts) {
249
- await new Promise((resolve) => setTimeout(resolve, 5000));
250
-
251
- const statusResult = await checkTaskStatus(
252
- {
253
- api_url: apiUrl,
254
- api_token_id: apiTokenId,
255
- api_token_secret: apiTokenSecret,
256
- },
257
- targetNode,
258
- upid,
259
- );
260
-
261
- if (statusResult.success) {
262
- if (statusResult.data.status === 'stopped') {
263
- if (statusResult.data.exitstatus === 'OK') {
264
- downloadComplete = true;
265
- templateReady = true;
266
- gauge.stop(true);
267
- } else {
268
- gauge.stop(false);
269
- console.log(`\n✗ Template download failed: ${statusResult.data.exitstatus}`);
270
- break;
271
- }
272
- } else {
273
- gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
274
- }
275
- } else {
276
- gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
277
- }
278
-
279
- attempts++;
280
- }
281
-
282
- if (!downloadComplete && !templateReady) {
283
- gauge.stop(false);
284
- console.log('\n✗ Template download timed out after 5 minutes');
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
- }
206
+ templateFilename,
207
+ });
208
+ templateReady = downloadOutcome.ready;
301
209
  } else {
302
210
  console.log(
303
211
  '\nTemplate not downloaded. Service saved but will need a template before deployment.',
304
212
  );
305
213
  }
214
+
215
+ if (!templateReady) {
216
+ console.log(
217
+ '\nTemplate is not available. The service has been saved but needs a template.\n',
218
+ );
219
+ console.log('Next steps:');
220
+ console.log(
221
+ ` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
222
+ );
223
+ console.log(
224
+ ` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
225
+ );
226
+ console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
227
+ }
306
228
  } else {
307
229
  console.log(`✓ Template '${templateFilename}' found`);
308
230
  }
@@ -5,29 +5,24 @@
5
5
 
6
6
  import * as p from '@clack/prompts';
7
7
  import {
8
+ type ProxmoxCredentials,
8
9
  buildTemplatePath,
9
- buildTemplateUrl,
10
- checkTaskStatus,
11
- downloadTemplate,
12
10
  extractTemplateFilename,
13
11
  listAvailableTemplates,
14
12
  listNodeStorage,
15
13
  } from '../../api-clients/proxmox';
16
14
  import {
17
- getContainerServiceByName,
15
+ getContainerServiceByServiceId,
18
16
  getServiceCredentials,
19
17
  updateServiceProviderConfig,
20
18
  } from '../../services/container-service';
21
- import { FuelGauge } from '../fuel-gauge';
22
19
  import { celiloIntro, celiloOutro, promptText } from '../prompts';
23
20
  import type { CommandResult } from '../types';
24
21
  import { validateRequired } from '../validators';
25
-
26
- interface ProxmoxCredentials {
27
- api_url: string;
28
- api_token_id: string;
29
- api_token_secret: string;
30
- }
22
+ import {
23
+ runApplianceDownload,
24
+ selectUbuntuApplianceFromCatalog,
25
+ } from './proxmox-template-selection';
31
26
 
32
27
  interface ProxmoxProviderConfig {
33
28
  default_target_node: string;
@@ -48,7 +43,7 @@ export async function handleServiceReconfigure(
48
43
  }
49
44
 
50
45
  try {
51
- const service = await getContainerServiceByName(serviceId);
46
+ const service = await getContainerServiceByServiceId(serviceId);
52
47
  if (!service) {
53
48
  return { success: false, error: `Service '${serviceId}' not found` };
54
49
  }
@@ -87,27 +82,8 @@ export async function handleServiceReconfigure(
87
82
  validate: validateRequired('Storage'),
88
83
  });
89
84
 
90
- // Detect current Ubuntu version from template filename
91
- const currentTemplateFile = extractTemplateFilename(currentConfig.lxc_template);
92
- const versionMatch = currentTemplateFile.match(/ubuntu-(\d+\.\d+)-/);
93
- const currentVersion = versionMatch?.[1] || '22.04';
94
-
95
- const ubuntuVersion = await p.select({
96
- message: 'Ubuntu LTS version for default template:',
97
- options: [
98
- { value: '24.04', label: 'Ubuntu 24.04 LTS (Noble Numbat)' },
99
- { value: '22.04', label: 'Ubuntu 22.04 LTS (Jammy Jellyfish)' },
100
- { value: '20.04', label: 'Ubuntu 20.04 LTS (Focal Fossa)' },
101
- ],
102
- initialValue: currentVersion,
103
- });
104
-
105
- if (p.isCancel(ubuntuVersion)) {
106
- p.cancel('Operation cancelled');
107
- return { success: false, error: 'Cancelled by user' };
108
- }
109
-
110
- // Find template storage
85
+ // Find template storage. Done before catalog selection so the user only
86
+ // ever sees the chosen storage in subsequent messages.
111
87
  console.log('\nFinding storage for templates...');
112
88
  const storageListResult = await listNodeStorage(credentials, targetNode);
113
89
 
@@ -122,10 +98,22 @@ export async function handleServiceReconfigure(
122
98
  }
123
99
  }
124
100
 
125
- const lxcTemplate = buildTemplatePath(templateStorage, ubuntuVersion as string);
126
- const templateFilename = extractTemplateFilename(lxcTemplate);
101
+ // Pick a template from Proxmox's live catalog. Pre-selects the family
102
+ // matching the existing volid (e.g. user has 24.04 -1 cached, mirror has -2).
103
+ const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
104
+ currentTemplate: extractTemplateFilename(currentConfig.lxc_template),
105
+ });
106
+ if (selectionResult.kind === 'cancelled') {
107
+ p.cancel('Operation cancelled');
108
+ return { success: false, error: 'Cancelled by user' };
109
+ }
110
+ if (selectionResult.kind === 'error') {
111
+ return { success: false, error: selectionResult.message };
112
+ }
113
+ const templateFilename = selectionResult.choice.template;
114
+ const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
127
115
 
128
- // Check if new template exists
116
+ // Check if new template exists in storage.
129
117
  console.log(`\nChecking if template '${templateFilename}' exists...`);
130
118
  const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
131
119
 
@@ -147,54 +135,23 @@ export async function handleServiceReconfigure(
147
135
  '\nTemplate not downloaded. Service will be updated but may fail verification.',
148
136
  );
149
137
  } else {
150
- const templateUrl = buildTemplateUrl(ubuntuVersion as string);
151
- const downloadResult = await downloadTemplate(
138
+ const outcome = await runApplianceDownload({
152
139
  credentials,
153
140
  targetNode,
154
141
  templateStorage,
155
- templateUrl,
156
- );
157
-
158
- if (!downloadResult.success) {
159
- return {
160
- success: false,
161
- error: `Failed to download template: ${downloadResult.message}\n\nTroubleshooting:\n 1. SSH into your Proxmox host and test: curl -I https://download.proxmox.com\n 2. Try downloading manually: pveam download ${templateStorage} ${templateFilename}\n 3. Check DNS and firewall settings on the Proxmox host`,
162
- };
163
- }
164
-
165
- const upid = downloadResult.data;
166
- const gauge = new FuelGauge(`Downloading ${templateFilename}`);
167
- gauge.start();
168
-
169
- let downloadComplete = false;
170
- let attempts = 0;
171
-
172
- while (!downloadComplete && attempts < 60) {
173
- await new Promise((resolve) => setTimeout(resolve, 5000));
174
- const statusResult = await checkTaskStatus(credentials, targetNode, upid);
175
-
176
- if (statusResult.success && statusResult.data.status === 'stopped') {
177
- if (statusResult.data.exitstatus === 'OK') {
178
- downloadComplete = true;
179
- gauge.stop(true);
180
- } else {
181
- gauge.stop(false);
182
- return {
183
- success: false,
184
- error: `Template download failed: ${statusResult.data.exitstatus}\n\nThis usually means the Proxmox host cannot reach download.proxmox.com.\n\nTroubleshooting:\n 1. Try a different Ubuntu version: celilo service reconfigure ${serviceId}\n 2. Download manually: pveam download ${templateStorage} ${templateFilename}\n 3. Check DNS and firewall settings on the Proxmox host`,
185
- };
186
- }
187
- } else {
188
- gauge.addOutput(`Status: downloading... (${attempts * 5}s elapsed)`);
189
- }
190
- attempts++;
191
- }
192
-
193
- if (!downloadComplete) {
194
- gauge.stop(false);
142
+ templateFilename,
143
+ });
144
+
145
+ if (!outcome.ready) {
146
+ const detail =
147
+ outcome.reason === 'task-failed'
148
+ ? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}`
149
+ : outcome.reason === 'started-failed'
150
+ ? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}`
151
+ : 'Template download did not complete in time';
195
152
  return {
196
153
  success: false,
197
- error: 'Template download timed out after 5 minutes.',
154
+ error: `${detail}\n\nTroubleshooting:\n 1. SSH into your Proxmox host and run: pveam update && pveam download ${templateStorage} ${templateFilename}\n 2. Re-try: celilo service reconfigure ${serviceId}\n 3. Check DNS and firewall settings on the Proxmox host`,
198
155
  };
199
156
  }
200
157
  }
@@ -4,21 +4,16 @@
4
4
  */
5
5
 
6
6
  import * as p from '@clack/prompts';
7
- import {
8
- buildTemplateUrl,
9
- checkTaskStatus,
10
- downloadTemplate,
11
- extractTemplateFilename,
12
- } from '../../api-clients/proxmox';
7
+ import { extractTemplateFilename } from '../../api-clients/proxmox';
13
8
  import {
14
9
  type ProxmoxCredentials,
15
10
  getContainerServiceByServiceId,
16
11
  getServiceCredentials,
17
12
  verifyContainerService,
18
13
  } from '../../services/container-service';
19
- import { FuelGauge } from '../fuel-gauge';
20
14
  import { celiloIntro, celiloOutro } from '../prompts';
21
15
  import type { CommandResult } from '../types';
16
+ import { runApplianceDownload } from './proxmox-template-selection';
22
17
 
23
18
  /**
24
19
  * Handle service verify command
@@ -108,7 +103,6 @@ export async function handleServiceVerify(
108
103
 
109
104
  if (shouldDownload) {
110
105
  try {
111
- // Get credentials and config
112
106
  const credentials = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
113
107
  const providerConfig = service.providerConfig as {
114
108
  default_target_node: string;
@@ -116,86 +110,33 @@ export async function handleServiceVerify(
116
110
  storage: string;
117
111
  };
118
112
 
119
- // Extract template info
120
113
  const templateFilename = extractTemplateFilename(providerConfig.lxc_template);
121
- const templateParts = providerConfig.lxc_template.split(':');
122
- const templateStorage = templateParts[0] || 'local';
123
-
124
- // Detect Ubuntu version from template filename
125
- const versionMatch = templateFilename.match(/ubuntu-(\d+\.\d+)-/);
126
- if (!versionMatch) {
127
- return {
128
- success: false,
129
- error: 'Cannot auto-download: unrecognized template format',
130
- };
131
- }
132
- const ubuntuVersion = versionMatch[1];
133
-
134
- const templateUrl = buildTemplateUrl(ubuntuVersion);
114
+ const templateStorage = providerConfig.lxc_template.split(':')[0] || 'local';
135
115
 
136
- const downloadResult = await downloadTemplate(
116
+ // The saved volid was the canonical filename when the service was
117
+ // created; if Proxmox refreshed the revision since then, this download
118
+ // will fail with `started-failed` and the user can `service reconfigure`
119
+ // to pick a fresh filename from the catalog.
120
+ const outcome = await runApplianceDownload({
137
121
  credentials,
138
- providerConfig.default_target_node,
122
+ targetNode: providerConfig.default_target_node,
139
123
  templateStorage,
140
- templateUrl,
141
- );
142
-
143
- if (!downloadResult.success) {
144
- return {
145
- success: false,
146
- error: `Failed to download template: ${downloadResult.message}`,
147
- };
148
- }
149
-
150
- // Wait for download to complete with fuel-gauge progress
151
- const upid = downloadResult.data;
152
- const gauge = new FuelGauge(`Downloading ${templateFilename}`);
153
- gauge.start();
154
-
155
- let downloadComplete = false;
156
- let attempts = 0;
157
- const maxAttempts = 60; // 5 minutes
158
-
159
- while (!downloadComplete && attempts < maxAttempts) {
160
- await new Promise((resolve) => setTimeout(resolve, 5000));
161
-
162
- const statusResult = await checkTaskStatus(
163
- credentials,
164
- providerConfig.default_target_node,
165
- upid,
166
- );
167
-
168
- if (statusResult.success) {
169
- if (statusResult.data.status === 'stopped') {
170
- if (statusResult.data.exitstatus === 'OK') {
171
- downloadComplete = true;
172
- gauge.stop(true);
173
- } else {
174
- gauge.stop(false);
175
- return {
176
- success: false,
177
- error: `Template download failed: ${statusResult.data.exitstatus}\n\nThis usually means the Proxmox host cannot reach download.proxmox.com.\n\nTroubleshooting:\n 1. SSH into your Proxmox host and test: curl -I https://download.proxmox.com\n 2. Try downloading manually: pveam download ${templateStorage} ${templateFilename}\n 3. Choose a different Ubuntu version: celilo service reconfigure ${service.serviceId}\n 4. Check DNS and firewall settings on the Proxmox host`,
178
- };
179
- }
180
- } else {
181
- gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
182
- }
183
- } else {
184
- gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
185
- }
186
-
187
- attempts++;
188
- }
189
-
190
- if (!downloadComplete) {
191
- gauge.stop(false);
124
+ templateFilename,
125
+ });
126
+
127
+ if (!outcome.ready) {
128
+ const detail =
129
+ outcome.reason === 'task-failed'
130
+ ? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}\n\nThis usually means the saved template revision is no longer available on Proxmox's mirror.`
131
+ : outcome.reason === 'started-failed'
132
+ ? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}\n\nThis usually means the saved template name does not match Proxmox's current catalog.`
133
+ : 'Template download did not complete in time. The Proxmox host may have slow internet or connectivity issues.';
192
134
  return {
193
135
  success: false,
194
- error: `Template download timed out after 5 minutes.\n\nThe Proxmox host may have slow internet or connectivity issues.\nTry downloading manually: ssh root@<proxmox-host> pveam download ${templateStorage} ${templateFilename}`,
136
+ error: `${detail}\n\nTroubleshooting:\n 1. Pick a fresh template version: celilo service reconfigure ${service.serviceId}\n 2. SSH into your Proxmox host and run: pveam update && pveam download ${templateStorage} ${templateFilename}\n 3. Check DNS and firewall settings on the Proxmox host`,
195
137
  };
196
138
  }
197
139
 
198
- // Retry verification
199
140
  console.log('\nRetrying verification...');
200
141
  const retryResult = await verifyContainerService(service.id);
201
142
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { compareVersions } from './doctor';
2
+ import { compareVersions } from './system-doctor';
3
3
 
4
4
  describe('compareVersions', () => {
5
5
  test('detects ascending major/minor/patch', () => {