@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,62 @@
|
|
|
1
|
+
import { request } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// ── velaro calendly ───────────────────────────────────────────────────────────
|
|
5
|
+
// Superadmin commands to configure Calendly for any customer site.
|
|
6
|
+
// Requires a Velaro admin JWT (site 1032).
|
|
7
|
+
|
|
8
|
+
export const calendlyCommand = {
|
|
9
|
+
command: 'calendly <subcommand>',
|
|
10
|
+
describe: 'Manage Calendly integration for any site (Velaro admin only)',
|
|
11
|
+
builder: yargs => yargs
|
|
12
|
+
.command({
|
|
13
|
+
command: 'status',
|
|
14
|
+
describe: 'Show Calendly config for a site',
|
|
15
|
+
builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
|
|
16
|
+
handler: runCommand(async argv => {
|
|
17
|
+
const data = await request('GET', `/Calendly/superadmin/status?siteId=${argv.site}`);
|
|
18
|
+
if (!data.configured) {
|
|
19
|
+
console.log(`\nSite ${argv.site}: Calendly NOT configured.\n`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`\nSite ${argv.site}: Calendly configured ✓`);
|
|
23
|
+
console.log(` Account: ${data.userName}`);
|
|
24
|
+
console.log(` Default type: ${data.defaultEventTypeName || '(none)'}`);
|
|
25
|
+
console.log(` Single-use: ${data.useSingleUseLinks ? 'yes' : 'no'}\n`);
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
28
|
+
.command({
|
|
29
|
+
command: 'configure',
|
|
30
|
+
describe: 'Set up or update Calendly for a site',
|
|
31
|
+
builder: y => y
|
|
32
|
+
.option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
|
|
33
|
+
.option('token', { type: 'string', demandOption: true, describe: 'Calendly Personal Access Token' })
|
|
34
|
+
.option('event-uri', { type: 'string', demandOption: false, describe: 'Default event type URI' })
|
|
35
|
+
.option('event-name', { type: 'string', demandOption: false, describe: 'Default event type display name' })
|
|
36
|
+
.option('label', { type: 'string', demandOption: false, describe: 'Display label (e.g. "Acme Demo Booking")' })
|
|
37
|
+
.option('single-use', { type: 'boolean', default: true, describe: 'Use single-use scheduling links' }),
|
|
38
|
+
handler: runCommand(async argv => {
|
|
39
|
+
const data = await request('POST', `/Calendly/superadmin/config?siteId=${argv.site}`, {
|
|
40
|
+
accessToken: argv.token,
|
|
41
|
+
displayName: argv.label || '',
|
|
42
|
+
defaultEventTypeUri: argv['event-uri'] || '',
|
|
43
|
+
defaultEventTypeName: argv['event-name'] || '',
|
|
44
|
+
useSingleUseLinks: argv['single-use'],
|
|
45
|
+
});
|
|
46
|
+
if (!data.success) throw new Error(data.error || 'Configure failed');
|
|
47
|
+
console.log(`\n✓ Calendly configured for site ${argv.site}`);
|
|
48
|
+
console.log(` Account: ${data.userName}`);
|
|
49
|
+
console.log(` Org: ${data.organizationUri}\n`);
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
.command({
|
|
53
|
+
command: 'disconnect',
|
|
54
|
+
describe: 'Remove Calendly config from a site',
|
|
55
|
+
builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
|
|
56
|
+
handler: runCommand(async argv => {
|
|
57
|
+
await request('DELETE', `/Calendly/superadmin/config?siteId=${argv.site}`);
|
|
58
|
+
console.log(`\n✓ Calendly disconnected from site ${argv.site}\n`);
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
.demandCommand(1, 'Specify a subcommand: status | configure | disconnect'),
|
|
62
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { messagingGet } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro callback report -----------------------------------------------------
|
|
5
|
+
// CLI surface for the Callback Scheduling supervisor report (Stage 5 UI /
|
|
6
|
+
// Stage 8 MCP tool `callback_scheduling_report`). Endpoint lives in
|
|
7
|
+
// velaro-messaging at GET CallbackScheduling/report and applies the caller's
|
|
8
|
+
// siteId from the vmsg-siteId header (see api.js messagingRequest) -- never
|
|
9
|
+
// from CLI args, same IDOR pattern as every other messaging* command here.
|
|
10
|
+
//
|
|
11
|
+
// Backend contract: server/Velaro.Messaging/Controllers/CallbackSchedulingReportController.cs
|
|
12
|
+
// Response shape: { summary: { requested, completed, missed, overdue, rescheduled,
|
|
13
|
+
// byFulfillmentMode }, total, page, pageSize, items[] }. The endpoint never throws --
|
|
14
|
+
// a query failure degrades to an empty/zeroed report, so this command has nothing
|
|
15
|
+
// special to do for that case beyond printing whatever comes back.
|
|
16
|
+
|
|
17
|
+
const DIM = '\x1b[2m';
|
|
18
|
+
const RED = '\x1b[31m';
|
|
19
|
+
const YEL = '\x1b[33m';
|
|
20
|
+
const GRN = '\x1b[32m';
|
|
21
|
+
const BOLD = '\x1b[1m';
|
|
22
|
+
const RST = '\x1b[0m';
|
|
23
|
+
|
|
24
|
+
function statusColor(status) {
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 'Fulfilled': return GRN;
|
|
27
|
+
case 'Failed': return RED;
|
|
28
|
+
case 'Pending':
|
|
29
|
+
case 'Scheduled': return YEL;
|
|
30
|
+
default: return RST;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmtDate(d) {
|
|
35
|
+
return d ? new Date(d).toISOString().replace('T', ' ').slice(0, 19) : '-';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printSummary(summary) {
|
|
39
|
+
console.log(`\n${BOLD}Callback Scheduling Report${RST}`);
|
|
40
|
+
console.log(` Requested: ${summary?.requested ?? 0}`);
|
|
41
|
+
console.log(` Completed: ${GRN}${summary?.completed ?? 0}${RST}`);
|
|
42
|
+
console.log(` Missed: ${RED}${summary?.missed ?? 0}${RST}`);
|
|
43
|
+
console.log(` Overdue: ${YEL}${summary?.overdue ?? 0}${RST}`);
|
|
44
|
+
console.log(` Rescheduled: ${summary?.rescheduled ?? 0}`);
|
|
45
|
+
|
|
46
|
+
const byMode = summary?.byFulfillmentMode ?? {};
|
|
47
|
+
const modes = Object.keys(byMode);
|
|
48
|
+
if (modes.length) {
|
|
49
|
+
console.log(` By fulfillment mode:`);
|
|
50
|
+
for (const mode of modes) console.log(` ${mode.padEnd(16)} ${byMode[mode]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function printRows(data) {
|
|
55
|
+
const items = data?.items ?? [];
|
|
56
|
+
if (!items.length) {
|
|
57
|
+
console.log('\n No callback requests match this filter.\n');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`\n ${data.items.length} of ${data.total} requests (page ${data.page}, pageSize ${data.pageSize})\n`);
|
|
62
|
+
|
|
63
|
+
const COL = [8, 12, 16, 12, 20, 20, 18];
|
|
64
|
+
const header = ['Id', 'Source', 'Mode', 'Status', 'Requested For', 'Created', 'Caller'];
|
|
65
|
+
console.log(' ' + header.map((h, i) => h.padEnd(COL[i])).join(''));
|
|
66
|
+
console.log(' ' + '-'.repeat(COL.reduce((a, b) => a + b, 0)));
|
|
67
|
+
|
|
68
|
+
for (const r of items) {
|
|
69
|
+
const color = statusColor(r.status);
|
|
70
|
+
const caller = [r.callerName, r.callerPhone].filter(Boolean).join(' / ') || '-';
|
|
71
|
+
const padded = [
|
|
72
|
+
String(r.id ?? '-').padEnd(COL[0]),
|
|
73
|
+
String(r.source ?? '-').padEnd(COL[1]),
|
|
74
|
+
String(r.fulfillmentMode ?? '-').padEnd(COL[2]),
|
|
75
|
+
`${color}${String(r.status ?? '-').padEnd(COL[3])}${RST}`,
|
|
76
|
+
fmtDate(r.requestedForUtc).padEnd(COL[4]),
|
|
77
|
+
fmtDate(r.createdAtUtc).padEnd(COL[5]),
|
|
78
|
+
caller,
|
|
79
|
+
];
|
|
80
|
+
console.log(' ' + padded.join(''));
|
|
81
|
+
if (r.failureReason) console.log(` ${DIM}Failure: ${r.failureReason}${RST}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (data.total > data.page * data.pageSize) {
|
|
85
|
+
console.log(`\n ${DIM}More results -- use --page ${data.page + 1}${RST}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function reportHandler(argv) {
|
|
92
|
+
const qs = new URLSearchParams();
|
|
93
|
+
if (argv.status) qs.set('status', argv.status);
|
|
94
|
+
if (argv.mode) qs.set('fulfillmentMode', argv.mode);
|
|
95
|
+
if (argv.days) qs.set('dateRangeDays', String(argv.days));
|
|
96
|
+
qs.set('page', String(Math.max(1, argv.page ?? 1)));
|
|
97
|
+
qs.set('pageSize', String(Math.min(200, argv.pageSize ?? 50)));
|
|
98
|
+
|
|
99
|
+
const data = await messagingGet(`/CallbackScheduling/report?${qs.toString()}`);
|
|
100
|
+
|
|
101
|
+
printSummary(data?.summary);
|
|
102
|
+
printRows(data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reportCommand = {
|
|
106
|
+
command: 'report',
|
|
107
|
+
describe: 'Callback Scheduling supervisor report -- summary counts + drill-down rows',
|
|
108
|
+
builder: (y) => y
|
|
109
|
+
.option('status', { describe: 'Filter: Pending, Scheduled, Fulfilled, Failed', type: 'string' })
|
|
110
|
+
.option('mode', { describe: 'Filter by fulfillment mode (e.g. AiCallback, AgentCallback)', type: 'string' })
|
|
111
|
+
.option('days', { describe: 'Date range lookback in days, 1-365 (default 30)', type: 'number', default: 30 })
|
|
112
|
+
.option('page', { describe: 'Page number', type: 'number', default: 1 })
|
|
113
|
+
.option('pageSize', { describe: 'Results per page, max 200 (default 50)', type: 'number', default: 50 }),
|
|
114
|
+
handler: runCommand(reportHandler),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const callbackCommand = {
|
|
118
|
+
command: 'callback <subcommand>',
|
|
119
|
+
describe: 'Callback Scheduling reporting',
|
|
120
|
+
builder: (y) => y
|
|
121
|
+
.command(reportCommand)
|
|
122
|
+
.demandCommand(1, 'Specify a callback subcommand.')
|
|
123
|
+
.strict(),
|
|
124
|
+
handler: () => {},
|
|
125
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro callrail - CallRail call-source marketing attribution
|
|
3
|
+
*
|
|
4
|
+
* velaro callrail attributions List recent call attribution records
|
|
5
|
+
* velaro callrail logs View integration activity logs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { request } from '../api.js';
|
|
9
|
+
|
|
10
|
+
const GRN = '\x1b[32m';
|
|
11
|
+
const RED = '\x1b[31m';
|
|
12
|
+
const CYN = '\x1b[36m';
|
|
13
|
+
const DIM = '\x1b[2m';
|
|
14
|
+
const BOLD = '\x1b[1m';
|
|
15
|
+
const RST = '\x1b[0m';
|
|
16
|
+
|
|
17
|
+
// -- attributions ----------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
async function attributionsHandler(argv) {
|
|
20
|
+
const limit = Math.min(100, argv.limit ?? 25);
|
|
21
|
+
const rows = await request('GET', '/CallRail/attributions');
|
|
22
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
23
|
+
console.log('\nNo call attribution records found. Connect CallRail under Integrations, then add a webhook in your CallRail account.\n');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const trimmed = rows.slice(0, limit);
|
|
27
|
+
console.log(`\n${BOLD}${trimmed.length} of ${rows.length} call attribution record(s)${RST}\n`);
|
|
28
|
+
for (const r of trimmed) {
|
|
29
|
+
const provider = r.ProviderSlug ?? r.Provider ?? 'CallRail';
|
|
30
|
+
const ts = (r.ReceivedAtUtc ?? '').slice(0, 19).replace('T', ' ');
|
|
31
|
+
console.log(` ${DIM}${ts}${RST} ${CYN}${r.PhoneNumberNormalized}${RST} [${provider}]`);
|
|
32
|
+
console.log(` ${r.Source ?? '(unknown source)'} / ${r.Campaign ?? '(no campaign)'} / ${r.Medium ?? '(no medium)'}`);
|
|
33
|
+
if (r.RecordingUrl) console.log(` ${DIM}recording: ${r.RecordingUrl}${RST}`);
|
|
34
|
+
}
|
|
35
|
+
console.log('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// -- logs --------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
async function logsHandler(argv) {
|
|
41
|
+
const params = new URLSearchParams();
|
|
42
|
+
if (argv.severity) params.set('severity', argv.severity);
|
|
43
|
+
if (argv.search) params.set('search', argv.search);
|
|
44
|
+
params.set('pageSize', String(Math.min(100, argv.pageSize ?? 25)));
|
|
45
|
+
const data = await request('GET', `/Integration/Logs/CallRail?${params}`);
|
|
46
|
+
const items = data?.items ?? [];
|
|
47
|
+
if (items.length === 0) {
|
|
48
|
+
console.log('\nNo CallRail activity log entries found.\n');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(`\n${BOLD}${items.length} of ${data.total} CallRail activity entries${RST}\n`);
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const ts = new Date(item.createdAt).toISOString().replace('T', ' ').slice(0, 19);
|
|
54
|
+
const sev = item.severity === 'Error' ? `${RED}${item.severity}${RST} ` :
|
|
55
|
+
item.severity === 'Warning' ? `${'\x1b[33m'}${item.severity}${RST}` :
|
|
56
|
+
`${DIM}${item.severity}${RST} `;
|
|
57
|
+
console.log(` ${DIM}${ts}${RST} ${sev} [${item.category ?? '?'}] ${item.message ?? ''}`);
|
|
58
|
+
if (item.additionalInfo) console.log(` ${DIM}${item.additionalInfo}${RST}`);
|
|
59
|
+
}
|
|
60
|
+
if (data.total > items.length) console.log(`\n ${DIM}${data.total - items.length} more, use --page-size to see more${RST}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// -- command export ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export const callrailCommand = {
|
|
67
|
+
command: 'callrail <subcommand>',
|
|
68
|
+
describe: 'CallRail call-source marketing attribution',
|
|
69
|
+
builder: y => y
|
|
70
|
+
.command({
|
|
71
|
+
command: 'attributions',
|
|
72
|
+
describe: 'List recent call attribution records',
|
|
73
|
+
builder: yy => yy
|
|
74
|
+
.option('limit', { describe: 'Max results (default: 25)', type: 'number', default: 25 }),
|
|
75
|
+
handler: attributionsHandler,
|
|
76
|
+
})
|
|
77
|
+
.command({
|
|
78
|
+
command: 'logs',
|
|
79
|
+
describe: 'View CallRail integration activity logs',
|
|
80
|
+
builder: yy => yy
|
|
81
|
+
.option('severity', { describe: 'Filter: Info, Warning, Error', type: 'string' })
|
|
82
|
+
.option('search', { describe: 'Text to search for in messages', type: 'string' })
|
|
83
|
+
.option('page-size', { describe: 'Results per page (default: 25)', type: 'number', default: 25 }),
|
|
84
|
+
handler: logsHandler,
|
|
85
|
+
})
|
|
86
|
+
.demandCommand(1, 'Specify a subcommand. Try: velaro callrail --help'),
|
|
87
|
+
handler: () => {},
|
|
88
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* velaro campaigns - Email campaign reporting
|
|
3
|
+
*
|
|
4
|
+
* velaro campaigns benchmarks Your broadcast open/click rate vs. the Velaro average
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { request } from '../api.js';
|
|
8
|
+
import { runCommand } from '../run.js';
|
|
9
|
+
|
|
10
|
+
const GRN = '\x1b[32m';
|
|
11
|
+
const CYN = '\x1b[36m';
|
|
12
|
+
const DIM = '\x1b[2m';
|
|
13
|
+
const BOLD = '\x1b[1m';
|
|
14
|
+
const RST = '\x1b[0m';
|
|
15
|
+
|
|
16
|
+
// -- benchmarks ------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
async function benchmarksHandler() {
|
|
19
|
+
const r = await request('GET', '/EmailCampaigns/Benchmarks');
|
|
20
|
+
console.log(`\n${BOLD}Broadcast Email Benchmarks (last ${r.windowDays} days)${RST}\n`);
|
|
21
|
+
console.log(` You: open ${CYN}${r.yourOpenRate}%${RST} | click ${CYN}${r.yourClickRate}%${RST} (${r.yourSentCount} sent)`);
|
|
22
|
+
if (r.benchmarkAvailable) {
|
|
23
|
+
console.log(` Velaro average: open ${GRN}${r.benchmarkOpenRate}%${RST} | click ${GRN}${r.benchmarkClickRate}%${RST} (${r.benchmarkSampleSize} contributing sites)`);
|
|
24
|
+
} else {
|
|
25
|
+
console.log(` ${DIM}Velaro average not published yet — not enough participating sites.${RST}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(`\n ${DIM}${r.note}${RST}\n`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// -- command export ------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const campaignsCommand = {
|
|
33
|
+
command: 'campaigns <subcommand>',
|
|
34
|
+
describe: 'Email campaign reporting',
|
|
35
|
+
builder: y => y
|
|
36
|
+
.command({
|
|
37
|
+
command: 'benchmarks',
|
|
38
|
+
describe: 'Your broadcast email open/click rate vs. the all-Velaro-customer average',
|
|
39
|
+
handler: runCommand(benchmarksHandler),
|
|
40
|
+
})
|
|
41
|
+
.demandCommand(1, 'Specify a subcommand. Try: velaro campaigns --help')
|
|
42
|
+
.strict(),
|
|
43
|
+
handler: () => {},
|
|
44
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro case ------------------------------------------------------------
|
|
5
|
+
// CLI surface for Case Management (per-site configurable "Case Type" stages +
|
|
6
|
+
// custom fields, layered on ticketing). Backend lives in velaro-messaging:
|
|
7
|
+
// CaseTypesController.cs -> GET/POST/PUT/DELETE CaseTypes
|
|
8
|
+
// CasesController.cs -> GET Cases/board/{caseTypeId}, POST Cases, PUT Cases/{id}/stage
|
|
9
|
+
// SiteId is always applied server-side from the caller's auth context -- never
|
|
10
|
+
// passed as a CLI arg, same IDOR pattern as every other messaging* command here.
|
|
11
|
+
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
const DIM = '\x1b[2m';
|
|
14
|
+
const RST = '\x1b[0m';
|
|
15
|
+
|
|
16
|
+
async function typesHandler() {
|
|
17
|
+
const types = await messagingGet('/CaseTypes');
|
|
18
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
19
|
+
console.log('\n No case types configured for this site.\n');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`\n${BOLD}Case Types${RST}`);
|
|
23
|
+
for (const t of types) {
|
|
24
|
+
console.log(` #${t.id} ${t.name} ${DIM}(${t.stages?.length ?? 0} stages, ${t.fields?.length ?? 0} fields, prefix ${t.caseNumberPrefix})${RST}`);
|
|
25
|
+
}
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function boardHandler(argv) {
|
|
30
|
+
const data = await messagingGet(`/Cases/board/${argv.caseTypeId}`);
|
|
31
|
+
const stages = data?.caseType?.stages ?? [];
|
|
32
|
+
const cases = data?.cases ?? [];
|
|
33
|
+
console.log(`\n${BOLD}${data?.caseType?.name ?? 'Case'} Board${RST}`);
|
|
34
|
+
for (const s of stages) {
|
|
35
|
+
const inStage = cases.filter((c) => c.stage === s.key);
|
|
36
|
+
console.log(`\n ${BOLD}${s.label}${RST} (${inStage.length})`);
|
|
37
|
+
for (const c of inStage) {
|
|
38
|
+
console.log(` ${c.caseNumber} ${DIM}${c.subject ?? ''}${RST}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
console.log('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function moveHandler(argv) {
|
|
45
|
+
const result = await messagingPut(`/Cases/${argv.caseId}/stage`, { stage: argv.stage });
|
|
46
|
+
console.log(`\n Case ${result?.caseNumber ?? argv.caseId} moved to stage "${result?.stage ?? argv.stage}".\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createTypeHandler(argv) {
|
|
50
|
+
const stages = argv.stages.split(',').map((label, i) => ({
|
|
51
|
+
key: label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_'),
|
|
52
|
+
label: label.trim(),
|
|
53
|
+
}));
|
|
54
|
+
const result = await messagingPost('/CaseTypes', {
|
|
55
|
+
name: argv.name,
|
|
56
|
+
caseNumberPrefix: argv.prefix,
|
|
57
|
+
stages,
|
|
58
|
+
fields: [],
|
|
59
|
+
});
|
|
60
|
+
console.log(`\n Created case type "${result.name}" (id ${result.id}).\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const typesCommand = {
|
|
64
|
+
command: 'types',
|
|
65
|
+
describe: 'List this site\'s configured case types',
|
|
66
|
+
handler: runCommand(typesHandler),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const createTypeCommand = {
|
|
70
|
+
command: 'create-type',
|
|
71
|
+
describe: 'Create a new case type (stage pipeline)',
|
|
72
|
+
builder: (y) => y
|
|
73
|
+
.option('name', { describe: 'Case type name', type: 'string', demandOption: true })
|
|
74
|
+
.option('prefix', { describe: 'Case number prefix, e.g. UI or COL', type: 'string', default: 'CASE' })
|
|
75
|
+
.option('stages', { describe: 'Comma-separated stage labels, e.g. "New,In Review,Resolved"', type: 'string', demandOption: true }),
|
|
76
|
+
handler: runCommand(createTypeHandler),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const boardCommand = {
|
|
80
|
+
command: 'board <caseTypeId>',
|
|
81
|
+
describe: 'Show a case type\'s board grouped by stage',
|
|
82
|
+
handler: runCommand(boardHandler),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const moveCommand = {
|
|
86
|
+
command: 'move <caseId> <stage>',
|
|
87
|
+
describe: 'Move a case to a different stage (the same mutation the Kanban board drag performs)',
|
|
88
|
+
handler: runCommand(moveHandler),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const caseCommand = {
|
|
92
|
+
command: 'case <subcommand>',
|
|
93
|
+
describe: 'Case Management -- configurable case types + board',
|
|
94
|
+
builder: (y) => y
|
|
95
|
+
.command(typesCommand)
|
|
96
|
+
.command(createTypeCommand)
|
|
97
|
+
.command(boardCommand)
|
|
98
|
+
.command(moveCommand)
|
|
99
|
+
.demandCommand(1, 'Specify a case subcommand.')
|
|
100
|
+
.strict(),
|
|
101
|
+
handler: () => {},
|
|
102
|
+
};
|