@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11

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 (101) hide show
  1. package/drizzle/0009_dns_registrations.sql +13 -0
  2. package/drizzle/0010_dns_internal_records.sql +12 -0
  3. package/drizzle/0011_backups_name.sql +1 -0
  4. package/drizzle/meta/_journal.json +22 -1
  5. package/package.json +3 -3
  6. package/src/ansible/inventory.test.ts +10 -10
  7. package/src/ansible/validation.test.ts +25 -15
  8. package/src/api-clients/proxmox.test.ts +211 -1
  9. package/src/api-clients/proxmox.ts +399 -8
  10. package/src/cli/command-registry.ts +83 -6
  11. package/src/cli/commands/backup-delete.ts +10 -7
  12. package/src/cli/commands/backup-import.ts +11 -8
  13. package/src/cli/commands/backup-restore.ts +11 -8
  14. package/src/cli/commands/dns.ts +57 -0
  15. package/src/cli/commands/events.test.ts +4 -4
  16. package/src/cli/commands/events.ts +89 -24
  17. package/src/cli/commands/machine-add.ts +178 -163
  18. package/src/cli/commands/machine-remove.ts +10 -7
  19. package/src/cli/commands/module-config.test.ts +78 -0
  20. package/src/cli/commands/module-config.ts +18 -3
  21. package/src/cli/commands/module-import.ts +9 -5
  22. package/src/cli/commands/module-publish.ts +24 -0
  23. package/src/cli/commands/module-remove.ts +20 -9
  24. package/src/cli/commands/module-status.ts +15 -0
  25. package/src/cli/commands/module-upgrade.test.ts +37 -0
  26. package/src/cli/commands/module-upgrade.ts +26 -6
  27. package/src/cli/commands/proxmox-node-list.ts +101 -0
  28. package/src/cli/commands/proxmox-template-selection.ts +16 -15
  29. package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
  30. package/src/cli/commands/publish/alpha.test.ts +26 -0
  31. package/src/cli/commands/publish/alpha.ts +23 -0
  32. package/src/cli/commands/publish/types.ts +7 -2
  33. package/src/cli/commands/publish/workspace.ts +11 -1
  34. package/src/cli/commands/restore.ts +29 -0
  35. package/src/cli/commands/service-add-digitalocean.ts +120 -109
  36. package/src/cli/commands/service-add-proxmox.ts +283 -209
  37. package/src/cli/commands/service-reconfigure.test.ts +115 -0
  38. package/src/cli/commands/service-reconfigure.ts +252 -129
  39. package/src/cli/commands/service-remove.ts +19 -13
  40. package/src/cli/commands/service-verify.ts +9 -10
  41. package/src/cli/commands/storage-add-local.ts +120 -107
  42. package/src/cli/commands/storage-add-s3.ts +145 -131
  43. package/src/cli/commands/storage-remove.ts +11 -8
  44. package/src/cli/commands/system-doctor.ts +135 -40
  45. package/src/cli/commands/system-init.ts +119 -128
  46. package/src/cli/commands/system-migrate.test.ts +40 -0
  47. package/src/cli/commands/system-migrate.ts +65 -0
  48. package/src/cli/completion.ts +23 -0
  49. package/src/cli/index.ts +91 -7
  50. package/src/cli/service-credential.ts +54 -0
  51. package/src/config/paths.test.ts +61 -48
  52. package/src/db/client.ts +15 -146
  53. package/src/db/migrate.ts +14 -6
  54. package/src/db/schema-introspection.ts +88 -0
  55. package/src/db/schema.ts +74 -0
  56. package/src/hooks/capability-loader-firewall.test.ts +3 -3
  57. package/src/hooks/capability-loader.ts +43 -2
  58. package/src/hooks/run-named-hook.ts +28 -2
  59. package/src/hooks/types.ts +2 -1
  60. package/src/infrastructure/property-extractor.test.ts +15 -0
  61. package/src/infrastructure/property-extractor.ts +12 -0
  62. package/src/manifest/contracts/v1.ts +16 -0
  63. package/src/manifest/schema.ts +17 -0
  64. package/src/manifest/validate.test.ts +53 -0
  65. package/src/services/bus-interview.test.ts +2 -2
  66. package/src/services/bus-interview.ts +232 -0
  67. package/src/services/bus-secret-flow.test.ts +2 -2
  68. package/src/services/celilo-mgmt-hooks.test.ts +3 -2
  69. package/src/services/deploy-preflight.ts +25 -0
  70. package/src/services/deploy-validation.test.ts +54 -4
  71. package/src/services/deploy-validation.ts +27 -36
  72. package/src/services/dns-internal-records.test.ts +126 -0
  73. package/src/services/dns-internal-records.ts +119 -0
  74. package/src/services/dns-provider-backfill.test.ts +2 -2
  75. package/src/services/dns-provider-backfill.ts +14 -2
  76. package/src/services/dns-registrations.test.ts +120 -0
  77. package/src/services/dns-registrations.ts +108 -0
  78. package/src/services/events-daemon.test.ts +59 -0
  79. package/src/services/events-daemon.ts +191 -57
  80. package/src/services/fleet-checks.test.ts +508 -0
  81. package/src/services/fleet-checks.ts +678 -0
  82. package/src/services/module-build.test.ts +43 -38
  83. package/src/services/module-config.ts +12 -0
  84. package/src/services/module-deploy.ts +7 -6
  85. package/src/services/module-subscriptions.test.ts +88 -0
  86. package/src/services/module-subscriptions.ts +50 -1
  87. package/src/services/module-validator/bundled-deps.test.ts +55 -0
  88. package/src/services/module-validator/bundled-deps.ts +115 -0
  89. package/src/services/module-validator/capability-versions.test.ts +1 -1
  90. package/src/services/placement-reconcile.test.ts +86 -0
  91. package/src/services/placement-reconcile.ts +108 -0
  92. package/src/services/programmatic-responder.ts +34 -0
  93. package/src/services/terminal-responder.ts +113 -0
  94. package/src/templates/generator.test.ts +92 -12
  95. package/src/templates/generator.ts +165 -80
  96. package/src/test-utils/fixtures.test.ts +1 -1
  97. package/src/test-utils/integration-guard.ts +33 -0
  98. package/src/types/infrastructure.ts +6 -0
  99. package/src/variables/computed/computed-integration.test.ts +3 -3
  100. package/src/variables/computed/computed.test.ts +5 -5
  101. package/src/variables/declarative-derivation.test.ts +6 -6
@@ -3,31 +3,104 @@
3
3
  * Re-run the interactive interview for an existing service's provider configuration
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
6
  import {
8
7
  type ProxmoxCredentials,
8
+ type ProxmoxVmTemplate,
9
9
  buildTemplatePath,
10
10
  extractTemplateFilename,
11
11
  listAvailableTemplates,
12
12
  listNodeStorage,
13
13
  } from '../../api-clients/proxmox';
14
+ import { askConfirm, askSelect, askText, withInterviewSession } from '../../services/bus-interview';
14
15
  import {
15
16
  getContainerServiceByServiceId,
16
17
  getServiceCredentials,
17
18
  updateServiceProviderConfig,
18
19
  } from '../../services/container-service';
19
- import { celiloIntro, celiloOutro, promptText } from '../prompts';
20
+ import { celiloIntro, celiloOutro } from '../prompts';
20
21
  import type { CommandResult } from '../types';
21
- import { validateRequired } from '../validators';
22
22
  import {
23
23
  runApplianceDownload,
24
24
  selectUbuntuApplianceFromCatalog,
25
25
  } from './proxmox-template-selection';
26
+ import { buildCloudInitTemplate, detectExistingVmTemplates } from './proxmox-vm-template-build';
26
27
 
27
- interface ProxmoxProviderConfig {
28
+ // A `type` (not `interface`) so it carries an implicit index signature and is
29
+ // assignable to the loose `Record<string, unknown>` providerConfig column.
30
+ export type ProxmoxProviderConfig = {
28
31
  default_target_node: string;
29
32
  lxc_template: string;
30
33
  storage: string;
34
+ // VM template to clone for `requires.system.type: vm` modules. Optional —
35
+ // a service may only ever host LXC modules. MUST be carried through a
36
+ // reconfigure (ISS-0128): updateServiceProviderConfig replaces the whole
37
+ // column, so dropping it here silently deletes the operator's template.
38
+ vm_template?: string;
39
+ };
40
+
41
+ // Sentinel choices for the VM-template reconfigure step (non-template values
42
+ // returned by the select). Any other value is an existing template name.
43
+ const VM_KEEP = '__keep__';
44
+ const VM_BUILD = '__build__';
45
+ const VM_CLEAR = '__clear__';
46
+ const VM_SKIP = '__skip__';
47
+
48
+ export interface VmTemplateChoice {
49
+ value: string;
50
+ label: string;
51
+ }
52
+
53
+ /**
54
+ * Build the select options for the VM-template reconfigure step, given the
55
+ * current value and the templates detected on the node. Pure — no I/O — so the
56
+ * menu is unit-testable and a headless caller (ISS-0127) can resolve a choice
57
+ * without a prompt.
58
+ */
59
+ export function buildVmTemplateChoices(
60
+ current: string | undefined,
61
+ existing: ProxmoxVmTemplate[],
62
+ ): VmTemplateChoice[] {
63
+ const choices: VmTemplateChoice[] = [];
64
+ if (current) {
65
+ choices.push({ value: VM_KEEP, label: `Keep current (${current})` });
66
+ }
67
+ for (const template of existing) {
68
+ if (template.name === current) continue;
69
+ choices.push({ value: template.name, label: `${template.name} (VMID ${template.vmid})` });
70
+ }
71
+ choices.push({ value: VM_BUILD, label: 'Build a new Ubuntu 24.04 cloud-init template now' });
72
+ choices.push(
73
+ current
74
+ ? { value: VM_CLEAR, label: 'Clear — remove the VM template' }
75
+ : { value: VM_SKIP, label: 'Skip — no VM template' },
76
+ );
77
+ return choices;
78
+ }
79
+
80
+ /**
81
+ * Resolve the full providerConfig to persist from the reconfigure interview's
82
+ * resolved values. The caller seeds every field from the current config (via the
83
+ * prompt defaults and the VM-template choice), so the result is a function of
84
+ * `edits` alone. vm_template is included only when set — a falsy value clears it
85
+ * rather than persisting an undefined key. This is the guard for the
86
+ * wholesale-replace drop bug (ISS-0128): vm_template is a first-class edit, not
87
+ * an afterthought that gets omitted from the written object.
88
+ */
89
+ export function resolveReconfiguredProviderConfig(edits: {
90
+ default_target_node: string;
91
+ lxc_template: string;
92
+ storage: string;
93
+ vm_template: string | undefined;
94
+ }): ProxmoxProviderConfig {
95
+ const config: ProxmoxProviderConfig = {
96
+ default_target_node: edits.default_target_node,
97
+ lxc_template: edits.lxc_template,
98
+ storage: edits.storage,
99
+ };
100
+ if (edits.vm_template) {
101
+ config.vm_template = edits.vm_template;
102
+ }
103
+ return config;
31
104
  }
32
105
 
33
106
  export async function handleServiceReconfigure(
@@ -42,146 +115,196 @@ export async function handleServiceReconfigure(
42
115
  };
43
116
  }
44
117
 
45
- try {
46
- const service = await getContainerServiceByServiceId(serviceId);
47
- if (!service) {
48
- return { success: false, error: `Service '${serviceId}' not found` };
49
- }
118
+ // Every prompt below is a bus interview (ISS-0127), so reconfigure is
119
+ // drivable headlessly via `celilo events respond --values` / `events reply`.
120
+ // `withInterviewSession` runs a terminal-responder when stdin is a TTY so
121
+ // those questions render locally; on a non-TTY run the headless responder
122
+ // answers instead.
123
+ const scope = `service:${serviceId}`;
50
124
 
51
- if (service.providerName !== 'proxmox') {
52
- return {
53
- success: false,
54
- error: `Service reconfigure is currently only supported for Proxmox services (got: ${service.providerName})`,
55
- };
56
- }
125
+ return withInterviewSession(async () => {
126
+ try {
127
+ const service = await getContainerServiceByServiceId(serviceId);
128
+ if (!service) {
129
+ return { success: false, error: `Service '${serviceId}' not found` };
130
+ }
57
131
 
58
- celiloIntro(`Reconfigure Service: ${service.name}`);
59
-
60
- const credentials = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
61
- const currentConfig = service.providerConfig as unknown as ProxmoxProviderConfig;
62
-
63
- // Show current configuration
64
- console.log('Current configuration:');
65
- console.log(` Target node: ${currentConfig.default_target_node}`);
66
- console.log(` Storage: ${currentConfig.storage}`);
67
- console.log(` Template: ${currentConfig.lxc_template}`);
68
- console.log('');
69
-
70
- // Re-prompt for configurable fields with current values as defaults
71
- const targetNode = await promptText({
72
- message: 'Default target node:',
73
- defaultValue: currentConfig.default_target_node,
74
- placeholder: currentConfig.default_target_node,
75
- validate: validateRequired('Target node'),
76
- });
77
-
78
- const storage = await promptText({
79
- message: 'Default storage:',
80
- defaultValue: currentConfig.storage,
81
- placeholder: currentConfig.storage,
82
- validate: validateRequired('Storage'),
83
- });
84
-
85
- // Find template storage. Done before catalog selection so the user only
86
- // ever sees the chosen storage in subsequent messages.
87
- console.log('\nFinding storage for templates...');
88
- const storageListResult = await listNodeStorage(credentials, targetNode);
89
-
90
- let templateStorage = 'local';
91
- if (storageListResult.success) {
92
- const vztmplStorage = storageListResult.data.find(
93
- (s) => s.active && s.enabled && s.content.includes('vztmpl'),
94
- );
95
- if (vztmplStorage) {
96
- templateStorage = vztmplStorage.storage;
97
- console.log(`✓ Using storage '${templateStorage}' for templates`);
132
+ if (service.providerName !== 'proxmox') {
133
+ return {
134
+ success: false,
135
+ error: `Service reconfigure is currently only supported for Proxmox services (got: ${service.providerName})`,
136
+ };
98
137
  }
99
- }
100
138
 
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);
139
+ celiloIntro(`Reconfigure Service: ${service.name}`);
115
140
 
116
- // Check if new template exists in storage.
117
- console.log(`\nChecking if template '${templateFilename}' exists...`);
118
- const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
141
+ const credentials = (await getServiceCredentials(service.id)) as ProxmoxCredentials;
142
+ const currentConfig = service.providerConfig as unknown as ProxmoxProviderConfig;
119
143
 
120
- let templateExists = false;
121
- if (templatesResult.success) {
122
- templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
123
- }
144
+ // Show current configuration
145
+ console.log('Current configuration:');
146
+ console.log(` Target node: ${currentConfig.default_target_node}`);
147
+ console.log(` Storage: ${currentConfig.storage}`);
148
+ console.log(` LXC template: ${currentConfig.lxc_template}`);
149
+ console.log(` VM template: ${currentConfig.vm_template ?? '(none)'}`);
150
+ console.log('');
124
151
 
125
- if (!templateExists) {
126
- console.log(`✗ Template '${templateFilename}' not found`);
152
+ // Re-prompt for configurable fields with current values as defaults
153
+ const targetNode = await askText({
154
+ scope,
155
+ key: 'default_target_node',
156
+ message: 'Default target node',
157
+ defaultValue: currentConfig.default_target_node,
158
+ placeholder: currentConfig.default_target_node,
159
+ required: true,
160
+ });
127
161
 
128
- const shouldDownload = await p.confirm({
129
- message: 'Download template now?',
130
- initialValue: true,
162
+ const storage = await askText({
163
+ scope,
164
+ key: 'storage',
165
+ message: 'Default storage',
166
+ defaultValue: currentConfig.storage,
167
+ placeholder: currentConfig.storage,
168
+ required: true,
131
169
  });
132
170
 
133
- if (p.isCancel(shouldDownload) || !shouldDownload) {
134
- console.log(
135
- '\nTemplate not downloaded. Service will be updated but may fail verification.',
171
+ // Find template storage. Done before catalog selection so the user only
172
+ // ever sees the chosen storage in subsequent messages.
173
+ console.log('\nFinding storage for templates...');
174
+ const storageListResult = await listNodeStorage(credentials, targetNode);
175
+
176
+ let templateStorage = 'local';
177
+ if (storageListResult.success) {
178
+ const vztmplStorage = storageListResult.data.find(
179
+ (s) => s.active && s.enabled && s.content.includes('vztmpl'),
136
180
  );
137
- } else {
138
- const outcome = await runApplianceDownload({
139
- credentials,
140
- targetNode,
141
- templateStorage,
142
- templateFilename,
181
+ if (vztmplStorage) {
182
+ templateStorage = vztmplStorage.storage;
183
+ console.log(`✓ Using storage '${templateStorage}' for templates`);
184
+ }
185
+ }
186
+
187
+ // Pick a template from Proxmox's live catalog. Pre-selects the family
188
+ // matching the existing volid (e.g. user has 24.04 -1 cached, mirror has -2).
189
+ // The select + manual-entry fallback inside go through the bus interview
190
+ // too (ISS-0127), scoped to this service.
191
+ const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
192
+ scope,
193
+ currentTemplate: extractTemplateFilename(currentConfig.lxc_template),
194
+ });
195
+ if (selectionResult.kind === 'cancelled') {
196
+ return { success: false, error: 'Cancelled by user' };
197
+ }
198
+ if (selectionResult.kind === 'error') {
199
+ return { success: false, error: selectionResult.message };
200
+ }
201
+ const templateFilename = selectionResult.choice.template;
202
+ const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
203
+
204
+ // Check if new template exists in storage.
205
+ console.log(`\nChecking if template '${templateFilename}' exists...`);
206
+ const templatesResult = await listAvailableTemplates(
207
+ credentials,
208
+ targetNode,
209
+ templateStorage,
210
+ );
211
+
212
+ let templateExists = false;
213
+ if (templatesResult.success) {
214
+ templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
215
+ }
216
+
217
+ if (!templateExists) {
218
+ console.log(`✗ Template '${templateFilename}' not found`);
219
+
220
+ const shouldDownload = await askConfirm({
221
+ scope,
222
+ key: 'download_template',
223
+ message: 'Download template now?',
224
+ defaultValue: true,
143
225
  });
144
226
 
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';
152
- return {
153
- success: false,
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`,
155
- };
227
+ if (!shouldDownload) {
228
+ console.log(
229
+ '\nTemplate not downloaded. Service will be updated but may fail verification.',
230
+ );
231
+ } else {
232
+ const outcome = await runApplianceDownload({
233
+ credentials,
234
+ targetNode,
235
+ templateStorage,
236
+ templateFilename,
237
+ });
238
+
239
+ if (!outcome.ready) {
240
+ const detail =
241
+ outcome.reason === 'task-failed'
242
+ ? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}`
243
+ : outcome.reason === 'started-failed'
244
+ ? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}`
245
+ : 'Template download did not complete in time';
246
+ return {
247
+ success: false,
248
+ 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`,
249
+ };
250
+ }
156
251
  }
252
+ } else {
253
+ console.log(`✓ Template '${templateFilename}' found`);
157
254
  }
158
- } else {
159
- console.log(`✓ Template '${templateFilename}' found`);
160
- }
161
255
 
162
- // Update service configuration
163
- await updateServiceProviderConfig(service.id, {
164
- default_target_node: targetNode,
165
- lxc_template: lxcTemplate,
166
- storage,
167
- });
168
-
169
- celiloOutro(
170
- `Service '${serviceId}' reconfigured successfully!\n\n` +
171
- ` Target node: ${targetNode}\n` +
172
- ` Storage: ${storage}\n` +
173
- ` Template: ${lxcTemplate}\n\n` +
174
- `Next steps:\n - Verify: celilo service verify ${serviceId}\n - Deploy: celilo module deploy <module-id>`,
175
- );
256
+ // VM template (for `requires.system.type: vm` modules). Keep the current
257
+ // value, swap to another detected template, build a new one, or clear it.
258
+ // Without this step a reconfigure would silently drop vm_template (ISS-0128).
259
+ let vmTemplate = currentConfig.vm_template;
260
+ const existingTemplates = await detectExistingVmTemplates(credentials, targetNode);
261
+ const vmChoice = await askSelect({
262
+ scope,
263
+ key: 'vm_template',
264
+ message: 'VM template for VM-type modules',
265
+ options: buildVmTemplateChoices(currentConfig.vm_template, existingTemplates),
266
+ });
267
+ if (vmChoice === VM_BUILD) {
268
+ vmTemplate = await buildCloudInitTemplate({
269
+ credentials,
270
+ nodeName: targetNode,
271
+ diskStorage: storage,
272
+ });
273
+ } else if (vmChoice === VM_CLEAR || vmChoice === VM_SKIP) {
274
+ vmTemplate = undefined;
275
+ } else if (vmChoice !== VM_KEEP) {
276
+ vmTemplate = vmChoice; // an existing template name
277
+ }
278
+ // VM_KEEP leaves vmTemplate at currentConfig.vm_template.
176
279
 
177
- return {
178
- success: true,
179
- message: `Reconfigured service: ${serviceId}`,
180
- };
181
- } catch (error) {
182
- return {
183
- success: false,
184
- error: `Failed to reconfigure service: ${error instanceof Error ? error.message : String(error)}`,
185
- };
186
- }
280
+ // Update service configuration. resolveReconfiguredProviderConfig preserves
281
+ // every existing key and carries vm_template forward (ISS-0128 drop guard).
282
+ const newConfig = resolveReconfiguredProviderConfig({
283
+ default_target_node: targetNode,
284
+ lxc_template: lxcTemplate,
285
+ storage,
286
+ vm_template: vmTemplate,
287
+ });
288
+ await updateServiceProviderConfig(service.id, newConfig);
289
+
290
+ celiloOutro(
291
+ `Service '${serviceId}' reconfigured successfully!\n\n` +
292
+ ` Target node: ${targetNode}\n` +
293
+ ` Storage: ${storage}\n` +
294
+ ` LXC template: ${lxcTemplate}\n` +
295
+ ` VM template: ${vmTemplate ?? '(none)'}\n\n` +
296
+ `Next steps:\n - Verify: celilo service verify ${serviceId}\n - Deploy: celilo module deploy <module-id>`,
297
+ );
298
+
299
+ return {
300
+ success: true,
301
+ message: `Reconfigured service: ${serviceId}`,
302
+ };
303
+ } catch (error) {
304
+ return {
305
+ success: false,
306
+ error: `Failed to reconfigure service: ${error instanceof Error ? error.message : String(error)}`,
307
+ };
308
+ }
309
+ });
187
310
  }
@@ -3,10 +3,10 @@
3
3
  * Remove a container service
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
6
  import { eq } from 'drizzle-orm';
8
7
  import { getDb } from '../../db/client';
9
8
  import { moduleInfrastructure } from '../../db/schema';
9
+ import { askConfirm, withInterviewSession } from '../../services/bus-interview';
10
10
  import {
11
11
  getContainerServiceByServiceId,
12
12
  removeContainerService,
@@ -60,13 +60,16 @@ export async function handleServiceRemove(
60
60
  console.log('');
61
61
 
62
62
  if (!flags.force) {
63
- const confirmed = await p.confirm({
64
- message: 'Remove service and unassign modules?',
65
- initialValue: false,
66
- });
63
+ const confirmed = await withInterviewSession(() =>
64
+ askConfirm({
65
+ scope: `service:${service.serviceId}`,
66
+ key: 'remove_unassign_modules',
67
+ message: 'Remove service and unassign modules?',
68
+ defaultValue: false,
69
+ }),
70
+ );
67
71
 
68
- if (p.isCancel(confirmed) || !confirmed) {
69
- p.cancel('Operation cancelled');
72
+ if (!confirmed) {
70
73
  return { success: false, error: 'Cancelled by user' };
71
74
  }
72
75
  }
@@ -74,13 +77,16 @@ export async function handleServiceRemove(
74
77
 
75
78
  // Confirm deletion
76
79
  if (!flags.force) {
77
- const confirmed = await p.confirm({
78
- message: `Remove service '${service.serviceId}' (${service.name})?`,
79
- initialValue: false,
80
- });
80
+ const confirmed = await withInterviewSession(() =>
81
+ askConfirm({
82
+ scope: `service:${service.serviceId}`,
83
+ key: 'remove',
84
+ message: `Remove service '${service.serviceId}' (${service.name})?`,
85
+ defaultValue: false,
86
+ }),
87
+ );
81
88
 
82
- if (p.isCancel(confirmed) || !confirmed) {
83
- p.cancel('Operation cancelled');
89
+ if (!confirmed) {
84
90
  return { success: false, error: 'Cancelled by user' };
85
91
  }
86
92
  }
@@ -3,8 +3,8 @@
3
3
  * Re-verify a container service connection
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
6
  import { extractTemplateFilename } from '../../api-clients/proxmox';
7
+ import { askConfirm, withInterviewSession } from '../../services/bus-interview';
8
8
  import {
9
9
  type ProxmoxCredentials,
10
10
  getContainerServiceByServiceId,
@@ -91,15 +91,14 @@ export async function handleServiceVerify(
91
91
  testResult.message.includes('Template') &&
92
92
  testResult.message.includes('not found')
93
93
  ) {
94
- const shouldDownload = await p.confirm({
95
- message: 'Download missing template now?',
96
- initialValue: true,
97
- });
98
-
99
- if (p.isCancel(shouldDownload)) {
100
- celiloOutro('Verification cancelled');
101
- return { success: false, error: 'Cancelled by user' };
102
- }
94
+ const shouldDownload = await withInterviewSession(() =>
95
+ askConfirm({
96
+ scope: `service:${service.serviceId}`,
97
+ key: 'download_template',
98
+ message: 'Download missing template now?',
99
+ defaultValue: true,
100
+ }),
101
+ );
103
102
 
104
103
  if (shouldDownload) {
105
104
  try {