@velaro/cli 0.7.0 → 1.4.8
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 +161 -138
- package/bin/velaro.js +177 -62
- package/lib/api.js +91 -52
- package/lib/api.test.js +46 -0
- package/lib/banner.js +76 -0
- package/lib/commands/activity.js +133 -0
- package/lib/commands/acuity.js +66 -0
- package/lib/commands/agent.js +204 -50
- package/lib/commands/ai-config.js +193 -0
- package/lib/commands/ai-models.js +159 -0
- package/lib/commands/appointments.js +198 -0
- package/lib/commands/article.js +668 -388
- package/lib/commands/automation-draft.js +134 -0
- package/lib/commands/avatar.js +75 -0
- package/lib/commands/bigcommerce.js +50 -0
- package/lib/commands/billing-contacts.js +62 -0
- package/lib/commands/billing-email-preference.js +64 -0
- package/lib/commands/billing-subscription.js +265 -0
- package/lib/commands/billing.js +138 -0
- package/lib/commands/bot.js +141 -137
- package/lib/commands/bundle.js +168 -0
- package/lib/commands/calendly.js +62 -0
- package/lib/commands/callback.js +125 -0
- package/lib/commands/callrail.js +88 -0
- package/lib/commands/campaigns.js +44 -0
- package/lib/commands/case.js +102 -0
- package/lib/commands/check.js +163 -163
- package/lib/commands/compliance.js +229 -0
- package/lib/commands/conversation-efficiency.js +178 -0
- package/lib/commands/copilotstudio.js +114 -0
- package/lib/commands/coupon-grant.js +192 -0
- package/lib/commands/db.js +101 -0
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/diagnostics.js +298 -0
- package/lib/commands/email-campaign.js +47 -0
- package/lib/commands/email-inbox.js +88 -0
- package/lib/commands/entitlement.js +176 -0
- package/lib/commands/env.js +45 -45
- package/lib/commands/feature-discovery.js +40 -0
- package/lib/commands/focus.js +278 -0
- package/lib/commands/index.js +38 -5
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/inline-widget-config.js +126 -0
- package/lib/commands/integration.js +93 -0
- package/lib/commands/kb.js +450 -309
- package/lib/commands/login.js +86 -86
- package/lib/commands/logs.js +680 -0
- package/lib/commands/magento.js +210 -0
- package/lib/commands/mcp-key.js +188 -159
- package/lib/commands/migrate.js +134 -0
- package/lib/commands/migration-status.js +66 -0
- package/lib/commands/monday.js +137 -0
- package/lib/commands/netsuite.js +87 -0
- package/lib/commands/notifications.js +63 -0
- package/lib/commands/notion.js +70 -0
- package/lib/commands/ops.js +267 -173
- package/lib/commands/payment-recovery.js +170 -0
- package/lib/commands/pickup.js +172 -0
- package/lib/commands/pricing.js +132 -0
- package/lib/commands/product.js +55 -0
- package/lib/commands/recruiting.js +374 -0
- package/lib/commands/report.js +462 -0
- package/lib/commands/routing.js +304 -0
- package/lib/commands/rule.js +85 -85
- package/lib/commands/sharepoint.js +167 -0
- package/lib/commands/site-provision.js +68 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/sitesync.js +158 -0
- package/lib/commands/slack.js +64 -0
- package/lib/commands/squarespace.js +108 -0
- package/lib/commands/status.js +24 -24
- package/lib/commands/subscription.js +43 -0
- package/lib/commands/support.js +128 -0
- package/lib/commands/survey.js +216 -0
- package/lib/commands/team.js +144 -144
- package/lib/commands/teams-phone.js +131 -0
- package/lib/commands/teams.js +106 -0
- package/lib/commands/telephony.js +99 -0
- package/lib/commands/update.js +47 -47
- package/lib/commands/webflow.js +128 -0
- package/lib/commands/whoami.js +25 -22
- package/lib/commands/widget-container.js +152 -0
- package/lib/commands/woocommerce.js +240 -0
- package/lib/commands/workflow.js +233 -98
- package/lib/config.js +85 -83
- package/lib/kb-screenshot.js +320 -0
- package/lib/migrations/amscro.json +72 -0
- package/lib/migrations/azenta.json +68 -0
- package/lib/migrations/bluefire.json +49 -0
- package/lib/migrations/donaldson.json +75 -0
- package/lib/oauth.js +149 -135
- package/lib/run.js +21 -16
- package/lib/sharepoint-auth.js +138 -0
- package/lib/subscription.js +41 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +34 -19
- package/scripts/postinstall.js +12 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
import { getCredentials } from '../api.js';
|
|
2
|
+
import { readConfig, ENVS } from '../config.js';
|
|
3
|
+
|
|
4
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const RESET = '\x1b[0m';
|
|
7
|
+
const DIM = '\x1b[2m';
|
|
8
|
+
const RED = '\x1b[31m';
|
|
9
|
+
const YEL = '\x1b[33m';
|
|
10
|
+
const CYN = '\x1b[36m';
|
|
11
|
+
|
|
12
|
+
function parseDuration(s) {
|
|
13
|
+
const m = /^(\d+)(m|h|d)$/i.exec(String(s).trim());
|
|
14
|
+
if (!m) return 2 * 3600 * 1000;
|
|
15
|
+
const n = parseInt(m[1], 10);
|
|
16
|
+
if (m[2] === 'm') return n * 60 * 1000;
|
|
17
|
+
if (m[2] === 'h') return n * 3600 * 1000;
|
|
18
|
+
return n * 24 * 3600 * 1000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printEntry(e) {
|
|
22
|
+
const ts = new Date(e.timestamp ?? e.LogTimestamp).toISOString().replace('T', ' ').slice(0, 19);
|
|
23
|
+
const lvl = (e.Level ?? e.level ?? '?').padEnd(5);
|
|
24
|
+
const color = (e.Level ?? e.level) === 'ERROR' ? RED : (e.Level ?? e.level) === 'WARN' ? YEL : RESET;
|
|
25
|
+
const tag = (e.IntegrationTag ?? e.integrationTag) ? ` ${CYN}[${e.IntegrationTag ?? e.integrationTag}]${RESET}` : '';
|
|
26
|
+
const site = (e.siteId ?? e.SiteId) ? ` ${DIM}(site ${e.siteId ?? e.SiteId})${RESET}` : '';
|
|
27
|
+
const logger = (e.Logger ?? e.logger ?? '').split('.').pop() ?? '';
|
|
28
|
+
const msg = e.Message ?? e.message ?? '';
|
|
29
|
+
console.log(`${DIM}${ts}${RESET} ${color}${lvl}${RESET} ${DIM}${logger}${RESET}${tag}${site} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getMsgBase(argv) {
|
|
33
|
+
const cfg = readConfig();
|
|
34
|
+
const env = argv.env || cfg.env || 'staging';
|
|
35
|
+
return cfg.envs?.[env]?.messagingApiBase ?? ENVS[env]?.messagingApiBase
|
|
36
|
+
?? 'https://velaro-messaging-api-staging.azurewebsites.net';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function requireAdmin(creds) {
|
|
40
|
+
if (creds.siteId !== 1032) {
|
|
41
|
+
console.error('velaro logs is only available to Velaro staff (site 1032).');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── search handler ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async function searchLogs(argv) {
|
|
49
|
+
const creds = await getCredentials();
|
|
50
|
+
await requireAdmin(creds);
|
|
51
|
+
const msgBase = await getMsgBase(argv);
|
|
52
|
+
|
|
53
|
+
const from = new Date(Date.now() - parseDuration(argv.last ?? '2h'));
|
|
54
|
+
const params = new URLSearchParams({ from: from.toISOString(), take: String(argv.take ?? 50) });
|
|
55
|
+
if (argv.query) params.set('q', argv.query);
|
|
56
|
+
if (argv.level) params.set('level', argv.level.toUpperCase());
|
|
57
|
+
if (argv.integration) params.set('integration', argv.integration);
|
|
58
|
+
if (argv.site) params.set('siteId', String(argv.site));
|
|
59
|
+
if (argv.source) params.set('source', argv.source);
|
|
60
|
+
if (argv.regex) params.set('regex', 'true');
|
|
61
|
+
|
|
62
|
+
const res = await fetch(`${msgBase}/superadmin/logs/search?${params}`, {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
65
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
66
|
+
},
|
|
67
|
+
signal: AbortSignal.timeout(20000),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entries = await res.json();
|
|
76
|
+
if (!Array.isArray(entries) || !entries.length) {
|
|
77
|
+
console.log('No results.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`\n${DIM}${entries.length} result(s) from ${from.toISOString().slice(0, 16)} UTC${RESET}\n`);
|
|
82
|
+
for (const e of entries) printEntry(e);
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── tail handler (SSE stream) ─────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function tailLogs(argv) {
|
|
89
|
+
const creds = await getCredentials();
|
|
90
|
+
await requireAdmin(creds);
|
|
91
|
+
const msgBase = await getMsgBase(argv);
|
|
92
|
+
|
|
93
|
+
const url = new URL(`${msgBase}/superadmin/logs/stream`);
|
|
94
|
+
if (argv.level) url.searchParams.set('level', argv.level.toUpperCase());
|
|
95
|
+
if (argv.integration) url.searchParams.set('integration', argv.integration);
|
|
96
|
+
if (argv.site) url.searchParams.set('siteId', String(argv.site));
|
|
97
|
+
if (argv.source) url.searchParams.set('source', argv.source);
|
|
98
|
+
|
|
99
|
+
const envLabel = argv.env || readConfig().env || 'staging';
|
|
100
|
+
console.log(`\n${DIM}Streaming from ${envLabel} — Ctrl+C to stop${RESET}\n`);
|
|
101
|
+
|
|
102
|
+
const res = await fetch(url.toString(), {
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
105
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
106
|
+
},
|
|
107
|
+
signal: AbortSignal.timeout(660_000),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
console.error(`HTTP ${res.status}: could not connect to log stream`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
for await (const chunk of res.body) {
|
|
117
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
118
|
+
for (const line of text.split('\n')) {
|
|
119
|
+
if (!line.startsWith('data: ')) continue;
|
|
120
|
+
const json = line.slice(6).trim();
|
|
121
|
+
if (!json || json.startsWith('{"status"')) continue;
|
|
122
|
+
try { printEntry(JSON.parse(json)); } catch { /* malformed line */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── digest-recipients handlers ────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function getAdminBase(argv) {
|
|
130
|
+
const cfg = readConfig();
|
|
131
|
+
const env = argv.env || cfg.env || 'staging';
|
|
132
|
+
return cfg.envs?.[env]?.adminApiBase ?? ENVS[env]?.adminApiBase
|
|
133
|
+
?? 'https://velaro-admin-staging.azurewebsites.net';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function getDigestRecipients(argv) {
|
|
137
|
+
const creds = await getCredentials();
|
|
138
|
+
await requireAdmin(creds);
|
|
139
|
+
const adminBase = await getAdminBase(argv);
|
|
140
|
+
|
|
141
|
+
const res = await fetch(`${adminBase}/superadmin/logs/digest-recipients`, {
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
144
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
145
|
+
},
|
|
146
|
+
signal: AbortSignal.timeout(15000),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
console.log(`\nLog digest recipients: ${data.recipients}`);
|
|
156
|
+
console.log(`\nTo persist changes: set LOG_DIGEST_RECIPIENTS in Azure App Settings.`);
|
|
157
|
+
console.log(`Digest job runs every 4h (Hangfire job: log-digest-4h, queue: low).\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function setDigestRecipients(argv) {
|
|
161
|
+
const creds = await getCredentials();
|
|
162
|
+
await requireAdmin(creds);
|
|
163
|
+
const adminBase = await getAdminBase(argv);
|
|
164
|
+
|
|
165
|
+
if (!argv.emails) {
|
|
166
|
+
console.error('--emails is required. Example: velaro logs set-digest-recipients --emails "noc@velaro.com;alex@velaro.com"');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const res = await fetch(`${adminBase}/superadmin/logs/digest-recipients`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
174
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ recipients: argv.emails }),
|
|
178
|
+
signal: AbortSignal.timeout(15000),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
const body = await res.text().catch(() => '');
|
|
183
|
+
console.error(`HTTP ${res.status}: ${body}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const data = await res.json();
|
|
188
|
+
console.log(`\n✓ Recipients validated: ${data.recipients}`);
|
|
189
|
+
console.log(`Note: ${data.note}\n`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── diagnose handler ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
async function diagnoseLogs(argv) {
|
|
195
|
+
const creds = await getCredentials();
|
|
196
|
+
await requireAdmin(creds);
|
|
197
|
+
const msgBase = await getMsgBase(argv);
|
|
198
|
+
const window = argv.last ?? '1h';
|
|
199
|
+
const from = new Date(Date.now() - parseDuration(window));
|
|
200
|
+
|
|
201
|
+
const BOLD = '\x1b[1m'; const RESET2 = '\x1b[0m'; const GREEN = '\x1b[32m';
|
|
202
|
+
|
|
203
|
+
console.log(`\n${BOLD}── Bot Diagnostic — last ${window} ──────────────────────────────${RESET2}`);
|
|
204
|
+
|
|
205
|
+
// Run error search and perf/routing search in parallel
|
|
206
|
+
async function search(q) {
|
|
207
|
+
const p = new URLSearchParams({ q, from: from.toISOString(), take: '50' });
|
|
208
|
+
const r = await fetch(`${msgBase}/superadmin/logs/search?${p}`, {
|
|
209
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
210
|
+
signal: AbortSignal.timeout(15000),
|
|
211
|
+
});
|
|
212
|
+
if (!r.ok) return [];
|
|
213
|
+
const d = await r.json();
|
|
214
|
+
return Array.isArray(d) ? d : [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const [errors, perfAndRouting] = await Promise.all([
|
|
218
|
+
search('level:ERROR'),
|
|
219
|
+
search('WF-AI-ROUTE OR PERF-SLOW'),
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
// ── Errors ──
|
|
223
|
+
console.log(`\n${BOLD}Errors${RESET2}`);
|
|
224
|
+
if (!errors.length) {
|
|
225
|
+
console.log(` ${GREEN}✓${RESET2} No errors`);
|
|
226
|
+
} else {
|
|
227
|
+
for (const e of errors) printEntry(e);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Perf ──
|
|
231
|
+
const perfEntries = perfAndRouting.filter(e => (e.Message ?? e.message ?? '').includes('PERF-SLOW'));
|
|
232
|
+
const routeEntries = perfAndRouting.filter(e => (e.Message ?? e.message ?? '').includes('WF-AI-ROUTE'));
|
|
233
|
+
|
|
234
|
+
console.log(`\n${BOLD}Perf [WF-PERF-SLOW] [AI-PERF-SLOW]${RESET2}`);
|
|
235
|
+
if (!perfEntries.length) {
|
|
236
|
+
console.log(` ${GREEN}✓${RESET2} No slow responses`);
|
|
237
|
+
} else {
|
|
238
|
+
for (const e of perfEntries) printEntry(e);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── AI Routing ──
|
|
242
|
+
console.log(`\n${BOLD}AI Intent Routing [WF-AI-ROUTE]${RESET2}`);
|
|
243
|
+
if (!routeEntries.length) {
|
|
244
|
+
console.log(` ${GREEN}✓${RESET2} No AI routing events`);
|
|
245
|
+
} else {
|
|
246
|
+
const matched = routeEntries.filter(e => (e.Message ?? e.message ?? '').includes('AI matched'));
|
|
247
|
+
const timeouts = routeEntries.filter(e => (e.Message ?? e.message ?? '').includes('timed out'));
|
|
248
|
+
const failed = routeEntries.filter(e => (e.Message ?? e.message ?? '').includes('failed'));
|
|
249
|
+
console.log(` Matched: ${matched.length} Timeouts: ${timeouts.length} Errors: ${failed.length}`);
|
|
250
|
+
for (const e of routeEntries.slice(0, 10)) printEntry(e);
|
|
251
|
+
if (routeEntries.length > 10) console.log(` ... and ${routeEntries.length - 10} more`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Summary ──
|
|
255
|
+
const status = errors.length ? '\x1b[31m⚠ ERRORS FOUND\x1b[0m'
|
|
256
|
+
: perfEntries.length ? '\x1b[33m⚠ SLOW RESPONSES\x1b[0m'
|
|
257
|
+
: routeEntries.length ? '\x1b[36m🤖 AI ROUTING ACTIVE\x1b[0m'
|
|
258
|
+
: `${GREEN}✅ ALL CLEAR\x1b[0m`;
|
|
259
|
+
console.log(`\n${BOLD}Summary:${RESET2} ${status} (${errors.length} errors, ${perfEntries.length} slow, ${routeEntries.length} ai-routes)\n`);
|
|
260
|
+
console.log(`Velaro Logger presaved filter: "AI Intent Routing + Perf"\n`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── alert rule helpers ───────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
const ALERT_RULE_DEFAULTS = {
|
|
266
|
+
windowMinutes: 5,
|
|
267
|
+
thresholdCount: 1,
|
|
268
|
+
minRefireMinutes: 60,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Strips ANSI/control characters from AI-generated text before it ever reaches
|
|
272
|
+
// the terminal (not just the copy-pasteable command line below) — an AI
|
|
273
|
+
// suggestion is untrusted text and should never be able to inject escape
|
|
274
|
+
// sequences into the user's terminal.
|
|
275
|
+
function sanitizeForTerminal(s) {
|
|
276
|
+
if (typeof s !== 'string') return s;
|
|
277
|
+
// eslint-disable-next-line no-control-regex
|
|
278
|
+
return s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// POSIX-safe single-quote wrapping for interpolating untrusted values into a
|
|
282
|
+
// printed shell command line (standard 'wrap, escape embedded quotes' trick).
|
|
283
|
+
function shellQuote(value) {
|
|
284
|
+
const s = String(value ?? '');
|
|
285
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Validates a value is meant to be a positive (>=1) finite integer for a
|
|
289
|
+
// numeric alert-rule flag. Returns the parsed integer, or null if invalid.
|
|
290
|
+
// `argv[flag]` from yargs with `type: 'number'` is already a number (or NaN
|
|
291
|
+
// if the user passed something non-numeric like `--threshold abc`), but we
|
|
292
|
+
// re-parse defensively since a flag can also arrive as a string in some
|
|
293
|
+
// invocation paths.
|
|
294
|
+
function parsePositiveIntFlag(flagName, rawValue, { max } = {}) {
|
|
295
|
+
if (rawValue === undefined || rawValue === null) return { ok: true, value: undefined };
|
|
296
|
+
const n = typeof rawValue === 'number' ? rawValue : Number(rawValue);
|
|
297
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
298
|
+
return { ok: false, error: `--${flagName} must be a whole number >= 1 (got ${JSON.stringify(rawValue)}).` };
|
|
299
|
+
}
|
|
300
|
+
if (max !== undefined && n > max) {
|
|
301
|
+
return { ok: false, error: `--${flagName} must be <= ${max} (got ${n}).` };
|
|
302
|
+
}
|
|
303
|
+
return { ok: true, value: n };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// A rule with none of the four notification channels fires into the void —
|
|
307
|
+
// it's pure log noise that pages no one. Mirrors the same guard in
|
|
308
|
+
// client/src/pages/Messaging/Integrations/AlertRules.tsx (extended here to
|
|
309
|
+
// also cover webhook, which that component's check omits).
|
|
310
|
+
function hasAnyRecipient({ emails, sms, webhook, mobileUserIds }) {
|
|
311
|
+
return Boolean(
|
|
312
|
+
(emails && String(emails).trim()) ||
|
|
313
|
+
(sms && String(sms).trim()) ||
|
|
314
|
+
(webhook && String(webhook).trim()) ||
|
|
315
|
+
(mobileUserIds && String(mobileUserIds).trim())
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function printAlertRule(r) {
|
|
320
|
+
const state = r.enabled === false ? `${DIM}paused${RESET}` : `${CYN}active${RESET}`;
|
|
321
|
+
const bits = [`>=${r.thresholdCount} in ${r.windowMinutes}m`];
|
|
322
|
+
if (r.level) bits.push(`level=${r.level}`);
|
|
323
|
+
if (r.queryText) bits.push(`q="${r.queryText}"`);
|
|
324
|
+
if (r.integration) bits.push(`tag=${r.integration}`);
|
|
325
|
+
if (r.siteId) bits.push(`site=${r.siteId}`);
|
|
326
|
+
if (r.notifyPhoneNumbers) bits.push('SMS');
|
|
327
|
+
if (r.notifyEmails) bits.push('email');
|
|
328
|
+
if (r.notifyWebhookUrl) bits.push('webhook');
|
|
329
|
+
if (r.notifyMobileUserIds) bits.push('mobile push');
|
|
330
|
+
bits.push(`cooldown ${r.minRefireMinutes}m`);
|
|
331
|
+
console.log(`${DIM}[${r.id}]${RESET} ${r.name} ${state}`);
|
|
332
|
+
console.log(` ${bits.join(' ')}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function listAlertRules(argv) {
|
|
336
|
+
const creds = await getCredentials();
|
|
337
|
+
await requireAdmin(creds);
|
|
338
|
+
const msgBase = await getMsgBase(argv);
|
|
339
|
+
|
|
340
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/rules`, {
|
|
341
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
342
|
+
signal: AbortSignal.timeout(15000),
|
|
343
|
+
});
|
|
344
|
+
if (!res.ok) {
|
|
345
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const rules = await res.json();
|
|
349
|
+
if (!Array.isArray(rules) || !rules.length) {
|
|
350
|
+
console.log('No alert rules yet. Create one with: velaro logs alerts create --name "..." --query "..."');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(`\n${DIM}${rules.length} alert rule(s)${RESET}\n`);
|
|
354
|
+
for (const r of rules) printAlertRule(r);
|
|
355
|
+
console.log('');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function suggestAlertRule(argv) {
|
|
359
|
+
const creds = await getCredentials();
|
|
360
|
+
await requireAdmin(creds);
|
|
361
|
+
const msgBase = await getMsgBase(argv);
|
|
362
|
+
|
|
363
|
+
if (!argv.description) {
|
|
364
|
+
console.error('--description is required. Example: velaro logs alerts suggest --description "text me if NetSuite errors 3+ times in 10 min"');
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/suggest`, {
|
|
369
|
+
method: 'POST',
|
|
370
|
+
headers: {
|
|
371
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
372
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
373
|
+
'Content-Type': 'application/json',
|
|
374
|
+
},
|
|
375
|
+
body: JSON.stringify({ description: argv.description }),
|
|
376
|
+
signal: AbortSignal.timeout(30000),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const data = await res.json();
|
|
385
|
+
if (data.error) {
|
|
386
|
+
console.error(`Could not generate a suggestion: ${sanitizeForTerminal(String(data.error))}`);
|
|
387
|
+
console.log('You can still create a rule manually with: velaro logs alerts create --name "..." --query "..."');
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Every AI-generated field is untrusted text — strip control/ANSI escapes
|
|
392
|
+
// before it ever reaches the terminal, not just in the copy-paste line below.
|
|
393
|
+
const name = sanitizeForTerminal(data.name ?? '');
|
|
394
|
+
const queryText = sanitizeForTerminal(data.queryText ?? '');
|
|
395
|
+
const level = sanitizeForTerminal(data.level ?? '');
|
|
396
|
+
const integration = sanitizeForTerminal(data.integration ?? '');
|
|
397
|
+
const reasoning = data.reasoning ? sanitizeForTerminal(String(data.reasoning)) : null;
|
|
398
|
+
const windowMinutes = data.windowMinutes ?? ALERT_RULE_DEFAULTS.windowMinutes;
|
|
399
|
+
const thresholdCount = data.thresholdCount ?? ALERT_RULE_DEFAULTS.thresholdCount;
|
|
400
|
+
const minRefireMinutes = data.minRefireMinutes ?? ALERT_RULE_DEFAULTS.minRefireMinutes;
|
|
401
|
+
|
|
402
|
+
console.log(`\n${DIM}Suggested rule (preview only — not created; pass to 'create' to save):${RESET}\n`);
|
|
403
|
+
console.log(` name: ${name}`);
|
|
404
|
+
console.log(` queryText: ${queryText}`);
|
|
405
|
+
console.log(` level: ${level || '(any)'}`);
|
|
406
|
+
console.log(` integration: ${integration || '(any)'}`);
|
|
407
|
+
console.log(` windowMinutes: ${windowMinutes}`);
|
|
408
|
+
console.log(` thresholdCount: ${thresholdCount}`);
|
|
409
|
+
console.log(` minRefireMinutes: ${minRefireMinutes}`);
|
|
410
|
+
if (reasoning) console.log(`\n${DIM}Why:${RESET} ${reasoning}`);
|
|
411
|
+
console.log(`\nNote: this suggestion has no notification channels — add --emails/--sms/--webhook when you create it, or the rule will fire into the void.`);
|
|
412
|
+
console.log(`\nTo create it:\n velaro logs alerts create --name ${shellQuote(name)} --query ${shellQuote(queryText)}${level ? ` --level ${shellQuote(level)}` : ''} --window-minutes ${windowMinutes} --threshold ${thresholdCount} --cooldown-minutes ${minRefireMinutes} --emails "..." --yes\n`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function createAlertRule(argv) {
|
|
416
|
+
const creds = await getCredentials();
|
|
417
|
+
await requireAdmin(creds);
|
|
418
|
+
const msgBase = await getMsgBase(argv);
|
|
419
|
+
|
|
420
|
+
if (!argv.name || !argv.query) {
|
|
421
|
+
console.error('--name and --query are required. Example: velaro logs alerts create --name "NetSuite errors" --query "exception"');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// HIGH: LogAlertDecisionEngine evaluates `count >= effectiveThreshold`, so a
|
|
426
|
+
// threshold of 0 (or negative, or NaN from a bad --threshold value) fires on
|
|
427
|
+
// every single evaluation tick — a permanently-firing, unresolvable alert.
|
|
428
|
+
// The backend's CreateRule endpoint (unlike UpdateRule/PatchRuleByName) does
|
|
429
|
+
// not clamp this, so the CLI must validate before sending the request.
|
|
430
|
+
const checks = [
|
|
431
|
+
parsePositiveIntFlag('threshold', argv.threshold),
|
|
432
|
+
parsePositiveIntFlag('window-minutes', argv.windowMinutes, { max: 1440 }),
|
|
433
|
+
parsePositiveIntFlag('cooldown-minutes', argv.cooldownMinutes),
|
|
434
|
+
];
|
|
435
|
+
const failed = checks.find(c => !c.ok);
|
|
436
|
+
if (failed) {
|
|
437
|
+
console.error(failed.error);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const [thresholdCheck, windowCheck, cooldownCheck] = checks;
|
|
441
|
+
|
|
442
|
+
// HIGH: a rule with zero notification channels fires into the void.
|
|
443
|
+
if (!hasAnyRecipient({ emails: argv.emails, sms: argv.sms, webhook: argv.webhook, mobileUserIds: argv.mobileUserIds })
|
|
444
|
+
&& !argv.noRecipientsOk) {
|
|
445
|
+
console.error('No notification channel given (--emails, --sms, --webhook, or --mobile-user-ids) — this rule would fire but notify no one.');
|
|
446
|
+
console.error('If that\'s intentional (e.g. you only want it visible in Alert History), pass --no-recipients-ok to proceed anyway.');
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const body = {
|
|
451
|
+
name: argv.name,
|
|
452
|
+
queryText: argv.query,
|
|
453
|
+
level: argv.level ? argv.level.toUpperCase() : undefined,
|
|
454
|
+
integration: argv.integration,
|
|
455
|
+
windowMinutes: windowCheck.value ?? ALERT_RULE_DEFAULTS.windowMinutes,
|
|
456
|
+
thresholdCount: thresholdCheck.value ?? ALERT_RULE_DEFAULTS.thresholdCount,
|
|
457
|
+
minRefireMinutes: cooldownCheck.value ?? ALERT_RULE_DEFAULTS.minRefireMinutes,
|
|
458
|
+
notifyEmails: argv.emails,
|
|
459
|
+
notifyPhoneNumbers: argv.sms,
|
|
460
|
+
notifyWebhookUrl: argv.webhook,
|
|
461
|
+
notifyMobileUserIds: argv.mobileUserIds,
|
|
462
|
+
enabled: true,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// HIGH: create is a real, live write with no undo from the CLI — preview
|
|
466
|
+
// before sending, require --yes to actually proceed. Mirrors the
|
|
467
|
+
// `kb swap-connection` confirmation pattern in cli/lib/commands/kb.js.
|
|
468
|
+
if (!argv.yes) {
|
|
469
|
+
console.log(`\n${DIM}About to create this alert rule (dry run — nothing sent yet):${RESET}\n`);
|
|
470
|
+
printAlertRule({ ...body, id: '(new)' });
|
|
471
|
+
console.log(`\nRe-run with --yes to actually create it.\n`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/rules`, {
|
|
476
|
+
method: 'POST',
|
|
477
|
+
headers: {
|
|
478
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
479
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
480
|
+
'Content-Type': 'application/json',
|
|
481
|
+
},
|
|
482
|
+
body: JSON.stringify(body),
|
|
483
|
+
signal: AbortSignal.timeout(15000),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (!res.ok) {
|
|
487
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const data = await res.json();
|
|
492
|
+
console.log(`\n${CYN}✓${RESET} Created rule [${data.id}] "${data.name}"\n`);
|
|
493
|
+
printAlertRule(data);
|
|
494
|
+
console.log('');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function toggleAlertRule(argv) {
|
|
498
|
+
const creds = await getCredentials();
|
|
499
|
+
await requireAdmin(creds);
|
|
500
|
+
const msgBase = await getMsgBase(argv);
|
|
501
|
+
|
|
502
|
+
if (!argv.id) {
|
|
503
|
+
console.error('Rule id is required. Example: velaro logs alerts toggle 42');
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/rules/${argv.id}/toggle`, {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
510
|
+
signal: AbortSignal.timeout(15000),
|
|
511
|
+
});
|
|
512
|
+
if (!res.ok) {
|
|
513
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
const data = await res.json();
|
|
517
|
+
console.log(`Rule ${argv.id} is now ${data.enabled ? 'active' : 'paused'}.`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function deleteAlertRule(argv) {
|
|
521
|
+
const creds = await getCredentials();
|
|
522
|
+
await requireAdmin(creds);
|
|
523
|
+
const msgBase = await getMsgBase(argv);
|
|
524
|
+
|
|
525
|
+
if (!argv.id) {
|
|
526
|
+
console.error('Rule id is required. Example: velaro logs alerts delete 42');
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// HIGH: delete is a real, live, irreversible write — fetch and print the
|
|
531
|
+
// rule being deleted (name, query, recipients) before doing anything.
|
|
532
|
+
// There's no single-rule GET endpoint, so pull the list and find it there.
|
|
533
|
+
const listRes = await fetch(`${msgBase}/superadmin/log-alerts/rules`, {
|
|
534
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
535
|
+
signal: AbortSignal.timeout(15000),
|
|
536
|
+
});
|
|
537
|
+
let target = null;
|
|
538
|
+
if (listRes.ok) {
|
|
539
|
+
const rules = await listRes.json().catch(() => []);
|
|
540
|
+
target = Array.isArray(rules) ? rules.find(r => String(r.id) === String(argv.id)) : null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (target) {
|
|
544
|
+
console.log(`\n${DIM}About to delete this alert rule:${RESET}\n`);
|
|
545
|
+
printAlertRule(target);
|
|
546
|
+
} else {
|
|
547
|
+
console.log(`\n${YEL}Could not fetch rule ${argv.id} to preview it (it may already be gone) — proceeding on id alone.${RESET}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!argv.yes) {
|
|
551
|
+
console.log(`\nRe-run with --yes to actually delete it.\n`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const res = await fetch(`${msgBase}/superadmin/log-alerts/rules/${argv.id}`, {
|
|
556
|
+
method: 'DELETE',
|
|
557
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
558
|
+
signal: AbortSignal.timeout(15000),
|
|
559
|
+
});
|
|
560
|
+
if (!res.ok) {
|
|
561
|
+
console.error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
console.log(`\nDeleted rule ${argv.id}.\n`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ── command export ────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
export const logsCommand = {
|
|
570
|
+
command: 'logs <subcommand>',
|
|
571
|
+
describe: 'Search and stream Velaro Logger (Velaro staff only)',
|
|
572
|
+
builder: yargs => yargs
|
|
573
|
+
.command({
|
|
574
|
+
command: 'search [query]',
|
|
575
|
+
describe: 'Search application logs',
|
|
576
|
+
builder: y => y
|
|
577
|
+
.positional('query', { describe: 'Text to search for (or regex with --regex)', type: 'string' })
|
|
578
|
+
.option('level', { describe: 'Filter by level: ERROR, WARN, INFO, DEBUG', type: 'string' })
|
|
579
|
+
.option('integration', { describe: 'Integration tag (HubSpot, NetSuite, [Skill], etc.)', type: 'string' })
|
|
580
|
+
.option('site', { describe: 'Filter by site ID', type: 'number' })
|
|
581
|
+
.option('source', { describe: 'App source (velaro-messaging-Staging, etc.)', type: 'string' })
|
|
582
|
+
.option('last', { describe: 'Time window: 30m, 2h, 1d', default: '2h', type: 'string' })
|
|
583
|
+
.option('regex', { describe: 'Treat query as regex', boolean: true, default: false })
|
|
584
|
+
.option('take', { describe: 'Max results (max 500)', default: 50, type: 'number' })
|
|
585
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
586
|
+
handler: searchLogs,
|
|
587
|
+
})
|
|
588
|
+
.command({
|
|
589
|
+
command: 'tail',
|
|
590
|
+
describe: 'Stream new log entries in real-time via SSE',
|
|
591
|
+
builder: y => y
|
|
592
|
+
.option('level', { describe: 'Filter by level: ERROR, WARN, INFO, DEBUG', type: 'string' })
|
|
593
|
+
.option('integration', { describe: 'Integration tag', type: 'string' })
|
|
594
|
+
.option('site', { describe: 'Filter by site ID', type: 'number' })
|
|
595
|
+
.option('source', { describe: 'App source', type: 'string' })
|
|
596
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
597
|
+
handler: tailLogs,
|
|
598
|
+
})
|
|
599
|
+
.command({
|
|
600
|
+
command: 'digest-recipients',
|
|
601
|
+
describe: 'Show current log digest email recipients (Velaro staff only)',
|
|
602
|
+
builder: y => y
|
|
603
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
604
|
+
handler: getDigestRecipients,
|
|
605
|
+
})
|
|
606
|
+
.command({
|
|
607
|
+
command: 'set-digest-recipients',
|
|
608
|
+
describe: 'Update log digest email recipients (Velaro staff only)',
|
|
609
|
+
builder: y => y
|
|
610
|
+
.option('emails', { describe: 'Semicolon-separated email list, e.g. "noc@velaro.com;alex@velaro.com"', type: 'string', demandOption: true })
|
|
611
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
612
|
+
handler: setDigestRecipients,
|
|
613
|
+
})
|
|
614
|
+
.command({
|
|
615
|
+
command: 'diagnose',
|
|
616
|
+
describe: 'Run combined bot diagnostic: errors + [WF-PERF-SLOW] + [AI-PERF-SLOW] + [WF-AI-ROUTE]',
|
|
617
|
+
builder: y => y
|
|
618
|
+
.option('last', { describe: 'Time window: 30m, 1h, 2h, 1d', default: '1h', type: 'string' })
|
|
619
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
620
|
+
handler: diagnoseLogs,
|
|
621
|
+
})
|
|
622
|
+
.command({
|
|
623
|
+
command: 'alerts <subcommand>',
|
|
624
|
+
describe: 'Manage Velaro Logger alert rules (Velaro staff only)',
|
|
625
|
+
builder: y => y
|
|
626
|
+
.command({
|
|
627
|
+
command: 'list',
|
|
628
|
+
describe: 'List current alert rules',
|
|
629
|
+
builder: yy => yy.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
630
|
+
handler: listAlertRules,
|
|
631
|
+
})
|
|
632
|
+
.command({
|
|
633
|
+
command: 'suggest',
|
|
634
|
+
describe: 'Preview an AI-suggested rule config from a plain-English description (does not create it)',
|
|
635
|
+
builder: yy => yy
|
|
636
|
+
.option('description', { describe: 'Plain-English description of what to alert on', type: 'string', demandOption: true })
|
|
637
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
638
|
+
handler: suggestAlertRule,
|
|
639
|
+
})
|
|
640
|
+
.command({
|
|
641
|
+
command: 'create',
|
|
642
|
+
describe: 'Create a new alert rule',
|
|
643
|
+
builder: yy => yy
|
|
644
|
+
.option('name', { describe: 'Rule name', type: 'string', demandOption: true })
|
|
645
|
+
.option('query', { describe: 'Message-contains text to match', type: 'string', demandOption: true })
|
|
646
|
+
.option('level', { describe: 'ERROR, WARN, INFO, or DEBUG', type: 'string' })
|
|
647
|
+
.option('integration', { describe: 'Integration tag to match', type: 'string' })
|
|
648
|
+
.option('window-minutes', { describe: 'Rolling window in minutes', type: 'number' })
|
|
649
|
+
.option('threshold', { describe: 'Fire when count >= this within the window', type: 'number' })
|
|
650
|
+
.option('cooldown-minutes', { describe: 'Minimum minutes between re-fires', type: 'number' })
|
|
651
|
+
.option('emails', { describe: 'Comma-separated notification email addresses', type: 'string' })
|
|
652
|
+
.option('sms', { describe: 'Comma-separated E.164 phone numbers for SMS', type: 'string' })
|
|
653
|
+
.option('webhook', { describe: 'Webhook URL (Slack/Teams) to POST on fire', type: 'string' })
|
|
654
|
+
.option('mobile-user-ids', { describe: 'Comma-separated Velaro workspace user IDs for mobile push', type: 'string' })
|
|
655
|
+
.option('no-recipients-ok', { describe: 'Allow creating a rule with zero notification channels (visible in Alert History only)', type: 'boolean', default: false })
|
|
656
|
+
.option('yes', { describe: 'Skip the dry-run preview and actually create the rule', type: 'boolean', default: false })
|
|
657
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
658
|
+
handler: createAlertRule,
|
|
659
|
+
})
|
|
660
|
+
.command({
|
|
661
|
+
command: 'toggle <id>',
|
|
662
|
+
describe: 'Enable/pause an alert rule',
|
|
663
|
+
builder: yy => yy
|
|
664
|
+
.positional('id', { describe: 'Rule id', type: 'number' })
|
|
665
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
666
|
+
handler: toggleAlertRule,
|
|
667
|
+
})
|
|
668
|
+
.command({
|
|
669
|
+
command: 'delete <id>',
|
|
670
|
+
describe: 'Delete an alert rule',
|
|
671
|
+
builder: yy => yy
|
|
672
|
+
.positional('id', { describe: 'Rule id', type: 'number' })
|
|
673
|
+
.option('yes', { describe: 'Skip the confirmation preview and actually delete', type: 'boolean', default: false })
|
|
674
|
+
.option('env', { describe: 'staging or prod', type: 'string' }),
|
|
675
|
+
handler: deleteAlertRule,
|
|
676
|
+
})
|
|
677
|
+
.demandCommand(1, 'Specify a subcommand: list, suggest, create, toggle, or delete'),
|
|
678
|
+
})
|
|
679
|
+
.demandCommand(1, 'Specify a subcommand: search, tail, diagnose, digest-recipients, set-digest-recipients, or alerts'),
|
|
680
|
+
};
|