@velaro/cli 1.2.0 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
package/lib/commands/ops.js
CHANGED
|
@@ -1,173 +1,267 @@
|
|
|
1
|
-
import { get, getCredentials } from '../api.js';
|
|
2
|
-
import { readConfig, ENVS } from '../config.js';
|
|
3
|
-
|
|
4
|
-
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
function ago(dateStr) {
|
|
7
|
-
if (!dateStr) return 'unknown';
|
|
8
|
-
const diffMs = Date.now() - new Date(dateStr).getTime();
|
|
9
|
-
const mins = Math.round(diffMs / 60000);
|
|
10
|
-
if (mins < 2) return 'just now';
|
|
11
|
-
if (mins < 60) return `${mins}m ago`;
|
|
12
|
-
const hrs = Math.round(mins / 60);
|
|
13
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
14
|
-
return `${Math.round(hrs / 24)}d ago`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function utcLabel(dateStr) {
|
|
18
|
-
if (!dateStr) return '';
|
|
19
|
-
const d = new Date(dateStr);
|
|
20
|
-
return d.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const OK = '\x1b[32m✓\x1b[0m';
|
|
24
|
-
const WARN = '\x1b[33m⚠\x1b[0m';
|
|
25
|
-
const ERR = '\x1b[31m✗\x1b[0m';
|
|
26
|
-
const HIGH = '\x1b[31m[high]\x1b[0m ';
|
|
27
|
-
const MED = '\x1b[33m[med]\x1b[0m ';
|
|
28
|
-
const LOW = '\x1b[36m[low]\x1b[0m ';
|
|
29
|
-
|
|
30
|
-
function severityPrefix(s) {
|
|
31
|
-
if (s === 'high') return HIGH;
|
|
32
|
-
if (s === 'medium') return MED;
|
|
33
|
-
return LOW;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function pingVersion(label, base) {
|
|
37
|
-
try {
|
|
38
|
-
const res = await fetch(`${base}/Version`, { signal: AbortSignal.timeout(6000) });
|
|
39
|
-
if (!res.ok) return { label, ok: false, status: res.status };
|
|
40
|
-
const data = await res.json();
|
|
41
|
-
return { label, ok: true, version: data.version, deployedAt: data.deployedAt };
|
|
42
|
-
} catch (e) {
|
|
43
|
-
return { label, ok: false, err: e.message };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function pingHealth(label, url) {
|
|
48
|
-
try {
|
|
49
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
|
|
50
|
-
return { label, ok: res.ok, status: res.status };
|
|
51
|
-
} catch (e) {
|
|
52
|
-
return { label, ok: false, err: e.message };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ──
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
console.log(`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
1
|
+
import { get, del, post, getCredentials } from '../api.js';
|
|
2
|
+
import { readConfig, ENVS } from '../config.js';
|
|
3
|
+
|
|
4
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function ago(dateStr) {
|
|
7
|
+
if (!dateStr) return 'unknown';
|
|
8
|
+
const diffMs = Date.now() - new Date(dateStr).getTime();
|
|
9
|
+
const mins = Math.round(diffMs / 60000);
|
|
10
|
+
if (mins < 2) return 'just now';
|
|
11
|
+
if (mins < 60) return `${mins}m ago`;
|
|
12
|
+
const hrs = Math.round(mins / 60);
|
|
13
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
14
|
+
return `${Math.round(hrs / 24)}d ago`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function utcLabel(dateStr) {
|
|
18
|
+
if (!dateStr) return '';
|
|
19
|
+
const d = new Date(dateStr);
|
|
20
|
+
return d.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OK = '\x1b[32m✓\x1b[0m';
|
|
24
|
+
const WARN = '\x1b[33m⚠\x1b[0m';
|
|
25
|
+
const ERR = '\x1b[31m✗\x1b[0m';
|
|
26
|
+
const HIGH = '\x1b[31m[high]\x1b[0m ';
|
|
27
|
+
const MED = '\x1b[33m[med]\x1b[0m ';
|
|
28
|
+
const LOW = '\x1b[36m[low]\x1b[0m ';
|
|
29
|
+
|
|
30
|
+
function severityPrefix(s) {
|
|
31
|
+
if (s === 'high') return HIGH;
|
|
32
|
+
if (s === 'medium') return MED;
|
|
33
|
+
return LOW;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function pingVersion(label, base) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${base}/Version`, { signal: AbortSignal.timeout(6000) });
|
|
39
|
+
if (!res.ok) return { label, ok: false, status: res.status };
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
return { label, ok: true, version: data.version, deployedAt: data.deployedAt };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return { label, ok: false, err: e.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function pingHealth(label, url) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
|
|
50
|
+
return { label, ok: res.ok, status: res.status };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return { label, ok: false, err: e.message };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── hangfire subcommands ──────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
async function requireAdmin() {
|
|
59
|
+
const creds = await getCredentials();
|
|
60
|
+
if (creds.siteId !== 1032) {
|
|
61
|
+
console.error('velaro ops is only available to Velaro staff.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return creds;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hangfireCommand = {
|
|
68
|
+
command: 'hangfire <action>',
|
|
69
|
+
describe: 'Manage Hangfire failed jobs (Velaro staff only)',
|
|
70
|
+
builder: (yargs) => yargs
|
|
71
|
+
.positional('action', { choices: ['list', 'delete', 'retry'], describe: 'Action to perform' })
|
|
72
|
+
.option('id', { type: 'string', describe: 'Job ID (for delete/retry of a single job)' })
|
|
73
|
+
.option('all', { type: 'boolean', describe: 'Apply to all failed jobs (delete only)' }),
|
|
74
|
+
handler: async (argv) => {
|
|
75
|
+
await requireAdmin();
|
|
76
|
+
const { action, id, all } = argv;
|
|
77
|
+
|
|
78
|
+
if (action === 'list') {
|
|
79
|
+
const jobs = await get('/DatabaseTool/FailedJobs');
|
|
80
|
+
if (!jobs?.length) { console.log('No failed jobs.'); return; }
|
|
81
|
+
console.log(`\n${jobs.length} failed job(s):\n`);
|
|
82
|
+
for (const j of jobs) {
|
|
83
|
+
const name = [j.typeName, j.methodName].filter(Boolean).join('.') || '(unknown)';
|
|
84
|
+
console.log(` #${j.id.padEnd(5)} ${name.padEnd(50)} ${j.exceptionMessage?.split('\n')[0]?.slice(0, 80) ?? ''}`);
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
} else if (action === 'delete') {
|
|
88
|
+
if (all) {
|
|
89
|
+
const res = await del('/DatabaseTool/FailedJobs');
|
|
90
|
+
console.log(`Deleted ${res.deleted} failed job(s).`);
|
|
91
|
+
} else if (id) {
|
|
92
|
+
const res = await del(`/DatabaseTool/FailedJobs/${id}`);
|
|
93
|
+
console.log(`Deleted ${res.deleted} job(s).`);
|
|
94
|
+
} else {
|
|
95
|
+
console.error('Specify --id <jobId> or --all');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
} else if (action === 'retry') {
|
|
99
|
+
if (!id) { console.error('Specify --id <jobId>'); process.exit(1); }
|
|
100
|
+
const res = await post(`/DatabaseTool/FailedJobs/${id}/retry`);
|
|
101
|
+
console.log(`Requeued ${res.requeued} job(s).`);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── ops command ───────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export const opsCommand = {
|
|
109
|
+
command: 'ops',
|
|
110
|
+
describe: 'System health: deploy times, Hangfire, API status, and active alerts (Velaro staff only)',
|
|
111
|
+
builder: (yargs) => yargs.command(hangfireCommand),
|
|
112
|
+
handler: async () => {
|
|
113
|
+
const creds = await getCredentials();
|
|
114
|
+
const isAdmin = creds.siteId === 1032;
|
|
115
|
+
|
|
116
|
+
if (!isAdmin) {
|
|
117
|
+
console.error('velaro ops is only available to Velaro staff.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cfg = readConfig();
|
|
122
|
+
|
|
123
|
+
// ── 1. Deploy versions (anonymous — both envs in parallel) ───────────────
|
|
124
|
+
const prodBase = cfg.envs?.prod?.adminApiBase ?? ENVS.prod.adminApiBase;
|
|
125
|
+
const stagingBase = cfg.envs?.staging?.adminApiBase ?? ENVS.staging.adminApiBase;
|
|
126
|
+
|
|
127
|
+
const [prodVer, stagingVer] = await Promise.all([
|
|
128
|
+
pingVersion('prod', prodBase),
|
|
129
|
+
pingVersion('staging', stagingBase),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
console.log('\n\x1b[1m── Deploy ──────────────────────────────────────────────────\x1b[0m');
|
|
133
|
+
for (const v of [prodVer, stagingVer]) {
|
|
134
|
+
if (v.ok) {
|
|
135
|
+
const sym = OK;
|
|
136
|
+
const ver = `\x1b[36m${v.version}\x1b[0m`;
|
|
137
|
+
const time = `${ago(v.deployedAt).padEnd(12)} (${utcLabel(v.deployedAt)})`;
|
|
138
|
+
console.log(` ${sym} ${v.label.padEnd(8)} ${ver.padEnd(30)} deployed ${time}`);
|
|
139
|
+
} else {
|
|
140
|
+
const detail = v.err ?? `HTTP ${v.status}`;
|
|
141
|
+
console.log(` ${ERR} ${v.label.padEnd(8)} UNREACHABLE — ${detail}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── 2. API health (admin + messaging, both envs) ──────────────────────────
|
|
146
|
+
const prodMsgBase = cfg.envs?.prod?.messagingApiBase ?? ENVS.prod.messagingApiBase;
|
|
147
|
+
const stagingMsgBase = cfg.envs?.staging?.messagingApiBase ?? ENVS.staging.messagingApiBase;
|
|
148
|
+
|
|
149
|
+
const [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth] = await Promise.all([
|
|
150
|
+
pingHealth('admin/prod', `${prodBase}/Status`),
|
|
151
|
+
pingHealth('admin/staging', `${stagingBase}/Status`),
|
|
152
|
+
pingHealth('msg/prod', `${prodMsgBase}/api/health`),
|
|
153
|
+
pingHealth('msg/staging', `${stagingMsgBase}/api/health`),
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
console.log('\n\x1b[1m── API Health ──────────────────────────────────────────────\x1b[0m');
|
|
157
|
+
for (const h of [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth]) {
|
|
158
|
+
if (h.ok) {
|
|
159
|
+
console.log(` ${OK} ${h.label}`);
|
|
160
|
+
} else {
|
|
161
|
+
const detail = h.err ?? `HTTP ${h.status}`;
|
|
162
|
+
console.log(` ${ERR} ${h.label.padEnd(20)} ${detail}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── 3. Hangfire failed jobs ───────────────────────────────────────────────
|
|
167
|
+
console.log('\n\x1b[1m── Hangfire ────────────────────────────────────────────────\x1b[0m');
|
|
168
|
+
try {
|
|
169
|
+
const jobs = await get('/DatabaseTool/FailedJobs');
|
|
170
|
+
if (!jobs?.length) {
|
|
171
|
+
console.log(` ${OK} 0 failed jobs`);
|
|
172
|
+
} else {
|
|
173
|
+
const sym = jobs.length >= 20 ? ERR : jobs.length >= 5 ? WARN : WARN;
|
|
174
|
+
console.log(` ${sym} ${jobs.length} failed job(s)`);
|
|
175
|
+
for (const j of jobs.slice(0, 5)) {
|
|
176
|
+
const when = j.failedAt ? ago(j.failedAt) : '?';
|
|
177
|
+
const name = [j.typeName, j.methodName].filter(Boolean).join('.');
|
|
178
|
+
const msg = j.exceptionMessage?.split('\n')[0]?.slice(0, 80) ?? j.reason?.slice(0, 80) ?? '';
|
|
179
|
+
console.log(` ${WARN} ${name.padEnd(35)} ${when.padEnd(10)} ${msg}`);
|
|
180
|
+
}
|
|
181
|
+
if (jobs.length > 5) console.log(` ... and ${jobs.length - 5} more`);
|
|
182
|
+
console.log(' Open /hangfire to inspect and retry.');
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.log(` ${WARN} Could not load Hangfire jobs: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── 4. Active alerts (needs-attention via messaging API) ──────────────────
|
|
189
|
+
console.log('\n\x1b[1m── Alerts ──────────────────────────────────────────────────\x1b[0m');
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(`${prodMsgBase}/SuperAdmin/needs-attention`, {
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
194
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
signal: AbortSignal.timeout(15000),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
console.log(` ${WARN} Could not reach alerts endpoint (HTTP ${res.status})`);
|
|
202
|
+
} else {
|
|
203
|
+
const items = await res.json();
|
|
204
|
+
const alertItems = Array.isArray(items)
|
|
205
|
+
? items.filter(i => i.type !== 'hangfire_failed') // shown above
|
|
206
|
+
: [];
|
|
207
|
+
if (!alertItems.length) {
|
|
208
|
+
console.log(` ${OK} Nothing needs attention`);
|
|
209
|
+
} else {
|
|
210
|
+
for (const item of alertItems) {
|
|
211
|
+
console.log(` ${severityPrefix(item.severity)}${item.title}`);
|
|
212
|
+
if (item.recommendedFix) {
|
|
213
|
+
console.log(` \x1b[2m${item.recommendedFix.slice(0, 100)}\x1b[0m`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {
|
|
219
|
+
console.log(` ${WARN} Alerts unavailable: ${e.message}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ── 5. Recent errors + perf + AI routing (last 1h) ───────────────────────
|
|
223
|
+
console.log('\n\x1b[1m── Recent Issues (last 1h) ─────────────────────────────────\x1b[0m');
|
|
224
|
+
try {
|
|
225
|
+
const stagingMsgBaseLocal = cfg.envs?.staging?.messagingApiBase ?? ENVS.staging.messagingApiBase;
|
|
226
|
+
const from1h = new Date(Date.now() - 3600000).toISOString();
|
|
227
|
+
|
|
228
|
+
const [errRes, perfRes] = await Promise.all([
|
|
229
|
+
fetch(`${stagingMsgBaseLocal}/superadmin/logs/search?q=level%3AERROR&from=${from1h}&take=5`, {
|
|
230
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
231
|
+
signal: AbortSignal.timeout(12000),
|
|
232
|
+
}),
|
|
233
|
+
fetch(`${stagingMsgBaseLocal}/superadmin/logs/search?q=WF-AI-ROUTE+OR+PERF-SLOW&from=${from1h}&take=20`, {
|
|
234
|
+
headers: { Authorization: `Bearer ${creds.velaroToken}`, 'X-Internal-SiteId': String(creds.siteId) },
|
|
235
|
+
signal: AbortSignal.timeout(12000),
|
|
236
|
+
}),
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
const errors = errRes.ok ? (await errRes.json().catch(() => [])) : [];
|
|
240
|
+
const perfLogs = perfRes.ok ? (await perfRes.json().catch(() => [])) : [];
|
|
241
|
+
|
|
242
|
+
const perfCount = perfLogs.filter(e => (e.Message ?? e.message ?? '').includes('PERF-SLOW')).length;
|
|
243
|
+
const routeCount = perfLogs.filter(e => (e.Message ?? e.message ?? '').includes('WF-AI-ROUTE')).length;
|
|
244
|
+
|
|
245
|
+
const errSym = errors.length ? ERR : OK;
|
|
246
|
+
const perfSym = perfCount ? WARN : OK;
|
|
247
|
+
const routeSym = routeCount ? '\x1b[36m🤖\x1b[0m' : OK;
|
|
248
|
+
|
|
249
|
+
console.log(` ${errSym} Errors last 1h: ${errors.length}`);
|
|
250
|
+
if (errors.length) {
|
|
251
|
+
for (const e of errors.slice(0, 3)) {
|
|
252
|
+
const msg = (e.Message ?? e.message ?? '').slice(0, 90);
|
|
253
|
+
console.log(` \x1b[31m${msg}\x1b[0m`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.log(` ${perfSym} Slow responses (PERF): ${perfCount}`);
|
|
257
|
+
console.log(` ${routeSym} AI routing events: ${routeCount}`);
|
|
258
|
+
if (errors.length || perfCount || routeCount) {
|
|
259
|
+
console.log(`\n Run \x1b[36mvelaro logs diagnose\x1b[0m for full detail.`);
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
console.log(` ${WARN} Could not load recent issues: ${e.message}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('\n──────────────────────────────────────────────────────────────\n');
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro payment-recovery ─────────────────────────────────────────────────────
|
|
5
|
+
// CLI surface for "Payment Recovery" -- text a contact a payment link for an
|
|
6
|
+
// outstanding balance, optionally with recurring reminders until paid or marked
|
|
7
|
+
// paid, view send history, and manage the per-site config. First-party AR
|
|
8
|
+
// reminders ONLY -- never debt collection (docs/architecture/payment-recovery-
|
|
9
|
+
// and-billing-webhooks.md). siteId is applied server-side from the vmsg-siteId
|
|
10
|
+
// header (see cli/lib/api.js messagingRequest) -- never a CLI arg.
|
|
11
|
+
//
|
|
12
|
+
// Backend contract: server/Velaro.Messaging/Controllers/PaymentRecoveryController.cs
|
|
13
|
+
// POST PaymentRecovery/Contacts/{contactId}/send { amountCents, currency?, description?, startReminderChain, cadenceHours?, maxReminders? } -> { conversationBaseId, workflowJobId, paymentLinkUrl }
|
|
14
|
+
// POST PaymentRecovery/Conversations/{conversationBaseId}/mark-paid -> { success }
|
|
15
|
+
// GET PaymentRecovery/Contacts/{contactId}/sends -> history[]
|
|
16
|
+
// GET PaymentRecovery/Config -> config | 404
|
|
17
|
+
// POST PaymentRecovery/Config -> create (409 if one already exists)
|
|
18
|
+
// PUT PaymentRecovery/Config/{id} -> apply-if-present update
|
|
19
|
+
|
|
20
|
+
const BOLD = '\x1b[1m';
|
|
21
|
+
const DIM = '\x1b[2m';
|
|
22
|
+
const GRN = '\x1b[32m';
|
|
23
|
+
const RST = '\x1b[0m';
|
|
24
|
+
|
|
25
|
+
function fmtDate(d) {
|
|
26
|
+
return d ? new Date(d).toISOString().replace('T', ' ').slice(0, 19) : '-';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function sendHandler(argv) {
|
|
30
|
+
const result = await messagingPost(`/PaymentRecovery/Contacts/${argv.contactId}/send`, {
|
|
31
|
+
amountCents: argv.amountCents,
|
|
32
|
+
currency: argv.currency,
|
|
33
|
+
description: argv.description,
|
|
34
|
+
startReminderChain: !!argv.remind,
|
|
35
|
+
cadenceHours: argv.cadenceHours,
|
|
36
|
+
maxReminders: argv.maxReminders,
|
|
37
|
+
});
|
|
38
|
+
console.log(`\n${GRN}✓${RST} Payment reminder sent to contact ${argv.contactId}.`);
|
|
39
|
+
console.log(` Conversation: ${result.conversationBaseId}`);
|
|
40
|
+
console.log(` Payment link: ${result.paymentLinkUrl}`);
|
|
41
|
+
if (result.workflowJobId) console.log(` Reminder job: ${result.workflowJobId}`);
|
|
42
|
+
console.log('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function markPaidHandler(argv) {
|
|
46
|
+
await messagingPost(`/PaymentRecovery/Conversations/${argv.conversationId}/mark-paid`, {});
|
|
47
|
+
console.log(`\n${GRN}✓${RST} Conversation ${argv.conversationId} marked paid. Reminders stopped.\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function historyHandler(argv) {
|
|
51
|
+
const history = await messagingGet(`/PaymentRecovery/Contacts/${argv.contactId}/sends`);
|
|
52
|
+
if (!Array.isArray(history) || !history.length) {
|
|
53
|
+
console.log(`\n No Payment Recovery sends found for contact ${argv.contactId}.\n`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(`\n${BOLD}Payment Recovery send history -- contact ${argv.contactId}${RST}\n`);
|
|
57
|
+
for (const h of history) {
|
|
58
|
+
const kind = h.isInitialSend ? `${GRN}initial${RST}` : 'reminder';
|
|
59
|
+
const chain = h.workflowJobId ? ` ${DIM}chain:${h.workflowJobId}${RST}` : '';
|
|
60
|
+
console.log(` ${fmtDate(h.sentAtUtc)} conv:${h.conversationBaseId ?? '-'} ${kind}${chain}`);
|
|
61
|
+
}
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function configGetHandler() {
|
|
66
|
+
let config;
|
|
67
|
+
try {
|
|
68
|
+
config = await messagingGet('/PaymentRecovery/Config');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (String(err.message).includes(' 404')) {
|
|
71
|
+
console.log(`\n No Payment Recovery config exists for this site yet. Run "velaro payment-recovery config set" to create one.\n`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
console.log(`\n${BOLD}Payment Recovery config${RST} (id ${config.id})`);
|
|
77
|
+
console.log(` Enabled: ${config.isEnabled ? `${GRN}yes${RST}` : 'no'}`);
|
|
78
|
+
console.log(` Default cadence: ${config.defaultCadenceHours}h`);
|
|
79
|
+
console.log(` Default max reminders:${config.defaultMaxReminders}`);
|
|
80
|
+
console.log(` Message template: ${config.defaultMessageTemplate ?? '-'}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function configSetHandler(argv) {
|
|
85
|
+
const body = {
|
|
86
|
+
isEnabled: argv.enabled,
|
|
87
|
+
defaultCadenceHours: argv.cadenceHours,
|
|
88
|
+
defaultMaxReminders: argv.maxReminders,
|
|
89
|
+
defaultMessageTemplate: argv.template,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let existing = null;
|
|
93
|
+
try {
|
|
94
|
+
existing = await messagingGet('/PaymentRecovery/Config');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (!String(err.message).includes(' 404')) throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const config = existing
|
|
100
|
+
? await messagingPut(`/PaymentRecovery/Config/${existing.id}`, body)
|
|
101
|
+
: await messagingPost('/PaymentRecovery/Config', body);
|
|
102
|
+
|
|
103
|
+
console.log(`\n${GRN}✓${RST} Payment Recovery config ${existing ? 'updated' : 'created'} (id ${config.id}).\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sendCommand = {
|
|
107
|
+
command: 'send <contactId>',
|
|
108
|
+
describe: 'Text a contact a payment link for an outstanding balance, optionally with recurring reminders',
|
|
109
|
+
builder: (y) => y
|
|
110
|
+
.positional('contactId', { describe: 'Contact ID to send the reminder to', type: 'number' })
|
|
111
|
+
.option('amount-cents', { describe: 'Amount owed, in cents (e.g. 5000 = $50.00)', type: 'number', demandOption: true })
|
|
112
|
+
.option('currency', { describe: 'ISO currency code (default usd)', type: 'string' })
|
|
113
|
+
.option('description', { describe: 'What this payment is for', type: 'string' })
|
|
114
|
+
.option('remind', { describe: 'Schedule recurring reminders until paid', type: 'boolean', default: false })
|
|
115
|
+
.option('cadence-hours', { describe: 'Hours between reminders, 1-168 (default: site config default)', type: 'number' })
|
|
116
|
+
.option('max-reminders', { describe: 'Max reminder texts, 1-10 (default: site config default)', type: 'number' }),
|
|
117
|
+
handler: runCommand(sendHandler),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const markPaidCommand = {
|
|
121
|
+
command: 'mark-paid <conversationId>',
|
|
122
|
+
describe: 'Mark a conversation\'s balance as paid -- stops any further reminder texts',
|
|
123
|
+
builder: (y) => y
|
|
124
|
+
.positional('conversationId', { describe: 'Conversation base ID', type: 'number' }),
|
|
125
|
+
handler: runCommand(markPaidHandler),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const historyCommand = {
|
|
129
|
+
command: 'history <contactId>',
|
|
130
|
+
describe: 'Show Payment Recovery send history for a contact',
|
|
131
|
+
builder: (y) => y
|
|
132
|
+
.positional('contactId', { describe: 'Contact ID', type: 'number' }),
|
|
133
|
+
handler: runCommand(historyHandler),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const configCommand = {
|
|
137
|
+
command: 'config <subcommand>',
|
|
138
|
+
describe: 'Get or set this site\'s Payment Recovery config',
|
|
139
|
+
builder: (y) => y
|
|
140
|
+
.command({
|
|
141
|
+
command: 'get',
|
|
142
|
+
describe: 'Show this site\'s Payment Recovery config',
|
|
143
|
+
handler: runCommand(configGetHandler),
|
|
144
|
+
})
|
|
145
|
+
.command({
|
|
146
|
+
command: 'set',
|
|
147
|
+
describe: 'Create or update this site\'s Payment Recovery config (apply-if-present -- omit a flag to leave it unchanged)',
|
|
148
|
+
builder: (y2) => y2
|
|
149
|
+
.option('enabled', { describe: 'Enable/disable Payment Recovery site-wide', type: 'boolean' })
|
|
150
|
+
.option('cadence-hours', { describe: 'Default hours between reminders, 1-168', type: 'number' })
|
|
151
|
+
.option('max-reminders', { describe: 'Default max reminder texts, 1-10', type: 'number' })
|
|
152
|
+
.option('template', { describe: 'Default reminder message text, supports <<paymentLinkUrl>>', type: 'string' }),
|
|
153
|
+
handler: runCommand(configSetHandler),
|
|
154
|
+
})
|
|
155
|
+
.demandCommand(1, 'Specify a config subcommand: get | set'),
|
|
156
|
+
handler: () => {},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const paymentRecoveryCommand = {
|
|
160
|
+
command: 'payment-recovery <subcommand>',
|
|
161
|
+
describe: 'Payment Recovery -- send payment reminders, manage reminder chains, and site config',
|
|
162
|
+
builder: (y) => y
|
|
163
|
+
.command(sendCommand)
|
|
164
|
+
.command(markPaidCommand)
|
|
165
|
+
.command(historyCommand)
|
|
166
|
+
.command(configCommand)
|
|
167
|
+
.demandCommand(1, 'Specify a payment-recovery subcommand: send | mark-paid | history | config')
|
|
168
|
+
.strict(),
|
|
169
|
+
handler: () => {},
|
|
170
|
+
};
|