@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
@@ -12,9 +12,9 @@ import {
12
12
  setDefaultBackupStorage,
13
13
  verifyBackupStorage,
14
14
  } from '../../services/backup-storage';
15
- import { celiloIntro, celiloOutro, promptConfirm, promptText } from '../prompts';
15
+ import { askConfirm, askText, withInterviewSession } from '../../services/bus-interview';
16
+ import { celiloIntro, celiloOutro } from '../prompts';
16
17
  import type { CommandResult } from '../types';
17
- import { validateRequired } from '../validators';
18
18
 
19
19
  /**
20
20
  * Best-effort write probe. Attempts to mkdir -p a `.celilo-write-probe`
@@ -52,122 +52,135 @@ export async function handleStorageAddLocal(
52
52
  _args: string[],
53
53
  flags: Record<string, boolean | string> = {},
54
54
  ): Promise<CommandResult> {
55
- try {
56
- celiloIntro('Add Local Backup Storage');
57
-
58
- // Support non-interactive mode via flags
59
- const flagName = typeof flags.name === 'string' ? flags.name : undefined;
60
- const flagPath = typeof flags.path === 'string' ? flags.path : undefined;
61
-
62
- const name =
63
- flagName ??
64
- (await promptText({
65
- message: 'Human-readable name:',
66
- defaultValue: 'Local Backups',
67
- placeholder: 'Local Backups',
68
- validate: validateRequired('Storage name'),
69
- }));
70
-
71
- // Default path lives under celilo's own data dir
72
- // (~/.local/share/celilo/backups on Linux, ~/Library/Application
73
- // Support/celilo/backups on macOS). The directory is owned by the
74
- // running user, mkdir -p succeeds without sudo, and operators who
75
- // want NAS / external paths can override at the prompt. This
76
- // replaces the prior placeholder of "/var/backups/celilo" which
77
- // looked authoritative but required root to write.
78
- const defaultPath = join(getDataDir(), 'backups');
79
-
80
- // Probe writability BEFORE saving the storage row. The previous
81
- // flow (save verify fail with EACCES → leave dangling
82
- // unverified row) was confusing and required the operator to know
83
- // about `storage verify` to recover. Probing here means an
84
- // unwriteable path never produces persistent state.
85
- //
86
- // Non-interactive (--name + --path): probe once, fail loud.
87
- // Interactive: re-prompt until a writeable path is supplied.
88
- let resolvedPath: string;
89
- if (flagPath !== undefined) {
90
- resolvedPath = resolve(expandTilde(flagPath));
91
- const writeError = probePathWriteable(resolvedPath);
92
- if (writeError !== null) {
93
- return {
94
- success: false,
95
- error: `Path '${resolvedPath}' is not writeable: ${writeError}\n\nTry a path under your home directory (e.g. '${defaultPath}') or run with elevated permissions.`,
96
- };
97
- }
98
- } else {
99
- let candidate: string | undefined;
100
- while (candidate === undefined) {
101
- const typed = await promptText({
102
- message: 'Storage directory path:',
103
- defaultValue: defaultPath,
104
- placeholder: defaultPath,
105
- validate: validateRequired('Storage path'),
106
- });
107
- const resolved = resolve(expandTilde(typed));
108
- const writeError = probePathWriteable(resolved);
109
- if (writeError === null) {
110
- candidate = resolved;
111
- break;
55
+ // Non-secret prompts route through the bus interview (ISS-0127). Local
56
+ // storage has no secret credentials. `withInterviewSession` renders bus
57
+ // questions locally when stdin is a TTY.
58
+ const scope = 'storage-add:local';
59
+
60
+ return withInterviewSession(async () => {
61
+ try {
62
+ celiloIntro('Add Local Backup Storage');
63
+
64
+ // Support non-interactive mode via flags
65
+ const flagName = typeof flags.name === 'string' ? flags.name : undefined;
66
+ const flagPath = typeof flags.path === 'string' ? flags.path : undefined;
67
+
68
+ const name =
69
+ flagName ??
70
+ (await askText({
71
+ scope,
72
+ key: 'name',
73
+ message: 'Human-readable name',
74
+ defaultValue: 'Local Backups',
75
+ placeholder: 'Local Backups',
76
+ required: true,
77
+ }));
78
+
79
+ // Default path lives under celilo's own data dir
80
+ // (~/.local/share/celilo/backups on Linux, ~/Library/Application
81
+ // Support/celilo/backups on macOS). The directory is owned by the
82
+ // running user, mkdir -p succeeds without sudo, and operators who
83
+ // want NAS / external paths can override at the prompt. This
84
+ // replaces the prior placeholder of "/var/backups/celilo" which
85
+ // looked authoritative but required root to write.
86
+ const defaultPath = join(getDataDir(), 'backups');
87
+
88
+ // Probe writability BEFORE saving the storage row. The previous
89
+ // flow (save verify → fail with EACCES → leave dangling
90
+ // unverified row) was confusing and required the operator to know
91
+ // about `storage verify` to recover. Probing here means an
92
+ // unwriteable path never produces persistent state.
93
+ //
94
+ // Non-interactive (--name + --path): probe once, fail loud.
95
+ // Interactive: re-prompt until a writeable path is supplied.
96
+ let resolvedPath: string;
97
+ if (flagPath !== undefined) {
98
+ resolvedPath = resolve(expandTilde(flagPath));
99
+ const writeError = probePathWriteable(resolvedPath);
100
+ if (writeError !== null) {
101
+ return {
102
+ success: false,
103
+ error: `Path '${resolvedPath}' is not writeable: ${writeError}\n\nTry a path under your home directory (e.g. '${defaultPath}') or run with elevated permissions.`,
104
+ };
112
105
  }
113
- console.log(
114
- `\n✗ Path '${resolved}' is not writeable: ${writeError}\nTry a path under your home directory (default '${defaultPath}' works without sudo).\n`,
115
- );
116
- // Loop continues re-prompt with the same default suggestion.
106
+ } else {
107
+ let candidate: string | undefined;
108
+ while (candidate === undefined) {
109
+ const typed = await askText({
110
+ scope,
111
+ key: 'path',
112
+ message: 'Storage directory path',
113
+ defaultValue: defaultPath,
114
+ placeholder: defaultPath,
115
+ required: true,
116
+ });
117
+ const resolved = resolve(expandTilde(typed));
118
+ const writeError = probePathWriteable(resolved);
119
+ if (writeError === null) {
120
+ candidate = resolved;
121
+ break;
122
+ }
123
+ console.log(
124
+ `\n✗ Path '${resolved}' is not writeable: ${writeError}\nTry a path under your home directory (default '${defaultPath}' works without sudo).\n`,
125
+ );
126
+ // Loop continues — re-prompt with the same default suggestion.
127
+ }
128
+ resolvedPath = candidate;
117
129
  }
118
- resolvedPath = candidate;
119
- }
120
130
 
121
- const storage = await addBackupStorage({
122
- name,
123
- providerName: 'local',
124
- credentials: { path: resolvedPath },
125
- });
126
-
127
- console.log(`\nStorage '${storage.storageId}' saved`);
128
- console.log('Testing storage access...');
131
+ const storage = await addBackupStorage({
132
+ name,
133
+ providerName: 'local',
134
+ credentials: { path: resolvedPath },
135
+ });
129
136
 
130
- const { result } = await verifyBackupStorage(storage.id);
137
+ console.log(`\nStorage '${storage.storageId}' saved`);
138
+ console.log('Testing storage access...');
131
139
 
132
- if (!result.success) {
133
- // Should be unreachable in interactive mode (the pre-save probe
134
- // covers the EACCES path); could still fire for less-common
135
- // verify failures (full disk, etc.). Keep the dangling-row
136
- // recovery hint as a safety net.
137
- console.log(`\n✗ Verification failed: ${result.message}`);
138
- celiloOutro(
139
- `Storage '${storage.storageId}' added but not verified.\n\nFix the path and re-verify: celilo storage verify ${storage.storageId}`,
140
- );
141
- return { success: true, message: `Added storage: ${storage.storageId} (not verified)` };
142
- }
140
+ const { result } = await verifyBackupStorage(storage.id);
143
141
 
144
- console.log(`✓ ${result.message}`);
142
+ if (!result.success) {
143
+ // Should be unreachable in interactive mode (the pre-save probe
144
+ // covers the EACCES path); could still fire for less-common
145
+ // verify failures (full disk, etc.). Keep the dangling-row
146
+ // recovery hint as a safety net.
147
+ console.log(`\n✗ Verification failed: ${result.message}`);
148
+ celiloOutro(
149
+ `Storage '${storage.storageId}' added but not verified.\n\nFix the path and re-verify: celilo storage verify ${storage.storageId}`,
150
+ );
151
+ return { success: true, message: `Added storage: ${storage.storageId} (not verified)` };
152
+ }
145
153
 
146
- if (flagName && flagPath) {
147
- // Non-interactive: auto-set as default
148
- setDefaultBackupStorage(storage.id);
149
- console.log('✓ Set as default');
150
- } else {
151
- const makeDefault = await promptConfirm({
152
- message: 'Set as default backup destination?',
153
- initialValue: true,
154
- });
154
+ console.log(`✓ ${result.message}`);
155
155
 
156
- if (makeDefault) {
156
+ if (flagName && flagPath) {
157
+ // Non-interactive: auto-set as default
157
158
  setDefaultBackupStorage(storage.id);
158
159
  console.log('✓ Set as default');
160
+ } else {
161
+ const makeDefault = await askConfirm({
162
+ scope,
163
+ key: 'make_default',
164
+ message: 'Set as default backup destination?',
165
+ defaultValue: true,
166
+ });
167
+
168
+ if (makeDefault) {
169
+ setDefaultBackupStorage(storage.id);
170
+ console.log('✓ Set as default');
171
+ }
159
172
  }
160
- }
161
173
 
162
- celiloOutro(
163
- `Storage '${storage.storageId}' (${name}) added and verified!\n\nStorage ID: ${storage.storageId}\nPath: ${resolvedPath}\n\nNext steps:\n celilo module backup <module-id> Create a backup\n celilo storage list List storage destinations`,
164
- );
174
+ celiloOutro(
175
+ `Storage '${storage.storageId}' (${name}) added and verified!\n\nStorage ID: ${storage.storageId}\nPath: ${resolvedPath}\n\nNext steps:\n celilo module backup <module-id> Create a backup\n celilo storage list List storage destinations`,
176
+ );
165
177
 
166
- return { success: true, message: `Added local storage: ${storage.storageId}` };
167
- } catch (error) {
168
- return {
169
- success: false,
170
- error: `Failed to add local storage: ${error instanceof Error ? error.message : String(error)}`,
171
- };
172
- }
178
+ return { success: true, message: `Added local storage: ${storage.storageId}` };
179
+ } catch (error) {
180
+ return {
181
+ success: false,
182
+ error: `Failed to add local storage: ${error instanceof Error ? error.message : String(error)}`,
183
+ };
184
+ }
185
+ });
173
186
  }
@@ -9,151 +9,165 @@ import {
9
9
  setDefaultBackupStorage,
10
10
  verifyBackupStorage,
11
11
  } from '../../services/backup-storage';
12
- import { celiloIntro, celiloOutro, promptConfirm, promptPassword, promptText } from '../prompts';
12
+ import { askConfirm, askText, withInterviewSession } from '../../services/bus-interview';
13
+ import { celiloIntro, celiloOutro } from '../prompts';
14
+ import { resolveServiceCredential } from '../service-credential';
13
15
  import type { CommandResult } from '../types';
14
- import { validateRequired } from '../validators';
15
16
 
16
17
  export async function handleStorageAddS3(
17
18
  _args: string[],
18
19
  flags: Record<string, boolean | string> = {},
19
20
  ): Promise<CommandResult> {
20
- try {
21
- celiloIntro('Add S3 Backup Storage');
22
-
23
- // Support non-interactive mode via flags (scriptable from docker-exec /
24
- // automation). region + endpoint have sensible defaults; name, bucket, and
25
- // the two credential flags are required for a fully non-interactive run.
26
- const flagName = typeof flags.name === 'string' ? flags.name : undefined;
27
- const flagBucket = typeof flags.bucket === 'string' ? flags.bucket : undefined;
28
- const flagRegion = typeof flags.region === 'string' ? flags.region : undefined;
29
- const flagEndpoint = typeof flags.endpoint === 'string' ? flags.endpoint : undefined;
30
- const flagAccessKeyId =
31
- typeof flags['access-key-id'] === 'string' ? flags['access-key-id'] : undefined;
32
- const flagSecretAccessKey =
33
- typeof flags['secret-access-key'] === 'string' ? flags['secret-access-key'] : undefined;
34
-
35
- const nonInteractive = Boolean(
36
- flagName && flagBucket && flagAccessKeyId && flagSecretAccessKey,
37
- );
38
-
39
- const name =
40
- flagName ??
41
- (await promptText({
42
- message: 'Human-readable name:',
43
- placeholder: 'e.g., Backblaze B2 Backups',
44
- validate: validateRequired('Storage name'),
45
- }));
46
-
47
- const bucket =
48
- flagBucket ??
49
- (await promptText({
50
- message: 'Bucket name:',
51
- placeholder: 'e.g., homelab-backups',
52
- validate: validateRequired('Bucket name'),
53
- }));
54
-
55
- const region =
56
- flagRegion ??
57
- (await promptText({
58
- message: 'Region:',
59
- defaultValue: 'us-east-1',
60
- placeholder: 'us-east-1',
61
- validate: validateRequired('Region'),
62
- }));
63
-
64
- const endpoint =
65
- flagEndpoint ??
66
- (await promptText({
67
- message: 'Endpoint URL:',
68
- defaultValue: 'https://s3.amazonaws.com',
69
- placeholder: 'https://s3.amazonaws.com',
70
- validate: (value) => {
71
- const err = validateRequired('Endpoint URL')(value);
72
- if (err) return err;
73
- if (value && !value.startsWith('https://') && !value.startsWith('http://')) {
74
- return 'Endpoint must start with https:// or http://';
75
- }
76
- return undefined;
21
+ // Non-secret prompts route through the bus interview (ISS-0127); the S3
22
+ // secret access key is a credential that travels by flag/env only (D7).
23
+ const scope = 'storage-add:s3';
24
+
25
+ return withInterviewSession(async () => {
26
+ try {
27
+ celiloIntro('Add S3 Backup Storage');
28
+
29
+ // Support non-interactive mode via flags (scriptable from docker-exec /
30
+ // automation). region + endpoint have sensible defaults; name, bucket, and
31
+ // the two credential flags are required for a fully non-interactive run.
32
+ const flagName = typeof flags.name === 'string' ? flags.name : undefined;
33
+ const flagBucket = typeof flags.bucket === 'string' ? flags.bucket : undefined;
34
+ const flagRegion = typeof flags.region === 'string' ? flags.region : undefined;
35
+ const flagEndpoint = typeof flags.endpoint === 'string' ? flags.endpoint : undefined;
36
+ const flagAccessKeyId =
37
+ typeof flags['access-key-id'] === 'string' ? flags['access-key-id'] : undefined;
38
+ const flagSecretAccessKey =
39
+ typeof flags['secret-access-key'] === 'string' ? flags['secret-access-key'] : undefined;
40
+
41
+ const nonInteractive = Boolean(
42
+ flagName && flagBucket && flagAccessKeyId && flagSecretAccessKey,
43
+ );
44
+
45
+ const name =
46
+ flagName ??
47
+ (await askText({
48
+ scope,
49
+ key: 'name',
50
+ message: 'Human-readable name',
51
+ placeholder: 'e.g., Backblaze B2 Backups',
52
+ required: true,
53
+ }));
54
+
55
+ const bucket =
56
+ flagBucket ??
57
+ (await askText({
58
+ scope,
59
+ key: 'bucket',
60
+ message: 'Bucket name',
61
+ placeholder: 'e.g., homelab-backups',
62
+ required: true,
63
+ }));
64
+
65
+ const region =
66
+ flagRegion ??
67
+ (await askText({
68
+ scope,
69
+ key: 'region',
70
+ message: 'Region',
71
+ defaultValue: 'us-east-1',
72
+ placeholder: 'us-east-1',
73
+ required: true,
74
+ }));
75
+
76
+ const endpoint =
77
+ flagEndpoint ??
78
+ (await askText({
79
+ scope,
80
+ key: 'endpoint',
81
+ message: 'Endpoint URL',
82
+ defaultValue: 'https://s3.amazonaws.com',
83
+ placeholder: 'https://s3.amazonaws.com',
84
+ required: true,
85
+ pattern: '^https?://',
86
+ }));
87
+
88
+ if (
89
+ flagEndpoint &&
90
+ !flagEndpoint.startsWith('https://') &&
91
+ !flagEndpoint.startsWith('http://')
92
+ ) {
93
+ return {
94
+ success: false,
95
+ error: `Invalid --endpoint '${flagEndpoint}': must start with https:// or http://`,
96
+ };
97
+ }
98
+
99
+ const accessKeyId =
100
+ flagAccessKeyId ??
101
+ (await askText({
102
+ scope,
103
+ key: 'access_key_id',
104
+ message: 'Access Key ID',
105
+ required: true,
106
+ }));
107
+
108
+ // Secret access key is a credential — flag/env only (D7).
109
+ const secretAccessKey = await resolveServiceCredential({
110
+ field: 'Secret Access Key',
111
+ flag: 'secret-access-key',
112
+ envVar: 'S3_SECRET_ACCESS_KEY',
113
+ flagValue: flagSecretAccessKey,
114
+ });
115
+
116
+ const storage = await addBackupStorage({
117
+ name,
118
+ providerName: 's3',
119
+ credentials: {
120
+ bucket,
121
+ region,
122
+ endpoint,
123
+ accessKeyId,
124
+ secretAccessKey,
77
125
  },
78
- }));
126
+ });
79
127
 
80
- if (
81
- flagEndpoint &&
82
- !flagEndpoint.startsWith('https://') &&
83
- !flagEndpoint.startsWith('http://')
84
- ) {
85
- return {
86
- success: false,
87
- error: `Invalid --endpoint '${flagEndpoint}': must start with https:// or http://`,
88
- };
89
- }
128
+ console.log(`\nStorage '${storage.storageId}' saved`);
129
+ console.log('Testing connection...');
90
130
 
91
- const accessKeyId =
92
- flagAccessKeyId ??
93
- (await promptText({
94
- message: 'Access Key ID:',
95
- validate: validateRequired('Access Key ID'),
96
- }));
97
-
98
- const secretAccessKey =
99
- flagSecretAccessKey ??
100
- (await promptPassword({
101
- message: 'Secret Access Key:',
102
- validate: validateRequired('Secret Access Key'),
103
- }));
104
-
105
- const storage = await addBackupStorage({
106
- name,
107
- providerName: 's3',
108
- credentials: {
109
- bucket,
110
- region,
111
- endpoint,
112
- accessKeyId,
113
- secretAccessKey,
114
- },
115
- });
116
-
117
- console.log(`\nStorage '${storage.storageId}' saved`);
118
- console.log('Testing connection...');
119
-
120
- const { result } = await verifyBackupStorage(storage.id);
121
-
122
- if (!result.success) {
123
- console.log(`\n✗ Verification failed: ${result.message}`);
124
- celiloOutro(
125
- `Storage '${storage.storageId}' added but not verified.\n\nCheck credentials and re-verify: celilo storage verify ${storage.storageId}`,
126
- );
127
- return { success: true, message: `Added storage: ${storage.storageId} (not verified)` };
128
- }
131
+ const { result } = await verifyBackupStorage(storage.id);
129
132
 
130
- console.log(`✓ ${result.message}`);
133
+ if (!result.success) {
134
+ console.log(`\n✗ Verification failed: ${result.message}`);
135
+ celiloOutro(
136
+ `Storage '${storage.storageId}' added but not verified.\n\nCheck credentials and re-verify: celilo storage verify ${storage.storageId}`,
137
+ );
138
+ return { success: true, message: `Added storage: ${storage.storageId} (not verified)` };
139
+ }
131
140
 
132
- if (nonInteractive) {
133
- // Non-interactive: auto-set as default (matches storage-add-local).
134
- setDefaultBackupStorage(storage.id);
135
- console.log('✓ Set as default');
136
- } else {
137
- const makeDefault = await promptConfirm({
138
- message: 'Set as default backup destination?',
139
- initialValue: true,
140
- });
141
+ console.log(`✓ ${result.message}`);
141
142
 
142
- if (makeDefault) {
143
+ if (nonInteractive) {
144
+ // Non-interactive: auto-set as default (matches storage-add-local).
143
145
  setDefaultBackupStorage(storage.id);
144
146
  console.log('✓ Set as default');
147
+ } else {
148
+ const makeDefault = await askConfirm({
149
+ scope,
150
+ key: 'make_default',
151
+ message: 'Set as default backup destination?',
152
+ defaultValue: true,
153
+ });
154
+
155
+ if (makeDefault) {
156
+ setDefaultBackupStorage(storage.id);
157
+ console.log('✓ Set as default');
158
+ }
145
159
  }
146
- }
147
160
 
148
- celiloOutro(
149
- `Storage '${storage.storageId}' (${name}) added and verified!\n\nStorage ID: ${storage.storageId}\nBucket: ${bucket}\nEndpoint: ${endpoint}\n\nNext steps:\n celilo module backup <module-id> Create a backup\n celilo storage list List storage destinations`,
150
- );
151
-
152
- return { success: true, message: `Added S3 storage: ${storage.storageId}` };
153
- } catch (error) {
154
- return {
155
- success: false,
156
- error: `Failed to add S3 storage: ${error instanceof Error ? error.message : String(error)}`,
157
- };
158
- }
161
+ celiloOutro(
162
+ `Storage '${storage.storageId}' (${name}) added and verified!\n\nStorage ID: ${storage.storageId}\nBucket: ${bucket}\nEndpoint: ${endpoint}\n\nNext steps:\n celilo module backup <module-id> Create a backup\n celilo storage list List storage destinations`,
163
+ );
164
+
165
+ return { success: true, message: `Added S3 storage: ${storage.storageId}` };
166
+ } catch (error) {
167
+ return {
168
+ success: false,
169
+ error: `Failed to add S3 storage: ${error instanceof Error ? error.message : String(error)}`,
170
+ };
171
+ }
172
+ });
159
173
  }
@@ -3,8 +3,8 @@
3
3
  * Remove a backup storage destination
4
4
  */
5
5
 
6
- import * as p from '@clack/prompts';
7
6
  import { getBackupStorageByStorageId, removeBackupStorage } from '../../services/backup-storage';
7
+ import { askConfirm, withInterviewSession } from '../../services/bus-interview';
8
8
  import { celiloIntro, celiloOutro } from '../prompts';
9
9
  import type { CommandResult } from '../types';
10
10
 
@@ -29,13 +29,16 @@ export async function handleStorageRemove(
29
29
  }
30
30
 
31
31
  if (!flags.force) {
32
- const confirmed = await p.confirm({
33
- message: `Remove storage '${storage.storageId}' (${storage.name})?`,
34
- initialValue: false,
35
- });
36
-
37
- if (p.isCancel(confirmed) || !confirmed) {
38
- p.cancel('Operation cancelled');
32
+ const confirmed = await withInterviewSession(() =>
33
+ askConfirm({
34
+ scope: `storage:${storage.storageId}`,
35
+ key: 'remove',
36
+ message: `Remove storage '${storage.storageId}' (${storage.name})?`,
37
+ defaultValue: false,
38
+ }),
39
+ );
40
+
41
+ if (!confirmed) {
39
42
  return { success: false, error: 'Cancelled by user' };
40
43
  }
41
44
  }