@calltelemetry/cli 0.15.1 → 1.1.0
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/README.md +47 -0
- package/dist/commands/cube.d.ts.map +1 -1
- package/dist/commands/cube.js +68 -0
- package/dist/commands/cube.js.map +1 -1
- package/dist/commands/sftp.d.ts +3 -0
- package/dist/commands/sftp.d.ts.map +1 -0
- package/dist/commands/sftp.js +1586 -0
- package/dist/commands/sftp.js.map +1 -0
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/appliance-api.d.ts +149 -0
- package/dist/lib/appliance-api.d.ts.map +1 -0
- package/dist/lib/appliance-api.js +492 -0
- package/dist/lib/appliance-api.js.map +1 -0
- package/dist/lib/cube-diag.d.ts +6 -0
- package/dist/lib/cube-diag.d.ts.map +1 -1
- package/dist/lib/cube-diag.js +9 -0
- package/dist/lib/cube-diag.js.map +1 -1
- package/dist/lib/cube-doctor.d.ts +107 -0
- package/dist/lib/cube-doctor.d.ts.map +1 -0
- package/dist/lib/cube-doctor.js +426 -0
- package/dist/lib/cube-doctor.js.map +1 -0
- package/dist/lib/openapi.d.ts +129 -0
- package/dist/lib/openapi.d.ts.map +1 -0
- package/dist/lib/openapi.js +325 -0
- package/dist/lib/openapi.js.map +1 -0
- package/dist/lib/sftp.d.ts +231 -0
- package/dist/lib/sftp.d.ts.map +1 -0
- package/dist/lib/sftp.js +439 -0
- package/dist/lib/sftp.js.map +1 -0
- package/dist/lib/version.d.ts +1 -1
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/dist/utils/prompt.d.ts +14 -0
- package/dist/utils/prompt.d.ts.map +1 -1
- package/dist/utils/prompt.js +28 -0
- package/dist/utils/prompt.js.map +1 -1
- package/dist/utils/table.d.ts +18 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +97 -0
- package/dist/utils/table.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1586 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import prompts from 'prompts';
|
|
3
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
import { ApplianceApi, ApplianceApiError, DEFAULT_API_URL, DEFAULT_TIMEOUT_MS, isLocalBaseUrl, parsePositiveInt, readJsonOption, } from '../lib/appliance-api.js';
|
|
6
|
+
import { describeSchema, getOperation, listOperations, operationBodySchema, operationQueryParams, pathPlaceholders, validateAgainstSchema, } from '../lib/openapi.js';
|
|
7
|
+
import { addAccountContacts, buildCreateAccountPayload, buildDiscoverPayload, buildMonitoringPayload, buildUpdateAccountPayload, collectSftpStatus, getAccountActivity, getAccountContacts, getAccountFiles, getAccountMonitoring, getAccountQuota, getAccountRuns, listAccounts, listMonitoredAccounts, parseContactIds, readImportFile, removeAccountContact, requireOperation, resolveAccount, supportsOperation, } from '../lib/sftp.js';
|
|
8
|
+
import { checkTcpPort } from '../lib/network.js';
|
|
9
|
+
import { rpc } from '../lib/rpc.js';
|
|
10
|
+
import { redactSecrets } from '../lib/redact.js';
|
|
11
|
+
import { getLogLevel } from '../lib/log.js';
|
|
12
|
+
import * as out from '../utils/output.js';
|
|
13
|
+
import { formatBytes, formatDateTime, formatGb, formatPercent, formatQuota, renderTable, } from '../utils/table.js';
|
|
14
|
+
function collect(value, previous = []) {
|
|
15
|
+
return [...previous, value];
|
|
16
|
+
}
|
|
17
|
+
function parseIntegerOption(value, label, opts = {}) {
|
|
18
|
+
if (value === undefined || value === '')
|
|
19
|
+
return opts.fallback;
|
|
20
|
+
const parsed = Number(value);
|
|
21
|
+
const min = opts.min ?? 0;
|
|
22
|
+
const max = opts.max ?? Number.MAX_SAFE_INTEGER;
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
24
|
+
throw new ApplianceApiError(`${label} must be an integer between ${min} and ${max}.`);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function parsePair(value, label) {
|
|
29
|
+
const index = value.indexOf('=');
|
|
30
|
+
if (index <= 0) {
|
|
31
|
+
throw new ApplianceApiError(`${label} must use key=value syntax: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return [value.slice(0, index), value.slice(index + 1)];
|
|
34
|
+
}
|
|
35
|
+
function parsePairs(values, label) {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const value of values ?? []) {
|
|
38
|
+
const [key, raw] = parsePair(value, label);
|
|
39
|
+
result[key] = raw;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function printJson(value) {
|
|
44
|
+
console.log(JSON.stringify(value, null, 2));
|
|
45
|
+
}
|
|
46
|
+
function printError(err) {
|
|
47
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
48
|
+
console.error(chalk.red(' ✗'), redactSecrets(message));
|
|
49
|
+
if (getLogLevel() === 'debug' && err instanceof Error && err.stack) {
|
|
50
|
+
console.error(chalk.gray(err.stack));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Wrap a Commander action with friendly error output and a non-zero exit code. */
|
|
54
|
+
function action(fn) {
|
|
55
|
+
return async (...args) => {
|
|
56
|
+
try {
|
|
57
|
+
await fn(...args);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
printError(err);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function addApiOptions(command, withJson = false) {
|
|
66
|
+
command
|
|
67
|
+
.option('--url <url>', `Appliance API base URL (default: ${DEFAULT_API_URL})`)
|
|
68
|
+
.option('--token <token>', 'Machine API token for automation (prefer CT_API_TOKEN)')
|
|
69
|
+
.option('--email <email>', 'Admin email for interactive session login (or CT_API_EMAIL)')
|
|
70
|
+
.option('--org <id>', 'Organization ID (or CT_ORG_ID)')
|
|
71
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', String(DEFAULT_TIMEOUT_MS))
|
|
72
|
+
.option('--verify-tls', 'Verify the appliance TLS certificate');
|
|
73
|
+
if (withJson)
|
|
74
|
+
command.option('--json', 'Output JSON');
|
|
75
|
+
return command;
|
|
76
|
+
}
|
|
77
|
+
async function connect(options, opts = {}) {
|
|
78
|
+
return ApplianceApi.connect({
|
|
79
|
+
baseUrl: options.url,
|
|
80
|
+
token: options.token,
|
|
81
|
+
email: options.email,
|
|
82
|
+
orgId: parsePositiveInt(options.org, '--org'),
|
|
83
|
+
timeoutMs: parseIntegerOption(options.timeout, '--timeout', { min: 1000 }),
|
|
84
|
+
verifyTls: options.verifyTls ? true : undefined,
|
|
85
|
+
requireAuth: opts.requireAuth ?? true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function connectWithSpec(options) {
|
|
89
|
+
const api = await connect(options);
|
|
90
|
+
const spec = await api.fetchSpec();
|
|
91
|
+
return { api, spec };
|
|
92
|
+
}
|
|
93
|
+
function printKeyValues(rows) {
|
|
94
|
+
if (rows.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
const width = Math.max(...rows.map(([key]) => key.length));
|
|
97
|
+
for (const [key, value] of rows) {
|
|
98
|
+
console.log(` ${chalk.gray(key.padEnd(width))} ${value}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function accountLabel(account) {
|
|
102
|
+
return `${account.username} (id ${account.id})`;
|
|
103
|
+
}
|
|
104
|
+
function actionSummary(actions) {
|
|
105
|
+
if (!Array.isArray(actions) || actions.length === 0)
|
|
106
|
+
return 'none';
|
|
107
|
+
return actions
|
|
108
|
+
.map(item => {
|
|
109
|
+
if (typeof item === 'object' && item !== null && 'type' in item) {
|
|
110
|
+
return String(item.type);
|
|
111
|
+
}
|
|
112
|
+
return String(item);
|
|
113
|
+
})
|
|
114
|
+
.join(', ');
|
|
115
|
+
}
|
|
116
|
+
function monitoringSchedule(monitoring) {
|
|
117
|
+
const schedule = monitoring?.schedule;
|
|
118
|
+
if (!schedule?.type)
|
|
119
|
+
return '—';
|
|
120
|
+
const time = schedule.hour !== null && schedule.hour !== undefined
|
|
121
|
+
? ` ${String(schedule.hour).padStart(2, '0')}:${String(schedule.minute ?? 0).padStart(2, '0')}`
|
|
122
|
+
: '';
|
|
123
|
+
return `${schedule.type}${time}`;
|
|
124
|
+
}
|
|
125
|
+
function isTty() {
|
|
126
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
127
|
+
}
|
|
128
|
+
async function promptHidden(message) {
|
|
129
|
+
if (!isTty())
|
|
130
|
+
return undefined;
|
|
131
|
+
const { value } = await prompts({ type: 'password', name: 'value', message });
|
|
132
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
133
|
+
}
|
|
134
|
+
function readPasswordFromStdin() {
|
|
135
|
+
const value = readFileSync(0, 'utf-8').replace(/\r?\n$/, '');
|
|
136
|
+
if (!value)
|
|
137
|
+
throw new ApplianceApiError('No password received on stdin.');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
async function resolveNewPassword(opts) {
|
|
141
|
+
if (opts.password)
|
|
142
|
+
return opts.password;
|
|
143
|
+
if (opts.passwordStdin)
|
|
144
|
+
return readPasswordFromStdin();
|
|
145
|
+
if (opts.generate)
|
|
146
|
+
return undefined;
|
|
147
|
+
const prompted = await promptHidden('SFTP password (leave blank to generate a secure password)');
|
|
148
|
+
return prompted;
|
|
149
|
+
}
|
|
150
|
+
function redactAccount(account) {
|
|
151
|
+
if (typeof account.password !== 'string')
|
|
152
|
+
return account;
|
|
153
|
+
return { ...account, password: '[REDACTED]' };
|
|
154
|
+
}
|
|
155
|
+
async function confirmDestructive(message, yes) {
|
|
156
|
+
if (yes)
|
|
157
|
+
return;
|
|
158
|
+
if (!isTty()) {
|
|
159
|
+
throw new ApplianceApiError('Refusing to continue without --yes in a non-interactive shell.');
|
|
160
|
+
}
|
|
161
|
+
const { value } = await prompts({
|
|
162
|
+
type: 'confirm',
|
|
163
|
+
name: 'value',
|
|
164
|
+
message,
|
|
165
|
+
initial: false,
|
|
166
|
+
});
|
|
167
|
+
if (!value)
|
|
168
|
+
throw new ApplianceApiError('Aborted.');
|
|
169
|
+
}
|
|
170
|
+
function printGeneratedPassword(password) {
|
|
171
|
+
if (!password)
|
|
172
|
+
return;
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(` ${chalk.bold('Generated password:')} ${chalk.cyan(password)}`);
|
|
175
|
+
console.log(chalk.gray(' Save this now. It cannot be retrieved later.'));
|
|
176
|
+
}
|
|
177
|
+
// ── Status and doctor ────────────────────────────────────────────────────
|
|
178
|
+
async function sftpDaemonStatus(baseUrl) {
|
|
179
|
+
if (!isLocalBaseUrl(baseUrl)) {
|
|
180
|
+
return { ok: true, detail: 'remote (skipped)' };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const output = await rpc('case Cdrcisco.CTSFTPD.status() do {:error, :down} -> IO.puts("down"); _ -> IO.puts("ok") end', 10_000);
|
|
184
|
+
const value = output.trim();
|
|
185
|
+
if (value === 'ok')
|
|
186
|
+
return { ok: true, detail: 'running' };
|
|
187
|
+
if (value === 'down')
|
|
188
|
+
return { ok: false, detail: 'not running' };
|
|
189
|
+
return { ok: false, detail: value || 'unknown' };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
return { ok: false, detail: `unavailable (${err.message})` };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function registerStatus(sftp) {
|
|
196
|
+
const status = addApiOptions(sftp
|
|
197
|
+
.command('status')
|
|
198
|
+
.description('Show SFTP accounts, storage, monitoring, and daemon status')
|
|
199
|
+
.option('--skip-daemon', 'Skip the local SFTP daemon RPC check'), true);
|
|
200
|
+
status.action(action(async (opts) => {
|
|
201
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
202
|
+
const snapshot = await collectSftpStatus(api, spec);
|
|
203
|
+
const daemon = opts.skipDaemon
|
|
204
|
+
? { ok: true, detail: 'skipped' }
|
|
205
|
+
: await sftpDaemonStatus(api.baseUrl);
|
|
206
|
+
if (opts.json) {
|
|
207
|
+
printJson({
|
|
208
|
+
api: api.describe(),
|
|
209
|
+
spec: { openapi: snapshot.operations.specOpenApi, sftp_operations: snapshot.operations.total },
|
|
210
|
+
daemon,
|
|
211
|
+
...snapshot,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
out.header('SFTP Status');
|
|
216
|
+
out.ok(`API ${api.baseUrl} · org ${api.orgId}`);
|
|
217
|
+
out.ok(`OpenAPI ${snapshot.operations.specOpenApi ?? 'unknown'} · ${snapshot.operations.total} SFTP operations`);
|
|
218
|
+
if (daemon.detail.includes('skipped'))
|
|
219
|
+
out.info(`SFTP daemon check ${daemon.detail}`);
|
|
220
|
+
else if (daemon.ok)
|
|
221
|
+
out.ok(`SFTP daemon ${daemon.detail}`);
|
|
222
|
+
else
|
|
223
|
+
out.fail(`SFTP daemon ${daemon.detail}`);
|
|
224
|
+
out.info(`Accounts ${snapshot.accounts.total} total · ${snapshot.accounts.enabled} enabled · ` +
|
|
225
|
+
`${snapshot.accounts.disabled} disabled`);
|
|
226
|
+
if (snapshot.monitoring.available) {
|
|
227
|
+
const missing = snapshot.monitoring.missingUploads === null
|
|
228
|
+
? ''
|
|
229
|
+
: ` · ${snapshot.monitoring.missingUploads} missing upload`;
|
|
230
|
+
out.info(`Monitoring ${snapshot.monitoring.monitoredCount} enabled${missing}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
out.warn('Monitoring is not available on this appliance release');
|
|
234
|
+
}
|
|
235
|
+
if (snapshot.storage) {
|
|
236
|
+
out.info(`Storage ${formatBytes(snapshot.storage.total_used_bytes)} used · ` +
|
|
237
|
+
`${formatGb(snapshot.storage.total_limit_gb)} limit · ` +
|
|
238
|
+
`${snapshot.storage.accounts_over_quota ?? 0} account(s) over quota`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
out.warn('Storage status is not available on this appliance release');
|
|
242
|
+
}
|
|
243
|
+
console.log();
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
function registerDoctor(sftp) {
|
|
247
|
+
const doctor = addApiOptions(sftp
|
|
248
|
+
.command('doctor')
|
|
249
|
+
.description('Run SFTP connectivity and API compatibility checks'), true);
|
|
250
|
+
doctor.action(action(async (opts) => {
|
|
251
|
+
const checks = [];
|
|
252
|
+
let api;
|
|
253
|
+
let spec;
|
|
254
|
+
try {
|
|
255
|
+
api = await connect(opts);
|
|
256
|
+
spec = await api.fetchSpec();
|
|
257
|
+
checks.push({
|
|
258
|
+
name: 'OpenAPI spec',
|
|
259
|
+
ok: true,
|
|
260
|
+
detail: `${spec.openapi ?? 'unknown'} · ${listOperations(spec, { contains: 'sftp' }).length} SFTP operations`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
checks.push({
|
|
265
|
+
name: 'OpenAPI spec',
|
|
266
|
+
ok: false,
|
|
267
|
+
detail: err.message,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (api) {
|
|
271
|
+
try {
|
|
272
|
+
const accounts = await listAccounts(api);
|
|
273
|
+
checks.push({
|
|
274
|
+
name: 'API authentication',
|
|
275
|
+
ok: true,
|
|
276
|
+
detail: `${accounts.length} account(s) readable`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
checks.push({
|
|
281
|
+
name: 'API authentication',
|
|
282
|
+
ok: false,
|
|
283
|
+
detail: err.message,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (spec && !supportsOperation(spec, 'GET sftp_storage/status')) {
|
|
287
|
+
checks.push({
|
|
288
|
+
name: 'Storage endpoint',
|
|
289
|
+
ok: true,
|
|
290
|
+
detail: 'not available on this release (skipped)',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
try {
|
|
295
|
+
await api.get('sftp_storage/status');
|
|
296
|
+
checks.push({ name: 'Storage endpoint', ok: true, detail: 'reachable' });
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
checks.push({ name: 'Storage endpoint', ok: false, detail: err.message });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const daemon = await sftpDaemonStatus(api?.baseUrl ?? opts.url ?? DEFAULT_API_URL);
|
|
304
|
+
checks.push({ name: 'SFTP daemon', ok: daemon.ok, detail: daemon.detail });
|
|
305
|
+
const host = (() => {
|
|
306
|
+
try {
|
|
307
|
+
return new URL(api?.baseUrl ?? DEFAULT_API_URL).hostname;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return '127.0.0.1';
|
|
311
|
+
}
|
|
312
|
+
})();
|
|
313
|
+
const port = await checkTcpPort(host, 22, 3000);
|
|
314
|
+
checks.push({
|
|
315
|
+
name: `SFTP port 22 (${host})`,
|
|
316
|
+
ok: port.open,
|
|
317
|
+
detail: port.open ? `open in ${port.durationMs}ms` : port.error ?? 'closed',
|
|
318
|
+
});
|
|
319
|
+
if (opts.json) {
|
|
320
|
+
printJson({ api: api?.describe(), checks });
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
out.header('SFTP Doctor');
|
|
324
|
+
for (const check of checks) {
|
|
325
|
+
const [summary, ...rest] = check.detail.split('\n');
|
|
326
|
+
if (check.ok) {
|
|
327
|
+
out.ok(`${check.name}: ${summary}`);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
out.fail(`${check.name}: ${summary}`);
|
|
331
|
+
for (const line of rest) {
|
|
332
|
+
if (line.trim())
|
|
333
|
+
console.log(chalk.gray(` ${line.trim()}`));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
console.log();
|
|
338
|
+
}
|
|
339
|
+
if (checks.some(check => !check.ok))
|
|
340
|
+
process.exitCode = 1;
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
// ── Accounts ─────────────────────────────────────────────────────────────
|
|
344
|
+
function registerAccounts(sftp) {
|
|
345
|
+
const accounts = sftp
|
|
346
|
+
.command('accounts')
|
|
347
|
+
.alias('account')
|
|
348
|
+
.description('Manage SFTP Action Accounts');
|
|
349
|
+
accounts.action(() => accounts.outputHelp());
|
|
350
|
+
addApiOptions(accounts
|
|
351
|
+
.command('list')
|
|
352
|
+
.description('List SFTP accounts')
|
|
353
|
+
.option('--enabled', 'Only enabled accounts')
|
|
354
|
+
.option('--disabled', 'Only disabled accounts')
|
|
355
|
+
.option('--search <text>', 'Filter by username or description'), true).action(action(async (opts) => {
|
|
356
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
357
|
+
requireOperation(spec, 'GET sftp_action_accounts');
|
|
358
|
+
let list = await listAccounts(api);
|
|
359
|
+
if (opts.enabled)
|
|
360
|
+
list = list.filter(account => account.enabled);
|
|
361
|
+
if (opts.disabled)
|
|
362
|
+
list = list.filter(account => !account.enabled);
|
|
363
|
+
if (opts.search) {
|
|
364
|
+
const needle = opts.search.toLowerCase();
|
|
365
|
+
list = list.filter(account => account.username.toLowerCase().includes(needle) ||
|
|
366
|
+
(account.description ?? '').toLowerCase().includes(needle));
|
|
367
|
+
}
|
|
368
|
+
if (opts.json) {
|
|
369
|
+
printJson(list);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (list.length === 0) {
|
|
373
|
+
out.info('No SFTP accounts found.');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
console.log(renderTable(['USERNAME', 'STATUS', 'QUOTA', 'MONITOR', 'HOME', 'DESCRIPTION'], list.map(account => [
|
|
377
|
+
account.username,
|
|
378
|
+
account.enabled ? 'enabled' : 'disabled',
|
|
379
|
+
formatQuota(account.quota_used_bytes, account.quota_limit_gb),
|
|
380
|
+
account.upload_monitoring_enabled ? 'on' : 'off',
|
|
381
|
+
account.home ?? '—',
|
|
382
|
+
account.description ?? '—',
|
|
383
|
+
]), { maxWidth: [28, 8, 22, 7, 30, 40] }));
|
|
384
|
+
console.log();
|
|
385
|
+
out.info(`${list.length} account(s)`);
|
|
386
|
+
}));
|
|
387
|
+
addApiOptions(accounts
|
|
388
|
+
.command('show')
|
|
389
|
+
.description('Show a single SFTP account')
|
|
390
|
+
.argument('<id|username>', 'Account ID or username')
|
|
391
|
+
.option('--actions', 'Include full action configuration'), true).action(action(async (ref, opts) => {
|
|
392
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
393
|
+
requireOperation(spec, 'GET sftp_action_accounts/{id}');
|
|
394
|
+
const account = await resolveAccount(api, ref);
|
|
395
|
+
if (opts.json) {
|
|
396
|
+
printJson(account);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
out.header(`SFTP Account ${account.username}`);
|
|
400
|
+
printKeyValues([
|
|
401
|
+
['ID', String(account.id)],
|
|
402
|
+
['Status', account.enabled ? 'enabled' : 'disabled'],
|
|
403
|
+
['Home', account.home ?? '—'],
|
|
404
|
+
['Description', account.description ?? '—'],
|
|
405
|
+
['Quota', formatQuota(account.quota_used_bytes, account.quota_limit_gb)],
|
|
406
|
+
[
|
|
407
|
+
'Monitoring',
|
|
408
|
+
account.upload_monitoring_enabled
|
|
409
|
+
? `${account.expected_schedule ?? 'enabled'}${account.expected_next_upload_at ? ` · next ${formatDateTime(account.expected_next_upload_at)}` : ''}`
|
|
410
|
+
: 'disabled',
|
|
411
|
+
],
|
|
412
|
+
['Last upload', formatDateTime(account.last_upload_at)],
|
|
413
|
+
['On success', actionSummary(account.on_success_actions)],
|
|
414
|
+
['On failure', actionSummary(account.on_failure_actions)],
|
|
415
|
+
['Created', formatDateTime(account.inserted_at)],
|
|
416
|
+
['Updated', formatDateTime(account.updated_at)],
|
|
417
|
+
]);
|
|
418
|
+
if (opts.actions) {
|
|
419
|
+
console.log();
|
|
420
|
+
console.log(chalk.bold('On success actions:'));
|
|
421
|
+
console.log(JSON.stringify(account.on_success_actions ?? [], null, 2));
|
|
422
|
+
console.log(chalk.bold('On failure actions:'));
|
|
423
|
+
console.log(JSON.stringify(account.on_failure_actions ?? [], null, 2));
|
|
424
|
+
}
|
|
425
|
+
console.log();
|
|
426
|
+
}));
|
|
427
|
+
addApiOptions(accounts
|
|
428
|
+
.command('create')
|
|
429
|
+
.description('Create an SFTP account')
|
|
430
|
+
.requiredOption('--username <username>', 'SFTP username')
|
|
431
|
+
.option('--password <password>', 'Password (prefer --password-stdin)')
|
|
432
|
+
.option('--password-stdin', 'Read the password from stdin')
|
|
433
|
+
.option('--generate', 'Generate a random password')
|
|
434
|
+
.option('--description <text>', 'Description')
|
|
435
|
+
.option('--quota-gb <gb>', 'Per-account quota in GB, or "none" for the org default')
|
|
436
|
+
.option('--callmanager-id <id>', 'Link the account to a CallManager for DRF discovery')
|
|
437
|
+
.option('--disabled', 'Create the account disabled')
|
|
438
|
+
.option('--on-success <action>', 'Success action: type, JSON, or @file (repeatable)', collect, [])
|
|
439
|
+
.option('--on-failure <action>', 'Failure action: type, JSON, or @file (repeatable)', collect, [])
|
|
440
|
+
.option('--dry-run', 'Print the request without sending it'), true).action(action(async (opts) => {
|
|
441
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
442
|
+
requireOperation(spec, 'POST sftp_action_accounts');
|
|
443
|
+
const password = await resolveNewPassword({
|
|
444
|
+
password: opts.password,
|
|
445
|
+
passwordStdin: opts.passwordStdin,
|
|
446
|
+
generate: opts.generate,
|
|
447
|
+
});
|
|
448
|
+
const { account, generatedPassword } = buildCreateAccountPayload({
|
|
449
|
+
username: opts.username,
|
|
450
|
+
password,
|
|
451
|
+
generatePassword: !password,
|
|
452
|
+
description: opts.description,
|
|
453
|
+
enabled: opts.disabled ? false : undefined,
|
|
454
|
+
quotaGb: opts.quotaGb,
|
|
455
|
+
callmanagerId: opts.callmanagerId,
|
|
456
|
+
onSuccess: opts.onSuccess,
|
|
457
|
+
onFailure: opts.onFailure,
|
|
458
|
+
});
|
|
459
|
+
if (opts.dryRun) {
|
|
460
|
+
console.log(`${chalk.bold('POST')} ${api.apiPath('sftp_action_accounts')}`);
|
|
461
|
+
printJson({ account: redactAccount(account) });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const response = await api.post('sftp_action_accounts', { account });
|
|
465
|
+
const created = (response.data.account ?? response.data);
|
|
466
|
+
if (opts.json) {
|
|
467
|
+
printJson({ account: created, generated_password: generatedPassword });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
out.successBox(`Created SFTP account ${created.username ?? opts.username}`);
|
|
471
|
+
printKeyValues([
|
|
472
|
+
['ID', String(created.id ?? '—')],
|
|
473
|
+
['Home', String(created.home ?? '—')],
|
|
474
|
+
['Status', created.enabled === false ? 'disabled' : 'enabled'],
|
|
475
|
+
]);
|
|
476
|
+
printGeneratedPassword(generatedPassword);
|
|
477
|
+
console.log();
|
|
478
|
+
}));
|
|
479
|
+
addApiOptions(accounts
|
|
480
|
+
.command('update')
|
|
481
|
+
.description('Update an SFTP account (including password and actions)')
|
|
482
|
+
.argument('<id|username>', 'Account ID or username')
|
|
483
|
+
.option('--description <text>', 'Description')
|
|
484
|
+
.option('--quota-gb <gb>', 'Per-account quota in GB, or "none" for the org default')
|
|
485
|
+
.option('--callmanager-id <id>', 'Link the account to a CallManager for DRF discovery')
|
|
486
|
+
.option('--enable', 'Enable the account')
|
|
487
|
+
.option('--disable', 'Disable the account')
|
|
488
|
+
.option('--password <password>', 'New password (prefer --password-stdin)')
|
|
489
|
+
.option('--password-stdin', 'Read the new password from stdin')
|
|
490
|
+
.option('--generate', 'Generate a new random password')
|
|
491
|
+
.option('--on-success <action>', 'Success action: type, JSON, or @file (repeatable)', collect, [])
|
|
492
|
+
.option('--on-failure <action>', 'Failure action: type, JSON, or @file (repeatable)', collect, [])
|
|
493
|
+
.option('--clear-success-actions', 'Remove all success actions')
|
|
494
|
+
.option('--clear-failure-actions', 'Remove all failure actions')
|
|
495
|
+
.option('--dry-run', 'Print the request without sending it'), true).action(action(async (ref, opts) => {
|
|
496
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
497
|
+
requireOperation(spec, 'PUT sftp_action_accounts/{id}');
|
|
498
|
+
const account = await resolveAccount(api, ref);
|
|
499
|
+
let password = opts.password;
|
|
500
|
+
if (!password && opts.passwordStdin)
|
|
501
|
+
password = readPasswordFromStdin();
|
|
502
|
+
const { account: payload, generatedPassword } = buildUpdateAccountPayload({
|
|
503
|
+
description: opts.description,
|
|
504
|
+
quotaGb: opts.quotaGb,
|
|
505
|
+
callmanagerId: opts.callmanagerId,
|
|
506
|
+
enable: opts.enable,
|
|
507
|
+
disable: opts.disable,
|
|
508
|
+
password,
|
|
509
|
+
generatePassword: opts.generate,
|
|
510
|
+
onSuccess: opts.onSuccess,
|
|
511
|
+
onFailure: opts.onFailure,
|
|
512
|
+
clearSuccess: opts.clearSuccessActions,
|
|
513
|
+
clearFailure: opts.clearFailureActions,
|
|
514
|
+
});
|
|
515
|
+
if (opts.dryRun) {
|
|
516
|
+
console.log(`${chalk.bold('PUT')} ${api.apiPath(`sftp_action_accounts/${account.id}`)}`);
|
|
517
|
+
printJson({ account: redactAccount(payload) });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const response = await api.put(`sftp_action_accounts/${account.id}`, { account: payload });
|
|
521
|
+
const updated = (response.data.account ?? response.data);
|
|
522
|
+
if (opts.json) {
|
|
523
|
+
printJson({ account: updated, generated_password: generatedPassword });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
out.successBox(`Updated SFTP account ${updated.username ?? account.username}`);
|
|
527
|
+
printKeyValues([
|
|
528
|
+
['Fields', Object.keys(payload).join(', ')],
|
|
529
|
+
['Status', updated.enabled === false ? 'disabled' : 'enabled'],
|
|
530
|
+
]);
|
|
531
|
+
printGeneratedPassword(generatedPassword);
|
|
532
|
+
console.log();
|
|
533
|
+
}));
|
|
534
|
+
addApiOptions(accounts
|
|
535
|
+
.command('delete')
|
|
536
|
+
.alias('rm')
|
|
537
|
+
.description('Delete an SFTP account')
|
|
538
|
+
.argument('<id|username>', 'Account ID or username')
|
|
539
|
+
.option('--yes', 'Skip the confirmation prompt'), true).action(action(async (ref, opts) => {
|
|
540
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
541
|
+
requireOperation(spec, 'DELETE sftp_action_accounts/{id}');
|
|
542
|
+
const account = await resolveAccount(api, ref);
|
|
543
|
+
await confirmDestructive(`Delete SFTP account ${accountLabel(account)}?`, opts.yes);
|
|
544
|
+
await api.delete(`sftp_action_accounts/${account.id}`);
|
|
545
|
+
if (opts.json)
|
|
546
|
+
printJson({ deleted: true, id: account.id, username: account.username });
|
|
547
|
+
else
|
|
548
|
+
out.successBox(`Deleted SFTP account ${account.username}`);
|
|
549
|
+
}));
|
|
550
|
+
for (const [name, enabled] of [
|
|
551
|
+
['enable', true],
|
|
552
|
+
['disable', false],
|
|
553
|
+
]) {
|
|
554
|
+
addApiOptions(accounts
|
|
555
|
+
.command(name)
|
|
556
|
+
.description(`${enabled ? 'Enable' : 'Disable'} an SFTP account`)
|
|
557
|
+
.argument('<id|username>', 'Account ID or username'), true).action(action(async (ref, opts) => {
|
|
558
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
559
|
+
requireOperation(spec, 'PUT sftp_action_accounts/{id}');
|
|
560
|
+
const account = await resolveAccount(api, ref);
|
|
561
|
+
const response = await api.put(`sftp_action_accounts/${account.id}`, {
|
|
562
|
+
account: { enabled },
|
|
563
|
+
});
|
|
564
|
+
const updated = (response.data.account ?? response.data);
|
|
565
|
+
if (opts.json)
|
|
566
|
+
printJson(updated);
|
|
567
|
+
else
|
|
568
|
+
out.ok(`${account.username} ${enabled ? 'enabled' : 'disabled'}`);
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
addApiOptions(accounts
|
|
572
|
+
.command('password')
|
|
573
|
+
.description('Set or generate an SFTP account password')
|
|
574
|
+
.argument('<id|username>', 'Account ID or username')
|
|
575
|
+
.option('--password <password>', 'New password (prefer --password-stdin)')
|
|
576
|
+
.option('--password-stdin', 'Read the new password from stdin')
|
|
577
|
+
.option('--generate', 'Generate a random password'), true).action(action(async (ref, opts) => {
|
|
578
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
579
|
+
requireOperation(spec, 'PUT sftp_action_accounts/{id}');
|
|
580
|
+
const account = await resolveAccount(api, ref);
|
|
581
|
+
let password = opts.password;
|
|
582
|
+
if (!password && opts.passwordStdin)
|
|
583
|
+
password = readPasswordFromStdin();
|
|
584
|
+
if (!password && !opts.generate)
|
|
585
|
+
password = await promptHidden('New SFTP password');
|
|
586
|
+
let generatedPassword;
|
|
587
|
+
if (!password) {
|
|
588
|
+
generatedPassword = buildCreateAccountPayload({
|
|
589
|
+
username: account.username,
|
|
590
|
+
onSuccess: [],
|
|
591
|
+
onFailure: [],
|
|
592
|
+
}).account.password;
|
|
593
|
+
password = generatedPassword;
|
|
594
|
+
}
|
|
595
|
+
await api.put(`sftp_action_accounts/${account.id}`, { account: { password } });
|
|
596
|
+
if (opts.json) {
|
|
597
|
+
printJson({ id: account.id, username: account.username, generated_password: generatedPassword });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
out.ok(`Password updated for ${account.username}`);
|
|
601
|
+
printGeneratedPassword(generatedPassword);
|
|
602
|
+
}));
|
|
603
|
+
addApiOptions(accounts
|
|
604
|
+
.command('quota')
|
|
605
|
+
.description('Show (and optionally refresh) an account storage quota')
|
|
606
|
+
.argument('<id|username>', 'Account ID or username')
|
|
607
|
+
.option('--refresh', 'Recalculate the quota before showing it'), true).action(action(async (ref, opts) => {
|
|
608
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
609
|
+
requireOperation(spec, 'GET sftp_action_accounts/{id}/quota');
|
|
610
|
+
const account = await resolveAccount(api, ref);
|
|
611
|
+
if (opts.refresh) {
|
|
612
|
+
requireOperation(spec, 'POST sftp_action_accounts/{id}/refresh_quota');
|
|
613
|
+
await api.post(`sftp_action_accounts/${account.id}/refresh_quota`, {});
|
|
614
|
+
}
|
|
615
|
+
const quota = await getAccountQuota(api, account);
|
|
616
|
+
if (opts.json) {
|
|
617
|
+
printJson({ account: account.username, ...quota });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
out.header(`Quota · ${account.username}`);
|
|
621
|
+
printKeyValues([
|
|
622
|
+
['Used', formatBytes(quota.quota_used_bytes)],
|
|
623
|
+
['Limit', formatGb(quota.quota_limit_gb)],
|
|
624
|
+
['Usage', formatPercent(quota.quota_percentage)],
|
|
625
|
+
['Over quota', quota.over_quota ? 'yes' : 'no'],
|
|
626
|
+
['Calculated', formatDateTime(quota.last_calculated_at)],
|
|
627
|
+
]);
|
|
628
|
+
console.log();
|
|
629
|
+
}));
|
|
630
|
+
addApiOptions(accounts
|
|
631
|
+
.command('files')
|
|
632
|
+
.description('List files in an account home directory')
|
|
633
|
+
.argument('<id|username>', 'Account ID or username')
|
|
634
|
+
.option('--path <path>', 'Directory path')
|
|
635
|
+
.option('--limit <n>', 'Maximum entries', '200'), true).action(action(async (ref, opts) => {
|
|
636
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
637
|
+
requireOperation(spec, 'GET sftp_action_accounts/{account_id}/files');
|
|
638
|
+
const account = await resolveAccount(api, ref);
|
|
639
|
+
const result = await getAccountFiles(api, account, opts.path, parseIntegerOption(opts.limit, '--limit', { min: 1, max: 2000, fallback: 200 }));
|
|
640
|
+
if (opts.json) {
|
|
641
|
+
printJson({ account: account.username, ...result });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (result.entries.length === 0) {
|
|
645
|
+
out.info(`No files in ${result.path ?? account.home ?? `account ${account.username}`}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
console.log(renderTable(['TYPE', 'SIZE', 'MODIFIED', 'NAME'], result.entries.map(entry => [
|
|
649
|
+
entry.type === 'directory' ? 'dir' : 'file',
|
|
650
|
+
entry.type === 'directory' ? '—' : formatBytes(entry.size),
|
|
651
|
+
formatDateTime(entry.modified_at),
|
|
652
|
+
entry.name,
|
|
653
|
+
]), { maxWidth: [5, 12, 20, 80] }));
|
|
654
|
+
console.log();
|
|
655
|
+
out.info(`${result.total ?? result.entries.length} entr${(result.total ?? result.entries.length) === 1 ? 'y' : 'ies'}`);
|
|
656
|
+
}));
|
|
657
|
+
addApiOptions(accounts
|
|
658
|
+
.command('download')
|
|
659
|
+
.description('Download a file from an account home directory')
|
|
660
|
+
.argument('<id|username>', 'Account ID or username')
|
|
661
|
+
.argument('<remote-path>', 'Path relative to the account home')
|
|
662
|
+
.option('--output <path>', 'Local destination path'), true).action(action(async (ref, remotePath, opts) => {
|
|
663
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
664
|
+
requireOperation(spec, 'GET sftp_action_accounts/{account_id}/files/download');
|
|
665
|
+
const account = await resolveAccount(api, ref);
|
|
666
|
+
const outputPath = opts.output ?? basename(remotePath);
|
|
667
|
+
await api.get(`sftp_action_accounts/${account.id}/files/download`, {
|
|
668
|
+
query: { path: remotePath },
|
|
669
|
+
accept: 'application/octet-stream',
|
|
670
|
+
outputPath,
|
|
671
|
+
parse: 'none',
|
|
672
|
+
});
|
|
673
|
+
const size = statSync(outputPath).size;
|
|
674
|
+
if (opts.json)
|
|
675
|
+
printJson({ account: account.username, remote_path: remotePath, output: outputPath, size });
|
|
676
|
+
else
|
|
677
|
+
out.ok(`Downloaded ${remotePath} → ${outputPath} (${formatBytes(size)})`);
|
|
678
|
+
}));
|
|
679
|
+
addApiOptions(accounts
|
|
680
|
+
.command('runs')
|
|
681
|
+
.description('List action runs for an account')
|
|
682
|
+
.argument('<id|username>', 'Account ID or username')
|
|
683
|
+
.option('--limit <n>', 'Maximum runs', '50')
|
|
684
|
+
.option('--offset <n>', 'Offset', '0'), true).action(action(async (ref, opts) => {
|
|
685
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
686
|
+
requireOperation(spec, 'GET sftp_action_accounts/{id}/runs');
|
|
687
|
+
const account = await resolveAccount(api, ref);
|
|
688
|
+
const runs = await getAccountRuns(api, account, parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000, fallback: 50 }), parseIntegerOption(opts.offset, '--offset', { min: 0, fallback: 0 }));
|
|
689
|
+
if (opts.json) {
|
|
690
|
+
printJson(runs);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (runs.length === 0) {
|
|
694
|
+
out.info(`No runs for ${account.username}.`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
console.log(renderTable(['ID', 'STATUS', 'TYPE', 'EVENT', 'FILE', 'WHEN'], runs.map(run => [
|
|
698
|
+
String(run.id),
|
|
699
|
+
run.status ?? '—',
|
|
700
|
+
run.type ?? '—',
|
|
701
|
+
run.event ?? '—',
|
|
702
|
+
run.file ?? '—',
|
|
703
|
+
formatDateTime(run.inserted_at),
|
|
704
|
+
]), { maxWidth: [8, 10, 22, 24, 50, 20] }));
|
|
705
|
+
console.log();
|
|
706
|
+
}));
|
|
707
|
+
addApiOptions(accounts
|
|
708
|
+
.command('activity')
|
|
709
|
+
.description('List audit activity for an account')
|
|
710
|
+
.argument('<id|username>', 'Account ID or username')
|
|
711
|
+
.option('--limit <n>', 'Maximum entries', '100')
|
|
712
|
+
.option('--offset <n>', 'Offset', '0'), true).action(action(async (ref, opts) => {
|
|
713
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
714
|
+
requireOperation(spec, 'GET sftp_action_accounts/{id}/activity');
|
|
715
|
+
const account = await resolveAccount(api, ref);
|
|
716
|
+
const activity = await getAccountActivity(api, account, parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000, fallback: 100 }), parseIntegerOption(opts.offset, '--offset', { min: 0, fallback: 0 }));
|
|
717
|
+
if (opts.json) {
|
|
718
|
+
printJson(activity);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
printActivityTable(activity);
|
|
722
|
+
}));
|
|
723
|
+
addApiOptions(accounts
|
|
724
|
+
.command('catalog')
|
|
725
|
+
.description('Show action types supported by the appliance'), true).action(action(async (opts) => {
|
|
726
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
727
|
+
requireOperation(spec, 'GET sftp_action_accounts/catalog');
|
|
728
|
+
const response = await api.get('sftp_action_accounts/catalog');
|
|
729
|
+
const catalog = (response.data.catalog ?? response.data);
|
|
730
|
+
if (opts.json) {
|
|
731
|
+
printJson(catalog);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const types = catalog.types ?? [];
|
|
735
|
+
if (types.length === 0) {
|
|
736
|
+
out.warn('The appliance returned an empty action catalog.');
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
console.log(renderTable(['TYPE', 'REQUIRED PARAMS', 'OPTIONAL PARAMS'], types.map(type => [
|
|
740
|
+
type.type ?? '—',
|
|
741
|
+
(type.required_params ?? []).join(', ') || '—',
|
|
742
|
+
(type.optional_params ?? []).join(', ') || '—',
|
|
743
|
+
]), { maxWidth: [24, 40, 60] }));
|
|
744
|
+
console.log();
|
|
745
|
+
}));
|
|
746
|
+
const contacts = accounts
|
|
747
|
+
.command('contacts')
|
|
748
|
+
.description('Manage notification contacts on an SFTP account');
|
|
749
|
+
contacts.action(() => contacts.outputHelp());
|
|
750
|
+
addApiOptions(contacts
|
|
751
|
+
.command('list')
|
|
752
|
+
.description('List notification contacts')
|
|
753
|
+
.argument('<id|username>', 'Account ID or username'), true).action(action(async (ref, opts) => {
|
|
754
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
755
|
+
requireOperation(spec, 'GET sftp_action_accounts/{id}/contacts');
|
|
756
|
+
const account = await resolveAccount(api, ref);
|
|
757
|
+
const list = await getAccountContacts(api, account);
|
|
758
|
+
if (opts.json) {
|
|
759
|
+
printJson(list);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (list.length === 0) {
|
|
763
|
+
out.info('No notification contacts.');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
console.log(renderTable(['ID', 'TYPE', 'CONTACT', 'DESCRIPTION'], list.map(contact => [
|
|
767
|
+
String(contact.id),
|
|
768
|
+
String(contact.type ?? contact.contact_type ?? '—'),
|
|
769
|
+
String(contact.contact_uri ?? contact.email ?? '—'),
|
|
770
|
+
String(contact.description ?? '—'),
|
|
771
|
+
]), { maxWidth: [8, 16, 50, 40] }));
|
|
772
|
+
console.log();
|
|
773
|
+
}));
|
|
774
|
+
addApiOptions(contacts
|
|
775
|
+
.command('add')
|
|
776
|
+
.description('Add notification contacts')
|
|
777
|
+
.argument('<id|username>', 'Account ID or username')
|
|
778
|
+
.option('--contact <id>', 'Contact ID (repeatable, comma-separated)', collect, []), true).action(action(async (ref, opts) => {
|
|
779
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
780
|
+
requireOperation(spec, 'POST sftp_action_accounts/{id}/contacts');
|
|
781
|
+
const account = await resolveAccount(api, ref);
|
|
782
|
+
const ids = parseContactIds(opts.contact);
|
|
783
|
+
await addAccountContacts(api, account, ids);
|
|
784
|
+
if (opts.json)
|
|
785
|
+
printJson({ account: account.username, added: ids });
|
|
786
|
+
else
|
|
787
|
+
out.ok(`Added ${ids.length} contact(s) to ${account.username}`);
|
|
788
|
+
}));
|
|
789
|
+
addApiOptions(contacts
|
|
790
|
+
.command('remove')
|
|
791
|
+
.description('Remove a notification contact')
|
|
792
|
+
.argument('<id|username>', 'Account ID or username')
|
|
793
|
+
.argument('<contact-id>', 'Contact ID'), true).action(action(async (ref, contactId, opts) => {
|
|
794
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
795
|
+
requireOperation(spec, 'DELETE sftp_action_accounts/{id}/contacts/{contact_id}');
|
|
796
|
+
const account = await resolveAccount(api, ref);
|
|
797
|
+
const id = parseContactIds([contactId])[0];
|
|
798
|
+
await removeAccountContact(api, account, id);
|
|
799
|
+
if (opts.json)
|
|
800
|
+
printJson({ account: account.username, removed: id });
|
|
801
|
+
else
|
|
802
|
+
out.ok(`Removed contact ${id} from ${account.username}`);
|
|
803
|
+
}));
|
|
804
|
+
addApiOptions(accounts
|
|
805
|
+
.command('export')
|
|
806
|
+
.description('Export accounts to CSV')
|
|
807
|
+
.option('--output <path>', 'Destination file, or - for stdout')
|
|
808
|
+
.option('--include-actions', 'Include action configuration in the CSV')).action(action(async (opts) => {
|
|
809
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
810
|
+
requireOperation(spec, 'GET sftp_action_accounts/export');
|
|
811
|
+
const output = opts.output ?? `sftp-accounts-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
812
|
+
if (output === '-') {
|
|
813
|
+
const response = await api.get('sftp_action_accounts/export', {
|
|
814
|
+
query: { include_actions: opts.includeActions ? 'true' : undefined },
|
|
815
|
+
accept: 'text/csv',
|
|
816
|
+
parse: 'text',
|
|
817
|
+
});
|
|
818
|
+
process.stdout.write(response.data);
|
|
819
|
+
if (!response.data.endsWith('\n'))
|
|
820
|
+
process.stdout.write('\n');
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
await api.get('sftp_action_accounts/export', {
|
|
824
|
+
query: { include_actions: opts.includeActions ? 'true' : undefined },
|
|
825
|
+
accept: 'text/csv',
|
|
826
|
+
outputPath: output,
|
|
827
|
+
parse: 'none',
|
|
828
|
+
});
|
|
829
|
+
out.ok(`Exported accounts to ${output}`);
|
|
830
|
+
}));
|
|
831
|
+
addApiOptions(accounts
|
|
832
|
+
.command('template')
|
|
833
|
+
.description('Download the CSV import template')
|
|
834
|
+
.option('--output <path>', 'Destination file, or - for stdout')).action(action(async (opts) => {
|
|
835
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
836
|
+
requireOperation(spec, 'GET sftp_action_accounts/template');
|
|
837
|
+
const output = opts.output ?? 'sftp-accounts-template.csv';
|
|
838
|
+
if (output === '-') {
|
|
839
|
+
const response = await api.get('sftp_action_accounts/template', {
|
|
840
|
+
accept: 'text/csv',
|
|
841
|
+
parse: 'text',
|
|
842
|
+
});
|
|
843
|
+
process.stdout.write(response.data);
|
|
844
|
+
if (!response.data.endsWith('\n'))
|
|
845
|
+
process.stdout.write('\n');
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
await api.get('sftp_action_accounts/template', {
|
|
849
|
+
accept: 'text/csv',
|
|
850
|
+
outputPath: output,
|
|
851
|
+
parse: 'none',
|
|
852
|
+
});
|
|
853
|
+
out.ok(`Wrote CSV template to ${output}`);
|
|
854
|
+
}));
|
|
855
|
+
addApiOptions(accounts
|
|
856
|
+
.command('import')
|
|
857
|
+
.description('Import accounts from a CSV file')
|
|
858
|
+
.argument('<file>', 'CSV file')
|
|
859
|
+
.option('--validate-only', 'Validate the CSV without importing')
|
|
860
|
+
.option('--skip-existing', 'Skip usernames that already exist')
|
|
861
|
+
.option('--on-success <action>', 'Default success action for imported accounts (repeatable)', collect, [])
|
|
862
|
+
.option('--on-failure <action>', 'Default failure action for imported accounts (repeatable)', collect, [])
|
|
863
|
+
.option('--dry-run', 'Print the request without sending it'), true).action(action(async (file, opts) => {
|
|
864
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
865
|
+
const operation = opts.validateOnly
|
|
866
|
+
? requireOperation(spec, 'POST sftp_action_accounts/import/validate')
|
|
867
|
+
: requireOperation(spec, 'POST sftp_action_accounts/import');
|
|
868
|
+
const upload = readImportFile(file);
|
|
869
|
+
const files = [{ ...upload, field: 'file' }];
|
|
870
|
+
if (opts.dryRun) {
|
|
871
|
+
console.log(`${chalk.bold('POST')} ${api.apiPath(operation.shortPath)}`);
|
|
872
|
+
printJson({
|
|
873
|
+
file: upload.filename,
|
|
874
|
+
bytes: upload.data.length,
|
|
875
|
+
skip_existing: opts.skipExisting ? 'true' : undefined,
|
|
876
|
+
on_success_actions: opts.onSuccess.length > 0 ? 'json' : undefined,
|
|
877
|
+
on_failure_actions: opts.onFailure.length > 0 ? 'json' : undefined,
|
|
878
|
+
});
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const fields = {};
|
|
882
|
+
if (!opts.validateOnly) {
|
|
883
|
+
fields.skip_existing = opts.skipExisting ? 'true' : undefined;
|
|
884
|
+
if (opts.onSuccess.length > 0) {
|
|
885
|
+
fields.on_success_actions = JSON.stringify(buildCreateAccountPayload({
|
|
886
|
+
username: 'import',
|
|
887
|
+
password: 'placeholder',
|
|
888
|
+
onSuccess: opts.onSuccess,
|
|
889
|
+
onFailure: [],
|
|
890
|
+
}).account.on_success_actions ?? []);
|
|
891
|
+
}
|
|
892
|
+
if (opts.onFailure.length > 0) {
|
|
893
|
+
fields.on_failure_actions = JSON.stringify(buildCreateAccountPayload({
|
|
894
|
+
username: 'import',
|
|
895
|
+
password: 'placeholder',
|
|
896
|
+
onSuccess: [],
|
|
897
|
+
onFailure: opts.onFailure,
|
|
898
|
+
}).account.on_failure_actions ?? []);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const response = await api.request('POST', operation.shortPath, {
|
|
902
|
+
form: { fields, files },
|
|
903
|
+
});
|
|
904
|
+
if (opts.json) {
|
|
905
|
+
printJson(response.data);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (opts.validateOnly) {
|
|
909
|
+
const analysis = (response.data.analysis ?? response.data);
|
|
910
|
+
if (analysis.valid === false) {
|
|
911
|
+
out.fail('CSV validation failed');
|
|
912
|
+
for (const error of analysis.errors ?? [])
|
|
913
|
+
console.log(` ${error}`);
|
|
914
|
+
process.exitCode = 1;
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
out.ok(`CSV valid · ${analysis.row_count ?? '?'} row(s)`);
|
|
918
|
+
for (const warning of analysis.warnings ?? [])
|
|
919
|
+
out.warn(warning);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const imported = (response.data.accounts ?? []);
|
|
923
|
+
out.ok(`Imported ${imported.length} account(s)`);
|
|
924
|
+
if (imported.length > 0) {
|
|
925
|
+
console.log(renderTable(['ID', 'USERNAME', 'STATUS', 'HOME'], imported.map(account => [
|
|
926
|
+
String(account.id),
|
|
927
|
+
account.username,
|
|
928
|
+
account.enabled ? 'enabled' : 'disabled',
|
|
929
|
+
account.home ?? '—',
|
|
930
|
+
])));
|
|
931
|
+
}
|
|
932
|
+
}));
|
|
933
|
+
}
|
|
934
|
+
// ── Monitoring ───────────────────────────────────────────────────────────
|
|
935
|
+
function registerMonitoring(sftp) {
|
|
936
|
+
const monitoring = sftp
|
|
937
|
+
.command('monitoring')
|
|
938
|
+
.alias('monitor')
|
|
939
|
+
.description('Upload monitoring and missed-upload alerts');
|
|
940
|
+
monitoring.action(() => monitoring.outputHelp());
|
|
941
|
+
addApiOptions(monitoring.command('list').description('List monitored SFTP accounts'), true).action(action(async (opts) => {
|
|
942
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
943
|
+
requireOperation(spec, 'GET sftp_monitoring');
|
|
944
|
+
const list = await listMonitoredAccounts(api);
|
|
945
|
+
if (opts.json) {
|
|
946
|
+
printJson(list);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
printMonitoringTable(list);
|
|
950
|
+
}));
|
|
951
|
+
addApiOptions(monitoring.command('missing').description('List accounts with missing uploads'), true).action(action(async (opts) => {
|
|
952
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
953
|
+
requireOperation(spec, 'GET sftp_monitoring/missing');
|
|
954
|
+
const response = await api.get('sftp_monitoring/missing');
|
|
955
|
+
const list = (response.data.data ?? response.data.accounts ?? []);
|
|
956
|
+
if (opts.json) {
|
|
957
|
+
printJson(response.data);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
961
|
+
out.ok('No missing uploads.');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
printMonitoringTable(list);
|
|
965
|
+
}));
|
|
966
|
+
addApiOptions(monitoring
|
|
967
|
+
.command('check')
|
|
968
|
+
.description('Trigger an immediate missed-upload check'), true).action(action(async (opts) => {
|
|
969
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
970
|
+
requireOperation(spec, 'POST sftp_monitoring/check');
|
|
971
|
+
const response = await api.post('sftp_monitoring/check', {});
|
|
972
|
+
if (opts.json)
|
|
973
|
+
printJson(response.data);
|
|
974
|
+
else
|
|
975
|
+
out.ok(String(response.data.message ?? 'Monitoring check completed'));
|
|
976
|
+
}));
|
|
977
|
+
addApiOptions(monitoring
|
|
978
|
+
.command('show')
|
|
979
|
+
.description('Show monitoring configuration for an account')
|
|
980
|
+
.argument('<id|username>', 'Account ID or username'), true).action(action(async (ref, opts) => {
|
|
981
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
982
|
+
requireOperation(spec, 'GET sftp_action_accounts/{account_id}/monitoring');
|
|
983
|
+
const account = await resolveAccount(api, ref);
|
|
984
|
+
const config = await getAccountMonitoring(api, account);
|
|
985
|
+
if (opts.json) {
|
|
986
|
+
printJson(config);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
out.header(`Monitoring · ${account.username}`);
|
|
990
|
+
printKeyValues([
|
|
991
|
+
['Enabled', config.enabled ? 'yes' : 'no'],
|
|
992
|
+
['Schedule', monitoringSchedule(config)],
|
|
993
|
+
['Next upload', formatDateTime(config.expected_next_upload_at)],
|
|
994
|
+
['Last upload', formatDateTime(config.last_upload_at)],
|
|
995
|
+
['Missed uploads', String(config.missed_upload_count ?? 0)],
|
|
996
|
+
['Alerts', config.config?.alert_on_missed_uploads ? 'on' : 'off'],
|
|
997
|
+
['Tolerance', `${config.config?.tolerance_minutes ?? '—'} min`],
|
|
998
|
+
['Threshold', String(config.config?.alert_threshold ?? '—')],
|
|
999
|
+
]);
|
|
1000
|
+
console.log();
|
|
1001
|
+
}));
|
|
1002
|
+
const addMonitoringOptions = (command) => command
|
|
1003
|
+
.option('--schedule <type>', 'Schedule: hourly, daily, weekly, monthly')
|
|
1004
|
+
.option('--hour <n>', 'Expected hour (0-23)')
|
|
1005
|
+
.option('--minute <n>', 'Expected minute (0-59)')
|
|
1006
|
+
.option('--day-of-week <n>', 'Expected day of week (1=Mon … 7=Sun)')
|
|
1007
|
+
.option('--day-of-month <n>', 'Expected day of month (1-28)')
|
|
1008
|
+
.option('--weekdays-only', 'Only expect uploads on weekdays')
|
|
1009
|
+
.option('--tolerance <minutes>', 'Minutes of tolerance')
|
|
1010
|
+
.option('--threshold <n>', 'Missed-upload alert threshold')
|
|
1011
|
+
.option('--alert-config <json>', 'Alert config JSON object or @file')
|
|
1012
|
+
.option('--no-alert-on-missed', 'Disable missed-upload alerts');
|
|
1013
|
+
addApiOptions(addMonitoringOptions(monitoring
|
|
1014
|
+
.command('enable')
|
|
1015
|
+
.description('Enable upload monitoring for an account')
|
|
1016
|
+
.argument('<id|username>', 'Account ID or username')), true).action(action(async (ref, opts) => {
|
|
1017
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1018
|
+
requireOperation(spec, 'POST sftp_action_accounts/{account_id}/monitoring/enable');
|
|
1019
|
+
const account = await resolveAccount(api, ref);
|
|
1020
|
+
const payload = buildMonitoringPayload(opts, true);
|
|
1021
|
+
const response = await api.post(`sftp_action_accounts/${account.id}/monitoring/enable`, payload);
|
|
1022
|
+
if (opts.json)
|
|
1023
|
+
printJson(response.data);
|
|
1024
|
+
else
|
|
1025
|
+
out.ok(`Enabled upload monitoring for ${account.username}`);
|
|
1026
|
+
}));
|
|
1027
|
+
addApiOptions(monitoring
|
|
1028
|
+
.command('disable')
|
|
1029
|
+
.description('Disable upload monitoring for an account')
|
|
1030
|
+
.argument('<id|username>', 'Account ID or username'), true).action(action(async (ref, opts) => {
|
|
1031
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1032
|
+
requireOperation(spec, 'POST sftp_action_accounts/{account_id}/monitoring/disable');
|
|
1033
|
+
const account = await resolveAccount(api, ref);
|
|
1034
|
+
const response = await api.post(`sftp_action_accounts/${account.id}/monitoring/disable`, {});
|
|
1035
|
+
if (opts.json)
|
|
1036
|
+
printJson(response.data);
|
|
1037
|
+
else
|
|
1038
|
+
out.ok(`Disabled upload monitoring for ${account.username}`);
|
|
1039
|
+
}));
|
|
1040
|
+
addApiOptions(addMonitoringOptions(monitoring
|
|
1041
|
+
.command('config')
|
|
1042
|
+
.description('Update monitoring configuration for an account')
|
|
1043
|
+
.argument('<id|username>', 'Account ID or username')), true).action(action(async (ref, opts) => {
|
|
1044
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1045
|
+
requireOperation(spec, 'PUT sftp_action_accounts/{account_id}/monitoring/config');
|
|
1046
|
+
const account = await resolveAccount(api, ref);
|
|
1047
|
+
const payload = buildMonitoringPayload(opts, false);
|
|
1048
|
+
if (Object.keys(payload).length === 0) {
|
|
1049
|
+
throw new ApplianceApiError('Nothing to update. Pass a schedule or alert option.');
|
|
1050
|
+
}
|
|
1051
|
+
const response = await api.put(`sftp_action_accounts/${account.id}/monitoring/config`, payload);
|
|
1052
|
+
if (opts.json)
|
|
1053
|
+
printJson(response.data);
|
|
1054
|
+
else
|
|
1055
|
+
out.ok(`Updated monitoring configuration for ${account.username}`);
|
|
1056
|
+
}));
|
|
1057
|
+
addApiOptions(monitoring
|
|
1058
|
+
.command('discover')
|
|
1059
|
+
.description('Discover CUCM DRF backup schedules and apply them to an account')
|
|
1060
|
+
.argument('<id|username>', 'Account ID or username')
|
|
1061
|
+
.option('--host <host>', 'CUCM host')
|
|
1062
|
+
.option('--username <username>', 'CUCM username')
|
|
1063
|
+
.option('--password <password>', 'CUCM password (prefer --password-stdin)')
|
|
1064
|
+
.option('--password-stdin', 'Read the CUCM password from stdin')
|
|
1065
|
+
.option('--schedule-name <name>', 'Schedule name filter')
|
|
1066
|
+
.option('--callmanager-id <id>', 'Existing CallManager ID')
|
|
1067
|
+
.option('--tolerance <minutes>', 'Minutes of tolerance'), true).action(action(async (ref, opts) => {
|
|
1068
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1069
|
+
requireOperation(spec, 'POST sftp_action_accounts/{account_id}/monitoring/discover');
|
|
1070
|
+
const account = await resolveAccount(api, ref);
|
|
1071
|
+
let password = opts.password;
|
|
1072
|
+
if (!password && opts.passwordStdin)
|
|
1073
|
+
password = readPasswordFromStdin();
|
|
1074
|
+
if (!password && opts.host && opts.username) {
|
|
1075
|
+
password = await promptHidden('CUCM password');
|
|
1076
|
+
}
|
|
1077
|
+
const payload = buildDiscoverPayload({ ...opts, password });
|
|
1078
|
+
const response = await api.post(`sftp_action_accounts/${account.id}/monitoring/discover`, payload);
|
|
1079
|
+
if (opts.json)
|
|
1080
|
+
printJson(response.data);
|
|
1081
|
+
else
|
|
1082
|
+
out.ok(`Discovered backup schedules for ${account.username}`);
|
|
1083
|
+
}));
|
|
1084
|
+
addApiOptions(monitoring
|
|
1085
|
+
.command('schedules')
|
|
1086
|
+
.description('Discover CUCM backup schedules without applying them')
|
|
1087
|
+
.argument('<id|username>', 'Account ID or username'), true).action(action(async (ref, opts) => {
|
|
1088
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1089
|
+
requireOperation(spec, 'GET sftp_action_accounts/{account_id}/monitoring/schedules');
|
|
1090
|
+
const account = await resolveAccount(api, ref);
|
|
1091
|
+
const response = await api.get(`sftp_action_accounts/${account.id}/monitoring/schedules`);
|
|
1092
|
+
if (opts.json) {
|
|
1093
|
+
printJson(response.data);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const schedules = (response.data.data ?? []);
|
|
1097
|
+
if (schedules.length === 0) {
|
|
1098
|
+
out.info('No CUCM backup schedules discovered.');
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
console.log(renderTable(['SCHEDULE', 'FREQUENCY', 'TIME', 'DAY', 'DEVICE', 'PATH'], schedules.map(schedule => [
|
|
1102
|
+
String(schedule.schedule_name ?? '—'),
|
|
1103
|
+
String(schedule.frequency ?? '—'),
|
|
1104
|
+
`${String(schedule.hour ?? '—')}:${String(schedule.minute ?? 0).padStart(2, '0')}`,
|
|
1105
|
+
String(schedule.day_of_month ??
|
|
1106
|
+
(Array.isArray(schedule.days_of_week) ? schedule.days_of_week.join(',') : '—')),
|
|
1107
|
+
String(schedule.device_name ?? '—'),
|
|
1108
|
+
String(schedule.device_path ?? '—'),
|
|
1109
|
+
]), { maxWidth: [28, 14, 8, 10, 28, 50] }));
|
|
1110
|
+
console.log();
|
|
1111
|
+
}));
|
|
1112
|
+
}
|
|
1113
|
+
function printMonitoringTable(list) {
|
|
1114
|
+
if (list.length === 0) {
|
|
1115
|
+
out.info('No monitored accounts.');
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
console.log(renderTable(['USERNAME', 'STATUS', 'SCHEDULE', 'NEXT', 'LAST', 'MISSED'], list.map(account => [
|
|
1119
|
+
account.username,
|
|
1120
|
+
account.monitoring?.enabled ? 'enabled' : 'disabled',
|
|
1121
|
+
monitoringSchedule(account.monitoring),
|
|
1122
|
+
formatDateTime(account.monitoring?.expected_next_upload_at),
|
|
1123
|
+
formatDateTime(account.monitoring?.last_upload_at),
|
|
1124
|
+
String(account.monitoring?.missed_upload_count ?? 0),
|
|
1125
|
+
]), { maxWidth: [28, 9, 20, 20, 20, 8] }));
|
|
1126
|
+
console.log();
|
|
1127
|
+
}
|
|
1128
|
+
function printActivityTable(activity) {
|
|
1129
|
+
if (activity.length === 0) {
|
|
1130
|
+
out.info('No activity found.');
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
console.log(renderTable(['ID', 'WHEN', 'ACTION', 'STATUS', 'ACTOR', 'TARGET', 'ERROR'], activity.map(row => [
|
|
1134
|
+
String(row.id),
|
|
1135
|
+
formatDateTime(row.inserted_at),
|
|
1136
|
+
row.action ?? '—',
|
|
1137
|
+
row.status ?? '—',
|
|
1138
|
+
row.actor_ip ?? '—',
|
|
1139
|
+
row.target_type ? `${row.target_type}:${row.target_id ?? '?'}` : '—',
|
|
1140
|
+
row.error_message ?? '—',
|
|
1141
|
+
]), { maxWidth: [8, 20, 24, 9, 18, 24, 40] }));
|
|
1142
|
+
console.log();
|
|
1143
|
+
}
|
|
1144
|
+
// ── Org-wide activity and runs ───────────────────────────────────────────
|
|
1145
|
+
function registerActivity(sftp) {
|
|
1146
|
+
addApiOptions(sftp
|
|
1147
|
+
.command('activity')
|
|
1148
|
+
.description('List org-wide SFTP audit activity')
|
|
1149
|
+
.option('--username <username>', 'Filter by username')
|
|
1150
|
+
.option('--limit <n>', 'Maximum entries', '100')
|
|
1151
|
+
.option('--offset <n>', 'Offset', '0'), true).action(action(async (opts) => {
|
|
1152
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1153
|
+
requireOperation(spec, 'GET sftp_activity');
|
|
1154
|
+
const response = await api.get('sftp_activity', {
|
|
1155
|
+
query: {
|
|
1156
|
+
username: opts.username,
|
|
1157
|
+
limit: parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000, fallback: 100 }),
|
|
1158
|
+
offset: parseIntegerOption(opts.offset, '--offset', { min: 0, fallback: 0 }),
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
1161
|
+
if (opts.json) {
|
|
1162
|
+
printJson(response.data);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
printActivityTable((response.data.activity ?? response.data.data ?? []));
|
|
1166
|
+
}));
|
|
1167
|
+
addApiOptions(sftp
|
|
1168
|
+
.command('runs')
|
|
1169
|
+
.description('List org-wide SFTP action runs')
|
|
1170
|
+
.option('--username <username>', 'Filter by username')
|
|
1171
|
+
.option('--status <status>', 'Filter by status')
|
|
1172
|
+
.option('--type <type>', 'Filter by run type')
|
|
1173
|
+
.option('--event <event>', 'Filter by event')
|
|
1174
|
+
.option('--account-id <id>', 'Filter by action account ID')
|
|
1175
|
+
.option('--start-date <date>', 'Filter by start date')
|
|
1176
|
+
.option('--end-date <date>', 'Filter by end date')
|
|
1177
|
+
.option('--limit <n>', 'Maximum runs', '50')
|
|
1178
|
+
.option('--offset <n>', 'Offset', '0'), true).action(action(async (opts) => {
|
|
1179
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1180
|
+
requireOperation(spec, 'GET sftp_action_runs');
|
|
1181
|
+
const response = await api.get('sftp_action_runs', {
|
|
1182
|
+
query: {
|
|
1183
|
+
username: opts.username,
|
|
1184
|
+
status: opts.status,
|
|
1185
|
+
type: opts.type,
|
|
1186
|
+
event: opts.event,
|
|
1187
|
+
action_account_id: parseIntegerOption(opts.accountId, '--account-id', { min: 1 }),
|
|
1188
|
+
start_date: opts.startDate,
|
|
1189
|
+
end_date: opts.endDate,
|
|
1190
|
+
limit: parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000, fallback: 50 }),
|
|
1191
|
+
offset: parseIntegerOption(opts.offset, '--offset', { min: 0, fallback: 0 }),
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
if (opts.json) {
|
|
1195
|
+
printJson(response.data);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const runs = (response.data.runs ?? response.data.data ?? []);
|
|
1199
|
+
if (runs.length === 0) {
|
|
1200
|
+
out.info('No runs found.');
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
console.log(renderTable(['ID', 'ACCOUNT', 'STATUS', 'TYPE', 'EVENT', 'FILE', 'WHEN'], runs.map(run => [
|
|
1204
|
+
String(run.id),
|
|
1205
|
+
run.username ?? String(run.action_account_id ?? '—'),
|
|
1206
|
+
run.status ?? '—',
|
|
1207
|
+
run.type ?? '—',
|
|
1208
|
+
run.event ?? '—',
|
|
1209
|
+
run.file ?? '—',
|
|
1210
|
+
formatDateTime(run.inserted_at),
|
|
1211
|
+
]), { maxWidth: [8, 20, 10, 22, 20, 40, 20] }));
|
|
1212
|
+
console.log();
|
|
1213
|
+
}));
|
|
1214
|
+
}
|
|
1215
|
+
// ── Storage ──────────────────────────────────────────────────────────────
|
|
1216
|
+
function registerStorage(sftp) {
|
|
1217
|
+
const storage = sftp.command('storage').description('SFTP storage quota and purge operations');
|
|
1218
|
+
storage.action(() => storage.outputHelp());
|
|
1219
|
+
addApiOptions(storage.command('status').description('Show org-wide SFTP storage status'), true).action(action(async (opts) => {
|
|
1220
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1221
|
+
requireOperation(spec, 'GET sftp_storage/status');
|
|
1222
|
+
const response = await api.get('sftp_storage/status');
|
|
1223
|
+
const status = (response.data.storage ?? response.data);
|
|
1224
|
+
if (opts.json) {
|
|
1225
|
+
printJson(status);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
out.header('SFTP Storage');
|
|
1229
|
+
printKeyValues([
|
|
1230
|
+
['Used', formatBytes(status.total_used_bytes)],
|
|
1231
|
+
['Limit', formatGb(status.total_limit_gb)],
|
|
1232
|
+
['Accounts', String(status.account_count ?? '—')],
|
|
1233
|
+
['Over quota', String(status.accounts_over_quota ?? 0)],
|
|
1234
|
+
]);
|
|
1235
|
+
console.log();
|
|
1236
|
+
}));
|
|
1237
|
+
addApiOptions(storage.command('refresh').description('Recalculate quotas for all SFTP accounts'), true).action(action(async (opts) => {
|
|
1238
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1239
|
+
requireOperation(spec, 'POST sftp_storage/refresh_quotas');
|
|
1240
|
+
const response = await api.post('sftp_storage/refresh_quotas', {});
|
|
1241
|
+
if (opts.json)
|
|
1242
|
+
printJson(response.data);
|
|
1243
|
+
else
|
|
1244
|
+
out.ok(String(response.data.message ?? 'Refreshed SFTP quotas'));
|
|
1245
|
+
}));
|
|
1246
|
+
addApiOptions(storage
|
|
1247
|
+
.command('purge')
|
|
1248
|
+
.description('Trigger storage purge for the organization')
|
|
1249
|
+
.option('--retention-days <n>', 'Retention window in days')
|
|
1250
|
+
.option('--yes', 'Skip the confirmation prompt'), true).action(action(async (opts) => {
|
|
1251
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1252
|
+
requireOperation(spec, 'POST sftp_storage/purge');
|
|
1253
|
+
const days = parseIntegerOption(opts.retentionDays, '--retention-days', { min: 1, max: 3650 });
|
|
1254
|
+
if (!opts.json) {
|
|
1255
|
+
await confirmDestructive(`Purge SFTP storage${days ? ` with a ${days}-day retention window` : ''}?`, opts.yes);
|
|
1256
|
+
}
|
|
1257
|
+
const response = await api.post('sftp_storage/purge', {}, {
|
|
1258
|
+
query: { retention_days: days },
|
|
1259
|
+
});
|
|
1260
|
+
if (opts.json)
|
|
1261
|
+
printJson(response.data);
|
|
1262
|
+
else
|
|
1263
|
+
out.ok(String(response.data.message ?? 'SFTP storage purge started'));
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
// ── Backup analytics ─────────────────────────────────────────────────────
|
|
1267
|
+
function registerAnalytics(sftp) {
|
|
1268
|
+
const analytics = sftp
|
|
1269
|
+
.command('analytics')
|
|
1270
|
+
.alias('backups')
|
|
1271
|
+
.description('CUCM backup analytics and trends');
|
|
1272
|
+
analytics.action(() => analytics.outputHelp());
|
|
1273
|
+
const analyticsOptions = (command) => command
|
|
1274
|
+
.option('--days <n>', 'Lookback window in days')
|
|
1275
|
+
.option('--start-time <iso>', 'Start time (ISO 8601)')
|
|
1276
|
+
.option('--end-time <iso>', 'End time (ISO 8601)')
|
|
1277
|
+
.option('--username <username>', 'Filter by account username')
|
|
1278
|
+
.option('--cluster <name>', 'CUCM cluster')
|
|
1279
|
+
.option('--granularity <unit>', 'Trend granularity')
|
|
1280
|
+
.option('--limit <n>', 'Maximum entries')
|
|
1281
|
+
.option('--offset <n>', 'Offset');
|
|
1282
|
+
const bindQuery = (opts) => ({
|
|
1283
|
+
days: parseIntegerOption(opts.days, '--days', { min: 1, max: 3650 }),
|
|
1284
|
+
start_time: opts.startTime,
|
|
1285
|
+
end_time: opts.endTime,
|
|
1286
|
+
username: opts.username,
|
|
1287
|
+
cluster: opts.cluster,
|
|
1288
|
+
granularity: opts.granularity,
|
|
1289
|
+
limit: parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000 }),
|
|
1290
|
+
offset: parseIntegerOption(opts.offset, '--offset', { min: 0 }),
|
|
1291
|
+
});
|
|
1292
|
+
const addAnalyticsCommand = (name, path, description, requiresCluster = false) => {
|
|
1293
|
+
addApiOptions(analyticsOptions(analytics.command(name).description(description)), true).action(action(async (opts) => {
|
|
1294
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1295
|
+
requireOperation(spec, `GET ${path}`);
|
|
1296
|
+
if (requiresCluster && (!opts.username || !opts.cluster)) {
|
|
1297
|
+
throw new ApplianceApiError('--username and --cluster are required.');
|
|
1298
|
+
}
|
|
1299
|
+
const response = await api.get(path, { query: bindQuery(opts) });
|
|
1300
|
+
if (opts.json) {
|
|
1301
|
+
printJson(response.data);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// These endpoints return nested analytics objects. Pretty JSON keeps
|
|
1305
|
+
// every field visible without guessing a release-specific table shape.
|
|
1306
|
+
printJson(response.data);
|
|
1307
|
+
}));
|
|
1308
|
+
};
|
|
1309
|
+
addAnalyticsCommand('dashboard', 'sftp_backup_analytics/cucm/dashboard', 'CUCM backup dashboard summary');
|
|
1310
|
+
addAnalyticsCommand('trends', 'sftp_backup_analytics/trends', 'Backup trend analytics');
|
|
1311
|
+
addAnalyticsCommand('cluster', 'sftp_backup_analytics/cucm/cluster', 'CUCM cluster backup summary', true);
|
|
1312
|
+
addAnalyticsCommand('validations', 'sftp_backup_analytics/cucm/cluster/validations', 'CUCM cluster validation history', true);
|
|
1313
|
+
}
|
|
1314
|
+
// ── Report schedules ─────────────────────────────────────────────────────
|
|
1315
|
+
function registerReports(sftp) {
|
|
1316
|
+
const reports = sftp.command('reports').description('SFTP report schedules and executions');
|
|
1317
|
+
reports.action(() => reports.outputHelp());
|
|
1318
|
+
const schedules = reports.command('schedules').description('Manage SFTP report schedules');
|
|
1319
|
+
schedules.action(() => schedules.outputHelp());
|
|
1320
|
+
addApiOptions(schedules.command('list').description('List report schedules'), true).action(action(async (opts) => {
|
|
1321
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1322
|
+
requireOperation(spec, 'GET sftp_reports/schedules');
|
|
1323
|
+
const response = await api.get('sftp_reports/schedules');
|
|
1324
|
+
const list = (response.data.schedules ?? response.data.data ?? []);
|
|
1325
|
+
if (opts.json) {
|
|
1326
|
+
printJson(list);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (list.length === 0) {
|
|
1330
|
+
out.info('No report schedules.');
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
console.log(renderTable(['ID', 'TYPE', 'ENABLED', 'NEXT RUN', 'LAST RUN', 'STATUS', 'FAILURES'], list.map(schedule => [
|
|
1334
|
+
String(schedule.id),
|
|
1335
|
+
String(schedule.schedule_type ?? '—'),
|
|
1336
|
+
schedule.enabled ? 'yes' : 'no',
|
|
1337
|
+
formatDateTime(schedule.next_run_at),
|
|
1338
|
+
formatDateTime(schedule.last_run_at),
|
|
1339
|
+
schedule.last_run_status ?? '—',
|
|
1340
|
+
String(schedule.failure_count ?? 0),
|
|
1341
|
+
]), { maxWidth: [8, 20, 8, 20, 20, 12, 9] }));
|
|
1342
|
+
console.log();
|
|
1343
|
+
}));
|
|
1344
|
+
addApiOptions(schedules
|
|
1345
|
+
.command('show')
|
|
1346
|
+
.description('Show a report schedule')
|
|
1347
|
+
.argument('<id>', 'Schedule ID'), true).action(action(async (id, opts) => {
|
|
1348
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1349
|
+
requireOperation(spec, 'GET sftp_reports/schedules/{id}');
|
|
1350
|
+
const scheduleId = parsePositiveInt(id, 'schedule ID');
|
|
1351
|
+
const response = await api.get(`sftp_reports/schedules/${scheduleId}`);
|
|
1352
|
+
printJson(response.data.schedule ?? response.data);
|
|
1353
|
+
}));
|
|
1354
|
+
for (const [name, method] of [
|
|
1355
|
+
['create', 'POST'],
|
|
1356
|
+
['update', 'PUT'],
|
|
1357
|
+
]) {
|
|
1358
|
+
const command = addApiOptions(schedules
|
|
1359
|
+
.command(name)
|
|
1360
|
+
.description(`${name === 'create' ? 'Create' : 'Update'} a report schedule from JSON`)
|
|
1361
|
+
.requiredOption('--file <path>', 'JSON file with schedule fields (or a {schedule:{...}} object)'), true);
|
|
1362
|
+
if (name === 'update')
|
|
1363
|
+
command.argument('<id>', 'Schedule ID');
|
|
1364
|
+
command.action(action(async (...args) => {
|
|
1365
|
+
const opts = args[args.length - 1];
|
|
1366
|
+
const scheduleId = name === 'update' ? parsePositiveInt(String(args[0]), 'schedule ID') : undefined;
|
|
1367
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1368
|
+
const path = scheduleId
|
|
1369
|
+
? requireOperation(spec, 'PUT sftp_reports/schedules/{id}').shortPath.replace('{id}', String(scheduleId))
|
|
1370
|
+
: requireOperation(spec, 'POST sftp_reports/schedules').shortPath;
|
|
1371
|
+
const parsed = readJsonOption(opts.file.startsWith('@') ? opts.file : `@${opts.file}`, '--file');
|
|
1372
|
+
const schedule = typeof parsed === 'object' && parsed !== null && 'schedule' in parsed
|
|
1373
|
+
? parsed.schedule
|
|
1374
|
+
: parsed;
|
|
1375
|
+
if (typeof schedule !== 'object' || schedule === null || Array.isArray(schedule)) {
|
|
1376
|
+
throw new ApplianceApiError('--file must contain a JSON object.');
|
|
1377
|
+
}
|
|
1378
|
+
const response = await api.request(method, path, {
|
|
1379
|
+
body: { schedule },
|
|
1380
|
+
});
|
|
1381
|
+
if (opts.json)
|
|
1382
|
+
printJson(response.data);
|
|
1383
|
+
else
|
|
1384
|
+
out.ok(`Report schedule ${name === 'create' ? 'created' : 'updated'}`);
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
addApiOptions(schedules
|
|
1388
|
+
.command('delete')
|
|
1389
|
+
.description('Delete a report schedule')
|
|
1390
|
+
.argument('<id>', 'Schedule ID')
|
|
1391
|
+
.option('--yes', 'Skip the confirmation prompt'), true).action(action(async (id, opts) => {
|
|
1392
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1393
|
+
requireOperation(spec, 'DELETE sftp_reports/schedules/{id}');
|
|
1394
|
+
const scheduleId = parsePositiveInt(id, 'schedule ID');
|
|
1395
|
+
await confirmDestructive(`Delete report schedule ${scheduleId}?`, opts.yes);
|
|
1396
|
+
await api.delete(`sftp_reports/schedules/${scheduleId}`);
|
|
1397
|
+
if (opts.json)
|
|
1398
|
+
printJson({ deleted: true, id: scheduleId });
|
|
1399
|
+
else
|
|
1400
|
+
out.ok(`Deleted report schedule ${scheduleId}`);
|
|
1401
|
+
}));
|
|
1402
|
+
addApiOptions(schedules
|
|
1403
|
+
.command('run')
|
|
1404
|
+
.description('Run a report schedule now')
|
|
1405
|
+
.argument('<id>', 'Schedule ID'), true).action(action(async (id, opts) => {
|
|
1406
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1407
|
+
requireOperation(spec, 'POST sftp_reports/schedules/{id}/run');
|
|
1408
|
+
const scheduleId = parsePositiveInt(id, 'schedule ID');
|
|
1409
|
+
const response = await api.post(`sftp_reports/schedules/${scheduleId}/run`, {});
|
|
1410
|
+
if (opts.json)
|
|
1411
|
+
printJson(response.data);
|
|
1412
|
+
else
|
|
1413
|
+
out.ok(`Triggered report schedule ${scheduleId}`);
|
|
1414
|
+
}));
|
|
1415
|
+
addApiOptions(schedules
|
|
1416
|
+
.command('executions')
|
|
1417
|
+
.description('List execution history for a report schedule')
|
|
1418
|
+
.argument('<id>', 'Schedule ID')
|
|
1419
|
+
.option('--limit <n>', 'Maximum entries', '50')
|
|
1420
|
+
.option('--offset <n>', 'Offset', '0'), true).action(action(async (id, opts) => {
|
|
1421
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1422
|
+
requireOperation(spec, 'GET sftp_reports/schedules/{id}/executions');
|
|
1423
|
+
const scheduleId = parsePositiveInt(id, 'schedule ID');
|
|
1424
|
+
const response = await api.get(`sftp_reports/schedules/${scheduleId}/executions`, {
|
|
1425
|
+
query: {
|
|
1426
|
+
limit: parseIntegerOption(opts.limit, '--limit', { min: 1, max: 1000, fallback: 50 }),
|
|
1427
|
+
offset: parseIntegerOption(opts.offset, '--offset', { min: 0, fallback: 0 }),
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
printJson(response.data);
|
|
1431
|
+
}));
|
|
1432
|
+
addApiOptions(reports
|
|
1433
|
+
.command('executions')
|
|
1434
|
+
.description('Show a single report execution')
|
|
1435
|
+
.argument('<id>', 'Execution ID'), true).action(action(async (id, opts) => {
|
|
1436
|
+
const { api, spec } = await connectWithSpec(opts);
|
|
1437
|
+
requireOperation(spec, 'GET sftp_reports/executions/{id}');
|
|
1438
|
+
const executionId = parsePositiveInt(id, 'execution ID');
|
|
1439
|
+
const response = await api.get(`sftp_reports/executions/${executionId}`);
|
|
1440
|
+
printJson(response.data);
|
|
1441
|
+
}));
|
|
1442
|
+
}
|
|
1443
|
+
// ── Generic OpenAPI operation ────────────────────────────────────────────
|
|
1444
|
+
function registerApi(sftp) {
|
|
1445
|
+
addApiOptions(sftp
|
|
1446
|
+
.command('api')
|
|
1447
|
+
.description('Call any SFTP operation exposed by the appliance OpenAPI spec')
|
|
1448
|
+
.argument('[operation]', 'Example: "GET sftp_action_accounts/{id}/quota"')
|
|
1449
|
+
.option('--list', 'List SFTP operations from the installed release')
|
|
1450
|
+
.option('--all', 'With --list, include non-SFTP operations')
|
|
1451
|
+
.option('--method <method>', 'HTTP method when the path supports several')
|
|
1452
|
+
.option('--param <key=value>', 'Path parameter (repeatable)', collect, [])
|
|
1453
|
+
.option('--query <key=value>', 'Query parameter (repeatable)', collect, [])
|
|
1454
|
+
.option('--body <json>', 'JSON body or @file')
|
|
1455
|
+
.option('--output <path>', 'Write the response to a file')
|
|
1456
|
+
.option('--raw', 'Print the response as raw text')
|
|
1457
|
+
.option('--dry-run', 'Print the request without sending it')
|
|
1458
|
+
.option('--yes', 'Allow destructive methods without confirmation'), true).action(action(async (operation, opts) => {
|
|
1459
|
+
let api;
|
|
1460
|
+
let spec;
|
|
1461
|
+
if (opts.list) {
|
|
1462
|
+
api = await connect(opts, { requireAuth: false });
|
|
1463
|
+
try {
|
|
1464
|
+
spec = await api.fetchSpec();
|
|
1465
|
+
}
|
|
1466
|
+
catch (err) {
|
|
1467
|
+
// A release may protect /api/openapi. Retry with local credentials
|
|
1468
|
+
// before surfacing an authentication error.
|
|
1469
|
+
if (err instanceof ApplianceApiError && (err.status === 401 || err.status === 403)) {
|
|
1470
|
+
api = await connect(opts, { requireAuth: true });
|
|
1471
|
+
spec = await api.fetchSpec();
|
|
1472
|
+
}
|
|
1473
|
+
else {
|
|
1474
|
+
throw err;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
else {
|
|
1479
|
+
api = await connect(opts);
|
|
1480
|
+
spec = await api.fetchSpec();
|
|
1481
|
+
}
|
|
1482
|
+
if (opts.list) {
|
|
1483
|
+
const operations = listOperations(spec, opts.all ? {} : { contains: 'sftp' });
|
|
1484
|
+
if (opts.json) {
|
|
1485
|
+
printJson(operations);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (operations.length === 0) {
|
|
1489
|
+
out.warn('No matching operations in the installed release spec.');
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
console.log(renderTable(['METHOD', 'OPERATION', 'SUMMARY'], operations.map(entry => [entry.method.toUpperCase(), entry.shortPath, entry.summary]), { maxWidth: [7, 64, 60] }));
|
|
1493
|
+
console.log();
|
|
1494
|
+
out.info(`${operations.length} operation(s) · OpenAPI ${spec.openapi ?? 'unknown'} · ${spec.info?.version ?? ''}`);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (!operation) {
|
|
1498
|
+
throw new ApplianceApiError('Provide an operation, or use --list to discover them.');
|
|
1499
|
+
}
|
|
1500
|
+
const withMethod = opts.method
|
|
1501
|
+
? `${opts.method.toUpperCase()} ${operation.replace(/^(GET|POST|PUT|PATCH|DELETE)\s+/i, '')}`
|
|
1502
|
+
: operation;
|
|
1503
|
+
const resolved = getOperation(spec, withMethod);
|
|
1504
|
+
const pathParams = parsePairs(opts.param, '--param');
|
|
1505
|
+
Object.assign(pathParams, resolved.extractedParams);
|
|
1506
|
+
// The generated spec does not always declare path parameters, so use
|
|
1507
|
+
// the template placeholders as the authoritative required set.
|
|
1508
|
+
for (const name of pathPlaceholders(resolved.path)) {
|
|
1509
|
+
if (name === 'org_id')
|
|
1510
|
+
continue;
|
|
1511
|
+
if (!pathParams[name]) {
|
|
1512
|
+
throw new ApplianceApiError(`Missing path parameter --param ${name}=<value> for ${resolved.shortPath}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
let path = resolved.path;
|
|
1516
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
1517
|
+
path = path.split(`{${key}}`).join(encodeURIComponent(value));
|
|
1518
|
+
}
|
|
1519
|
+
path = path.replace('{org_id}', String(api.orgId));
|
|
1520
|
+
const query = parsePairs(opts.query, '--query');
|
|
1521
|
+
for (const parameter of operationQueryParams(resolved.operation)) {
|
|
1522
|
+
if (parameter.required && !query[parameter.name]) {
|
|
1523
|
+
throw new ApplianceApiError(`Missing required query parameter --query ${parameter.name}=<value> for ${resolved.shortPath}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
let body;
|
|
1527
|
+
const bodySchema = operationBodySchema(spec, resolved.operation);
|
|
1528
|
+
if (opts.body !== undefined) {
|
|
1529
|
+
body = readJsonOption(opts.body, '--body');
|
|
1530
|
+
}
|
|
1531
|
+
else if (resolved.operation.requestBody?.required) {
|
|
1532
|
+
throw new ApplianceApiError(`This operation requires a body (${describeSchema(spec, bodySchema)}). Pass --body <json|@file>.`);
|
|
1533
|
+
}
|
|
1534
|
+
if (body !== undefined && bodySchema) {
|
|
1535
|
+
const errors = validateAgainstSchema(spec, bodySchema, body);
|
|
1536
|
+
if (errors.length > 0) {
|
|
1537
|
+
throw new ApplianceApiError(`Invalid request body:\n - ${errors.join('\n - ')}`);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const method = resolved.method.toUpperCase();
|
|
1541
|
+
if (opts.dryRun) {
|
|
1542
|
+
console.log(`${chalk.bold(method)} ${api.urlFor(path, query)}`);
|
|
1543
|
+
console.log(` Authorization: ${api.token ? '[REDACTED]' : '(none)'}`);
|
|
1544
|
+
if (body !== undefined) {
|
|
1545
|
+
console.log(' Content-Type: application/json');
|
|
1546
|
+
console.log(JSON.stringify(body, null, 2));
|
|
1547
|
+
}
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
if (method === 'DELETE') {
|
|
1551
|
+
await confirmDestructive(`Send DELETE ${resolved.shortPath}?`, opts.yes);
|
|
1552
|
+
}
|
|
1553
|
+
const response = await api.request(method, path, {
|
|
1554
|
+
query,
|
|
1555
|
+
body,
|
|
1556
|
+
outputPath: opts.output,
|
|
1557
|
+
parse: opts.output ? 'none' : opts.raw ? 'text' : 'json',
|
|
1558
|
+
accept: opts.raw ? 'text/plain' : 'application/json',
|
|
1559
|
+
});
|
|
1560
|
+
if (opts.output) {
|
|
1561
|
+
out.ok(`Wrote response to ${opts.output}`);
|
|
1562
|
+
}
|
|
1563
|
+
else if (opts.raw) {
|
|
1564
|
+
console.log(String(response.data));
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
printJson(response.data ?? null);
|
|
1568
|
+
}
|
|
1569
|
+
}));
|
|
1570
|
+
}
|
|
1571
|
+
export function registerSftpCommand(program) {
|
|
1572
|
+
const sftp = program
|
|
1573
|
+
.command('sftp')
|
|
1574
|
+
.description('SFTP Action Accounts, upload monitoring, storage, and support');
|
|
1575
|
+
sftp.action(() => sftp.outputHelp());
|
|
1576
|
+
registerStatus(sftp);
|
|
1577
|
+
registerDoctor(sftp);
|
|
1578
|
+
registerAccounts(sftp);
|
|
1579
|
+
registerMonitoring(sftp);
|
|
1580
|
+
registerActivity(sftp);
|
|
1581
|
+
registerStorage(sftp);
|
|
1582
|
+
registerAnalytics(sftp);
|
|
1583
|
+
registerReports(sftp);
|
|
1584
|
+
registerApi(sftp);
|
|
1585
|
+
}
|
|
1586
|
+
//# sourceMappingURL=sftp.js.map
|