@celilo/cli 0.5.0-alpha.7 → 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.
- package/package.json +2 -2
- package/src/api-clients/proxmox.test.ts +78 -0
- package/src/api-clients/proxmox.ts +96 -1
- package/src/cli/command-registry.ts +32 -3
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/events.ts +8 -3
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +10 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +275 -260
- package/src/cli/commands/service-reconfigure.ts +171 -153
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/completion.ts +15 -0
- package/src/cli/index.ts +25 -0
- package/src/cli/service-credential.ts +54 -0
- package/src/services/bus-interview.ts +232 -0
- package/src/services/deploy-validation.test.ts +52 -2
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/fleet-checks.test.ts +13 -0
- package/src/services/fleet-checks.ts +15 -0
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +30 -0
- 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 {
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
137
|
+
console.log(`\nStorage '${storage.storageId}' saved`);
|
|
138
|
+
console.log('Testing storage access...');
|
|
131
139
|
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
typeof flags
|
|
32
|
-
|
|
33
|
-
typeof flags
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
}
|