@velaro/cli 1.2.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,128 @@
|
|
|
1
|
+
// velaro support — staff SupportTools / staff-scoped actions (superadmin only)
|
|
2
|
+
import { request, messagingPost, messagingGet } from '../api.js';
|
|
3
|
+
|
|
4
|
+
async function toggleFlag(args) {
|
|
5
|
+
if (!args.site || !args.flag || args.enabled === undefined) {
|
|
6
|
+
console.error('Usage: velaro support toggle-flag --site 1032 --flag EnableScraperBotIndexing --enabled true');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const enabled = args.enabled === 'true' || args.enabled === true;
|
|
10
|
+
const result = await request('POST', `SupportTools/sites/${args.site}/features/${args.flag}`, { enabled });
|
|
11
|
+
console.log(`✅ Site ${args.site}: ${result.flag} ${result.oldValue} → ${result.newValue}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function scraperCreateJob(args) {
|
|
15
|
+
if (!args.site || !args.urls) {
|
|
16
|
+
console.error('Usage: velaro support scraper-create-job --site 1032 --urls "https://example.com" [--maxPages 30] [--crawlDepth 2] [--name "job name"]');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const urls = String(args.urls).split(',').map(u => u.trim()).filter(Boolean);
|
|
20
|
+
const body = {
|
|
21
|
+
name: args.name || `staff-triggered-${Date.now()}`,
|
|
22
|
+
urls,
|
|
23
|
+
crawlDepth: args.crawlDepth ?? 1,
|
|
24
|
+
maxPages: args.maxPages ?? 25,
|
|
25
|
+
};
|
|
26
|
+
const result = await request('POST', `ScraperJobs/StaffCreate/${args.site}`, body);
|
|
27
|
+
console.log(JSON.stringify(result, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function chatQuery(args) {
|
|
31
|
+
if (!args.site || !args.aiConfigId || !args.query) {
|
|
32
|
+
console.error('Usage: velaro support chat-query --site 100150 --aiConfigId 179 --query "your test question" [--channel Web]');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const body = {
|
|
36
|
+
aiConfigId: args.aiConfigId,
|
|
37
|
+
query: args.query,
|
|
38
|
+
channel: args.channel || 'Web',
|
|
39
|
+
overrideIndexes: args.overrideIndexes ? String(args.overrideIndexes).split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
|
40
|
+
};
|
|
41
|
+
// Hits velaro-messaging's AzureIndexes/StaffChatQuery/{targetSiteId} -- staff-only override for
|
|
42
|
+
// the customer-facing test-chat (ChatQuery) endpoint, added 2026-08-21 so a Velaro staff member
|
|
43
|
+
// can adversarial-test ANY site's bot without needing that site's own login.
|
|
44
|
+
const result = await messagingPost(`AzureIndexes/StaffChatQuery/${args.site}`, body);
|
|
45
|
+
console.log(JSON.stringify(result, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function impersonate(args) {
|
|
49
|
+
if (!args.site) {
|
|
50
|
+
console.error('Usage: velaro support impersonate --site 100150 (SuperAdmin only; creates a 15-minute X-Support-Token to act as that site\'s admin, fully audit-logged)');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
// Hits SupportToolsController.CreateImpersonationSession (SuperAdmin only, 15-min token,
|
|
54
|
+
// logged to both BillingAuditLog and the app logger). Added 2026-08-28 to close the gap where
|
|
55
|
+
// the backend endpoint existed but no CLI verb wrapped it.
|
|
56
|
+
const result = await request('POST', `SupportTools/sites/${args.site}/impersonate`);
|
|
57
|
+
console.log(`Impersonation session created for site ${args.site}, expires ${result.expiresAt}`);
|
|
58
|
+
console.log(`Token: ${result.token}`);
|
|
59
|
+
console.log('Send this as header X-Support-Token on subsequent requests to act as that site\'s admin.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function sendEmail(args) {
|
|
63
|
+
if (!args.to || !args.subject || !args.html) {
|
|
64
|
+
console.error('Usage: velaro support send-email --to andrea@velaro.com --subject "..." --html "<p>...</p>" [--cc alex@velaro.com]');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
// Hits velaro-messaging's SupportTools/send-internal-email -- staff JWT or internal-site
|
|
68
|
+
// vel_live_* key, same auth pattern as every other SupportTools action, never the raw
|
|
69
|
+
// X-Velaro-Internal-Secret. Restricted server-side to @velaro.com recipients on purpose,
|
|
70
|
+
// this is for internal staff notifications, not a general external-send relay. Added
|
|
71
|
+
// 2026-09-01 to close the gap where the only working path required manually fetching a
|
|
72
|
+
// raw production secret via az.
|
|
73
|
+
const result = await messagingPost('SupportTools/send-internal-email', {
|
|
74
|
+
to: args.to,
|
|
75
|
+
cc: args.cc,
|
|
76
|
+
subject: args.subject,
|
|
77
|
+
html: args.html,
|
|
78
|
+
});
|
|
79
|
+
console.log(`✅ Sent to ${result.sentCount}/${result.of} recipient(s).`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function agentLookup(args) {
|
|
83
|
+
if (!args.id) {
|
|
84
|
+
console.error('Usage: velaro support agent-lookup --id 238');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
// Hits velaro-messaging's SuperAdmin/agents/{id}/lookup -- read-only, cross-site WorkspaceUser.Id
|
|
88
|
+
// resolution for cases where a log line (AgentPresenceFlapping WARN, AgentConnectionLog, etc.)
|
|
89
|
+
// captured only a bare numeric AgentId with no SiteId. Added 2026-08-27.
|
|
90
|
+
const result = await messagingGet(`SuperAdmin/agents/${args.id}/lookup`);
|
|
91
|
+
console.log(JSON.stringify(result, null, 2));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function supportCommand(yargs) {
|
|
95
|
+
return yargs
|
|
96
|
+
.command('impersonate', 'Start a 15-minute impersonation session for any site (SuperAdmin only, fully audit-logged to the customer\'s own activity log and a management email)', y => y
|
|
97
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Target site ID to impersonate' })
|
|
98
|
+
, a => impersonate(a))
|
|
99
|
+
.command('send-email', 'Send an ad-hoc internal email via ACS, staff-authed, no raw secret ever needed (recipients must be @velaro.com)', y => y
|
|
100
|
+
.option('to', { type: 'string', demandOption: true, desc: 'Comma-separated @velaro.com recipient(s)' })
|
|
101
|
+
.option('cc', { type: 'string', desc: 'Comma-separated @velaro.com CC recipient(s)' })
|
|
102
|
+
.option('subject', { type: 'string', demandOption: true, desc: 'Email subject' })
|
|
103
|
+
.option('html', { type: 'string', demandOption: true, desc: 'Email body as HTML' })
|
|
104
|
+
, a => sendEmail(a))
|
|
105
|
+
.command('agent-lookup', 'Resolve a bare WorkspaceUser.Id (AgentId) to its Email/DisplayName/SiteId across all sites (superadmin only, read-only)', y => y
|
|
106
|
+
.option('id', { type: 'number', demandOption: true, desc: 'WorkspaceUser.Id / AgentId to look up' })
|
|
107
|
+
, a => agentLookup(a))
|
|
108
|
+
.command('toggle-flag', 'Toggle a single legacy Subscription.cs feature flag on a site (superadmin only, allowlisted flags)', y => y
|
|
109
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Site ID' })
|
|
110
|
+
.option('flag', { type: 'string', demandOption: true, desc: 'Flag name, e.g. EnableScraperBotIndexing (must be on SupportToolsController.AllowedFeatureFlags)' })
|
|
111
|
+
.option('enabled', { type: 'string', demandOption: true, desc: 'true or false' })
|
|
112
|
+
, a => toggleFlag(a))
|
|
113
|
+
.command('scraper-create-job', 'Trigger a scraper job on behalf of any site (superadmin only — no need to be logged in as that site\'s own user)', y => y
|
|
114
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Target site ID' })
|
|
115
|
+
.option('urls', { type: 'string', demandOption: true, desc: 'Comma-separated URLs to crawl' })
|
|
116
|
+
.option('maxPages', { type: 'number', desc: 'Max pages for this job (default 25, still capped by the site\'s own plan limits)' })
|
|
117
|
+
.option('crawlDepth', { type: 'number', desc: 'Crawl depth (default 1)' })
|
|
118
|
+
.option('name', { type: 'string', desc: 'Job name' })
|
|
119
|
+
, a => scraperCreateJob(a))
|
|
120
|
+
.command('chat-query', 'Run a staff-only test-chat query against ANY site\'s AI config (superadmin only — no need to be logged in as that site\'s own user)', y => y
|
|
121
|
+
.option('site', { type: 'number', demandOption: true, desc: 'Target site ID' })
|
|
122
|
+
.option('aiConfigId', { type: 'number', demandOption: true, desc: 'AI Configuration ID on the target site' })
|
|
123
|
+
.option('query', { type: 'string', demandOption: true, desc: 'The test question to send' })
|
|
124
|
+
.option('channel', { type: 'string', desc: 'Conversation source, e.g. Web, TwilioSms (default Web)' })
|
|
125
|
+
.option('overrideIndexes', { type: 'string', desc: 'Comma-separated index names to override the config\'s own IndexName for this query' })
|
|
126
|
+
, a => chatQuery(a))
|
|
127
|
+
.demandCommand(1, 'Specify a subcommand: impersonate | agent-lookup | toggle-flag | scraper-create-job | chat-query | send-email');
|
|
128
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// velaro survey — list/create/update/delete surveys, manage version history, list
|
|
2
|
+
// submissions, and manage the Phase 1 standalone hosted share link (Surveys/* and
|
|
3
|
+
// SurveySubmissions/* on velaro-messaging). Site isolation is enforced server-side by
|
|
4
|
+
// SurveysController's/SurveySubmissionsController's [Authorize]-scoped SiteId
|
|
5
|
+
// (ClientControllerBase) — never passed by the client.
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { messagingGet, messagingPost, messagingDel, messagingRequest } from '../api.js';
|
|
8
|
+
import { runCommand } from '../run.js';
|
|
9
|
+
|
|
10
|
+
const get = messagingGet;
|
|
11
|
+
const post = messagingPost;
|
|
12
|
+
const del = messagingDel;
|
|
13
|
+
|
|
14
|
+
export const surveyCommand = {
|
|
15
|
+
command: 'survey <subcommand>',
|
|
16
|
+
describe: 'Manage surveys — list, get, upsert, delete, version history, submissions, and standalone hosted share links',
|
|
17
|
+
builder: (yargs) =>
|
|
18
|
+
yargs
|
|
19
|
+
.command(surveyListCommand)
|
|
20
|
+
.command(surveyGetCommand)
|
|
21
|
+
.command(surveyUpsertCommand)
|
|
22
|
+
.command(surveyDeleteCommand)
|
|
23
|
+
.command(surveyVersionsCommand)
|
|
24
|
+
.command(surveyVersionGetCommand)
|
|
25
|
+
.command(surveyVersionRestoreCommand)
|
|
26
|
+
.command(surveyStandaloneLinkEnableCommand)
|
|
27
|
+
.command(surveyStandaloneLinkDisableCommand)
|
|
28
|
+
.command(surveyMissedChatSubmissionsCommand)
|
|
29
|
+
.command(surveyStandaloneSubmissionsCommand)
|
|
30
|
+
.demandCommand(1, 'Specify a subcommand: list, get, upsert, delete, versions, version-get, version-restore, standalone-link-enable, standalone-link-disable, missed-chat-submissions, standalone-submissions'),
|
|
31
|
+
handler: () => {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const surveyListCommand = {
|
|
37
|
+
command: 'list',
|
|
38
|
+
describe: 'List all surveys on the current site',
|
|
39
|
+
handler: runCommand(async () => {
|
|
40
|
+
const surveys = (await get('/Surveys/list')) ?? [];
|
|
41
|
+
if (!surveys.length) { console.log('No surveys found.'); return; }
|
|
42
|
+
|
|
43
|
+
const idW = Math.max(2, ...surveys.map((s) => String(s.id).length));
|
|
44
|
+
console.log(`\n${'id'.padStart(idW)} name`);
|
|
45
|
+
console.log(`${'-'.repeat(idW)} ----`);
|
|
46
|
+
for (const s of surveys) {
|
|
47
|
+
const linkNote = s.allowStandaloneAccess ? ` [standalone link active — slug: ${s.publicSlug}]` : '';
|
|
48
|
+
console.log(`${String(s.id).padStart(idW)} ${s.name ?? '(unnamed)'}${linkNote}`);
|
|
49
|
+
}
|
|
50
|
+
console.log(`\n${surveys.length} survey(s)`);
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ── get ──────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const surveyGetCommand = {
|
|
57
|
+
command: 'get <id>',
|
|
58
|
+
describe: 'Get one survey by ID with its full shape, including all questions. There is no dedicated single-survey backend endpoint — this filters "survey list", which already includes each survey\'s full Questions array. Use this to fetch a template before "survey upsert".',
|
|
59
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Survey ID' }),
|
|
60
|
+
handler: runCommand(async (argv) => {
|
|
61
|
+
const surveys = (await get('/Surveys/list')) ?? [];
|
|
62
|
+
const survey = surveys.find((s) => s.id === argv.id);
|
|
63
|
+
if (!survey) throw new Error(`Survey ${argv.id} not found`);
|
|
64
|
+
console.log(JSON.stringify(survey, null, 2));
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ── upsert ───────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const surveyUpsertCommand = {
|
|
71
|
+
command: 'upsert <file>',
|
|
72
|
+
describe: 'Create (omit id or set id=0) or update (set id) a survey from a JSON file containing the full SurveyViewModel shape (id, name, questions[], ...). Use "survey get <id>" on an existing survey to fetch a starting template.',
|
|
73
|
+
builder: (y) => y.positional('file', { type: 'string', describe: 'Path to a JSON file with the survey body' }),
|
|
74
|
+
handler: runCommand(async (argv) => {
|
|
75
|
+
const body = JSON.parse(readFileSync(argv.file, 'utf8'));
|
|
76
|
+
const survey = await post('/Surveys', body);
|
|
77
|
+
console.log(`Upserted survey ${survey.id} (${survey.name ?? '(unnamed)'}).`);
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── delete ───────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const surveyDeleteCommand = {
|
|
84
|
+
command: 'delete <id>',
|
|
85
|
+
describe: 'Delete a survey by ID',
|
|
86
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Survey ID' }),
|
|
87
|
+
handler: runCommand(async (argv) => {
|
|
88
|
+
// SurveysController.DeleteAsync takes the id in the request body, not the route —
|
|
89
|
+
// messagingDel() doesn't support a body, so call messagingRequest directly.
|
|
90
|
+
await messagingRequest('DELETE', '/Surveys', { id: argv.id });
|
|
91
|
+
console.log(`Deleted survey ${argv.id}.`);
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// ── versions ─────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
const surveyVersionsCommand = {
|
|
98
|
+
command: 'versions <id>',
|
|
99
|
+
describe: 'List version history for a survey, newest first',
|
|
100
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Survey ID' }),
|
|
101
|
+
handler: runCommand(async (argv) => {
|
|
102
|
+
const versions = (await get(`/Surveys/${argv.id}/versions`)) ?? [];
|
|
103
|
+
if (!versions.length) { console.log('No version history found.'); return; }
|
|
104
|
+
for (const v of versions) {
|
|
105
|
+
console.log(`v${v.versionNumber} ${v.changeType} by ${v.changedBy || '(unknown)'} at ${v.savedAt}`);
|
|
106
|
+
}
|
|
107
|
+
console.log(`\n${versions.length} version(s)`);
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const surveyVersionGetCommand = {
|
|
112
|
+
command: 'version-get <id> <versionNumber>',
|
|
113
|
+
describe: 'Show the full snapshot for one survey version',
|
|
114
|
+
builder: (y) =>
|
|
115
|
+
y
|
|
116
|
+
.positional('id', { type: 'number', describe: 'Survey ID' })
|
|
117
|
+
.positional('versionNumber', { type: 'number', describe: 'Version number (see "survey versions")' }),
|
|
118
|
+
handler: runCommand(async (argv) => {
|
|
119
|
+
const version = await get(`/Surveys/${argv.id}/versions/${argv.versionNumber}`);
|
|
120
|
+
console.log(JSON.stringify(version, null, 2));
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const surveyVersionRestoreCommand = {
|
|
125
|
+
command: 'version-restore <id> <versionNumber>',
|
|
126
|
+
describe: 'Restore a survey to a prior version. Snapshots the current state first (as "PreRestore") so this is itself reversible.',
|
|
127
|
+
builder: (y) =>
|
|
128
|
+
y
|
|
129
|
+
.positional('id', { type: 'number', describe: 'Survey ID' })
|
|
130
|
+
.positional('versionNumber', { type: 'number', describe: 'Version number to restore (see "survey versions")' }),
|
|
131
|
+
handler: runCommand(async (argv) => {
|
|
132
|
+
const survey = await post(`/Surveys/${argv.id}/versions/${argv.versionNumber}/restore`);
|
|
133
|
+
console.log(`Restored survey ${survey.id} to version ${argv.versionNumber}.`);
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ── standalone-link-enable ──────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
const surveyStandaloneLinkEnableCommand = {
|
|
140
|
+
command: 'standalone-link-enable <id>',
|
|
141
|
+
describe: "Generate (or re-confirm) a survey's public shareable link — a hosted page anyone can fill out without a live chat conversation, for email/SMS campaigns. Idempotent.",
|
|
142
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Survey ID' }),
|
|
143
|
+
handler: runCommand(async (argv) => {
|
|
144
|
+
const result = await post(`/Surveys/${argv.id}/standalone-link`);
|
|
145
|
+
console.log(`Standalone link enabled for survey ${argv.id}.`);
|
|
146
|
+
console.log(` Slug: ${result.publicSlug}`);
|
|
147
|
+
console.log(` Active: ${result.allowStandaloneAccess}`);
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// ── standalone-link-disable ─────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
const surveyStandaloneLinkDisableCommand = {
|
|
154
|
+
command: 'standalone-link-disable <id>',
|
|
155
|
+
describe: "Revoke public access to a survey's standalone link. The slug is kept, so re-enabling later reuses the same URL instead of minting a new one.",
|
|
156
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Survey ID' }),
|
|
157
|
+
handler: runCommand(async (argv) => {
|
|
158
|
+
const result = await del(`/Surveys/${argv.id}/standalone-link`);
|
|
159
|
+
console.log(`Standalone link disabled for survey ${argv.id}.`);
|
|
160
|
+
console.log(` Slug (kept, inactive): ${result.publicSlug}`);
|
|
161
|
+
console.log(` Active: ${result.allowStandaloneAccess}`);
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// ── missed-chat-submissions ─────────────────────────────────────────────────
|
|
166
|
+
// SurveySubmissions/missed-chat -- rows from the AM-4013 Part 6 closed-launcher
|
|
167
|
+
// survey surface (Type == "missedChat", no live ConversationBase). See
|
|
168
|
+
// `velaro bundle update --show-on-closed-launcher` for turning this surface on.
|
|
169
|
+
// Both endpoints server-side filter to Answers.Count > 0 -- an empty result can
|
|
170
|
+
// mean "no submissions" OR "submissions exist but every one was empty," not
|
|
171
|
+
// necessarily that the surface has never been used.
|
|
172
|
+
const DEFAULT_SUBMISSION_LIMIT = 50;
|
|
173
|
+
|
|
174
|
+
function printSubmissions(subs, label, limit) {
|
|
175
|
+
if (!subs.length) {
|
|
176
|
+
console.log(`No ${label} submissions with at least one answer found (submissions with zero answers are excluded server-side).`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const shown = subs.slice(0, limit);
|
|
180
|
+
for (const s of shown) {
|
|
181
|
+
console.log(`\n[${s.id}] survey ${s.surveyId ?? '(unknown)'}${s.missedReason ? ` — reason: ${s.missedReason}` : ''}`);
|
|
182
|
+
for (const a of s.answers ?? []) {
|
|
183
|
+
console.log(` ${a.question ?? '(question)'}: ${a.answer ?? ''}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const omitted = subs.length - shown.length;
|
|
187
|
+
console.log(`\n${shown.length} of ${subs.length} ${label} submission(s) shown${omitted > 0 ? ` (${omitted} more omitted — pass --limit to see more)` : ''}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const surveyMissedChatSubmissionsCommand = {
|
|
191
|
+
command: 'missed-chat-submissions',
|
|
192
|
+
describe: 'List submissions collected from the closed-launcher ("we\'re unavailable") survey surface',
|
|
193
|
+
builder: (y) => y
|
|
194
|
+
.option('survey-id', { type: 'number', describe: 'Narrow to one survey ID' })
|
|
195
|
+
.option('limit', { type: 'number', default: DEFAULT_SUBMISSION_LIMIT, describe: 'Max submissions to print' }),
|
|
196
|
+
handler: runCommand(async (argv) => {
|
|
197
|
+
const qs = argv.surveyId ? `?surveyId=${argv.surveyId}` : '';
|
|
198
|
+
const subs = (await get(`/SurveySubmissions/missed-chat${qs}`)) ?? [];
|
|
199
|
+
printSubmissions(subs, 'missed-chat', argv.limit);
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ── standalone-submissions ──────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
const surveyStandaloneSubmissionsCommand = {
|
|
206
|
+
command: 'standalone-submissions',
|
|
207
|
+
describe: 'List submissions collected from a survey\'s public standalone hosted link',
|
|
208
|
+
builder: (y) => y
|
|
209
|
+
.option('survey-id', { type: 'number', describe: 'Narrow to one survey ID' })
|
|
210
|
+
.option('limit', { type: 'number', default: DEFAULT_SUBMISSION_LIMIT, describe: 'Max submissions to print' }),
|
|
211
|
+
handler: runCommand(async (argv) => {
|
|
212
|
+
const qs = argv.surveyId ? `?surveyId=${argv.surveyId}` : '';
|
|
213
|
+
const subs = (await get(`/SurveySubmissions/standalone${qs}`)) ?? [];
|
|
214
|
+
printSubmissions(subs, 'standalone', argv.limit);
|
|
215
|
+
}),
|
|
216
|
+
};
|
package/lib/commands/team.js
CHANGED
|
@@ -1,144 +1,144 @@
|
|
|
1
|
-
import { get, post } from '../api.js';
|
|
2
|
-
import { runCommand } from '../run.js';
|
|
3
|
-
|
|
4
|
-
export const teamCommand = {
|
|
5
|
-
command: 'team <subcommand>',
|
|
6
|
-
describe: 'View teams and manage widget placements (deployments)',
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
yargs
|
|
9
|
-
.command(teamListCommand)
|
|
10
|
-
.command(widgetListCommand)
|
|
11
|
-
.command(widgetRenameCommand)
|
|
12
|
-
.command(widgetAssignCommand)
|
|
13
|
-
.demandCommand(1, 'Specify a subcommand: list, widget'),
|
|
14
|
-
handler: () => {},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// ── team list ──────────────────────────────────────────────────────────────────
|
|
18
|
-
// Shows teams and their widget placements inline — the mental model is:
|
|
19
|
-
// team = who handles conversations
|
|
20
|
-
// widget = which pages route to that team
|
|
21
|
-
|
|
22
|
-
const teamListCommand = {
|
|
23
|
-
command: 'list',
|
|
24
|
-
describe: 'List teams and their widget placements',
|
|
25
|
-
handler: runCommand(async () => {
|
|
26
|
-
const [teams, deployments] = await Promise.all([
|
|
27
|
-
get('/Teams/List'),
|
|
28
|
-
get('/Deployment'),
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
if (!teams?.length) {
|
|
32
|
-
console.log('No teams found.');
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Group deployments by team
|
|
37
|
-
const byTeam = {};
|
|
38
|
-
for (const d of (deployments ?? [])) {
|
|
39
|
-
if (!byTeam[d.teamId]) byTeam[d.teamId] = [];
|
|
40
|
-
byTeam[d.teamId].push(d);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const t of teams) {
|
|
44
|
-
const widgets = byTeam[t.id] ?? [];
|
|
45
|
-
const routing = t.routingAction ? ` routing=${t.routingAction}` : '';
|
|
46
|
-
console.log(`\n[${t.id}] ${t.name}${routing}`);
|
|
47
|
-
|
|
48
|
-
if (widgets.length) {
|
|
49
|
-
for (const w of widgets) {
|
|
50
|
-
console.log(` widget [${w.id}] "${w.displayName ?? 'unnamed'}" key: ${w.deploymentId}`);
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
console.log(` (no widgets assigned — conversations can reach this team via routing rules)`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(`\n${teams.length} team(s) · ${(deployments ?? []).length} widget(s)`);
|
|
58
|
-
console.log('\nPaste a widget key into your page\'s embed snippet to route that page to its team.');
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// ── widget subcommands ─────────────────────────────────────────────────────────
|
|
63
|
-
// "Widget" is the user-facing term. Internally these are deployments.
|
|
64
|
-
// A widget is a snippet you paste on a page. The page routes to the widget's team.
|
|
65
|
-
|
|
66
|
-
const widgetListCommand = {
|
|
67
|
-
command: 'widget list',
|
|
68
|
-
describe: 'List all widgets (embed snippets) and which team each routes to',
|
|
69
|
-
handler: runCommand(async () => {
|
|
70
|
-
const [deployments, teams] = await Promise.all([
|
|
71
|
-
get('/Deployment'),
|
|
72
|
-
get('/Teams/List'),
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
if (!deployments?.length) {
|
|
76
|
-
console.log('No widgets found.');
|
|
77
|
-
console.log('\nCreate a widget in the Velaro admin under Deployments,');
|
|
78
|
-
console.log('then paste its embed snippet on the pages you want chat on.');
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
83
|
-
const rows = deployments.map(d => ({
|
|
84
|
-
id: `[${d.id}]`,
|
|
85
|
-
key: d.deploymentId ?? '—',
|
|
86
|
-
team: teamMap[d.teamId] ?? `team ${d.teamId}`,
|
|
87
|
-
name: d.displayName ?? '(unnamed)',
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
const idW = Math.max(...rows.map(r => r.id.length));
|
|
91
|
-
const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
|
|
92
|
-
const teamW = Math.max('routes-to'.length, ...rows.map(r => r.team.length));
|
|
93
|
-
|
|
94
|
-
console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'routes-to'.padEnd(teamW)} name`);
|
|
95
|
-
console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
|
|
96
|
-
for (const r of rows) {
|
|
97
|
-
console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
|
|
98
|
-
}
|
|
99
|
-
console.log(`\n${rows.length} widget(s)`);
|
|
100
|
-
}),
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const widgetRenameCommand = {
|
|
104
|
-
command: 'widget rename <id> <name>',
|
|
105
|
-
describe: 'Rename a widget',
|
|
106
|
-
builder: (y) =>
|
|
107
|
-
y
|
|
108
|
-
.positional('id', { type: 'number', describe: 'Widget ID' })
|
|
109
|
-
.positional('name', { type: 'string', describe: 'New name' }),
|
|
110
|
-
handler: runCommand(async (argv) => {
|
|
111
|
-
const dep = await get(`/Deployment/${argv.id}`);
|
|
112
|
-
if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
|
|
113
|
-
await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
|
|
114
|
-
console.log(`Widget ${argv.id} renamed to "${argv.name}".`);
|
|
115
|
-
}),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const widgetAssignCommand = {
|
|
119
|
-
command: 'widget assign <id> <team-id>',
|
|
120
|
-
describe: 'Point a widget at a different team (embed code stays the same)',
|
|
121
|
-
builder: (y) =>
|
|
122
|
-
y
|
|
123
|
-
.positional('id', { type: 'number', describe: 'Widget ID' })
|
|
124
|
-
.positional('team-id', { type: 'number', describe: 'Team ID to route to' }),
|
|
125
|
-
handler: runCommand(async (argv) => {
|
|
126
|
-
const [dep, teams] = await Promise.all([
|
|
127
|
-
get(`/Deployment/${argv.id}`),
|
|
128
|
-
get('/Teams/List'),
|
|
129
|
-
]);
|
|
130
|
-
|
|
131
|
-
if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
|
|
132
|
-
|
|
133
|
-
const team = (teams ?? []).find(t => t.id === argv['team-id']);
|
|
134
|
-
if (!team) {
|
|
135
|
-
console.error(`Team ${argv['team-id']} not found.`);
|
|
136
|
-
console.error('Run: velaro team list');
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
|
|
141
|
-
console.log(`Widget "${dep.displayName ?? dep.id}" now routes to team "${team.name}".`);
|
|
142
|
-
console.log('Your embed snippet is unchanged — no website edits needed.');
|
|
143
|
-
}),
|
|
144
|
-
};
|
|
1
|
+
import { get, post } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
export const teamCommand = {
|
|
5
|
+
command: 'team <subcommand>',
|
|
6
|
+
describe: 'View teams and manage widget placements (deployments)',
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs
|
|
9
|
+
.command(teamListCommand)
|
|
10
|
+
.command(widgetListCommand)
|
|
11
|
+
.command(widgetRenameCommand)
|
|
12
|
+
.command(widgetAssignCommand)
|
|
13
|
+
.demandCommand(1, 'Specify a subcommand: list, widget'),
|
|
14
|
+
handler: () => {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ── team list ──────────────────────────────────────────────────────────────────
|
|
18
|
+
// Shows teams and their widget placements inline — the mental model is:
|
|
19
|
+
// team = who handles conversations
|
|
20
|
+
// widget = which pages route to that team
|
|
21
|
+
|
|
22
|
+
const teamListCommand = {
|
|
23
|
+
command: 'list',
|
|
24
|
+
describe: 'List teams and their widget placements',
|
|
25
|
+
handler: runCommand(async () => {
|
|
26
|
+
const [teams, deployments] = await Promise.all([
|
|
27
|
+
get('/Teams/List'),
|
|
28
|
+
get('/Deployment'),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
if (!teams?.length) {
|
|
32
|
+
console.log('No teams found.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Group deployments by team
|
|
37
|
+
const byTeam = {};
|
|
38
|
+
for (const d of (deployments ?? [])) {
|
|
39
|
+
if (!byTeam[d.teamId]) byTeam[d.teamId] = [];
|
|
40
|
+
byTeam[d.teamId].push(d);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const t of teams) {
|
|
44
|
+
const widgets = byTeam[t.id] ?? [];
|
|
45
|
+
const routing = t.routingAction ? ` routing=${t.routingAction}` : '';
|
|
46
|
+
console.log(`\n[${t.id}] ${t.name}${routing}`);
|
|
47
|
+
|
|
48
|
+
if (widgets.length) {
|
|
49
|
+
for (const w of widgets) {
|
|
50
|
+
console.log(` widget [${w.id}] "${w.displayName ?? 'unnamed'}" key: ${w.deploymentId}`);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` (no widgets assigned — conversations can reach this team via routing rules)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n${teams.length} team(s) · ${(deployments ?? []).length} widget(s)`);
|
|
58
|
+
console.log('\nPaste a widget key into your page\'s embed snippet to route that page to its team.');
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── widget subcommands ─────────────────────────────────────────────────────────
|
|
63
|
+
// "Widget" is the user-facing term. Internally these are deployments.
|
|
64
|
+
// A widget is a snippet you paste on a page. The page routes to the widget's team.
|
|
65
|
+
|
|
66
|
+
const widgetListCommand = {
|
|
67
|
+
command: 'widget list',
|
|
68
|
+
describe: 'List all widgets (embed snippets) and which team each routes to',
|
|
69
|
+
handler: runCommand(async () => {
|
|
70
|
+
const [deployments, teams] = await Promise.all([
|
|
71
|
+
get('/Deployment'),
|
|
72
|
+
get('/Teams/List'),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
if (!deployments?.length) {
|
|
76
|
+
console.log('No widgets found.');
|
|
77
|
+
console.log('\nCreate a widget in the Velaro admin under Deployments,');
|
|
78
|
+
console.log('then paste its embed snippet on the pages you want chat on.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
83
|
+
const rows = deployments.map(d => ({
|
|
84
|
+
id: `[${d.id}]`,
|
|
85
|
+
key: d.deploymentId ?? '—',
|
|
86
|
+
team: teamMap[d.teamId] ?? `team ${d.teamId}`,
|
|
87
|
+
name: d.displayName ?? '(unnamed)',
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const idW = Math.max(...rows.map(r => r.id.length));
|
|
91
|
+
const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
|
|
92
|
+
const teamW = Math.max('routes-to'.length, ...rows.map(r => r.team.length));
|
|
93
|
+
|
|
94
|
+
console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'routes-to'.padEnd(teamW)} name`);
|
|
95
|
+
console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
|
|
96
|
+
for (const r of rows) {
|
|
97
|
+
console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
|
|
98
|
+
}
|
|
99
|
+
console.log(`\n${rows.length} widget(s)`);
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const widgetRenameCommand = {
|
|
104
|
+
command: 'widget rename <id> <name>',
|
|
105
|
+
describe: 'Rename a widget',
|
|
106
|
+
builder: (y) =>
|
|
107
|
+
y
|
|
108
|
+
.positional('id', { type: 'number', describe: 'Widget ID' })
|
|
109
|
+
.positional('name', { type: 'string', describe: 'New name' }),
|
|
110
|
+
handler: runCommand(async (argv) => {
|
|
111
|
+
const dep = await get(`/Deployment/${argv.id}`);
|
|
112
|
+
if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
|
|
113
|
+
await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
|
|
114
|
+
console.log(`Widget ${argv.id} renamed to "${argv.name}".`);
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const widgetAssignCommand = {
|
|
119
|
+
command: 'widget assign <id> <team-id>',
|
|
120
|
+
describe: 'Point a widget at a different team (embed code stays the same)',
|
|
121
|
+
builder: (y) =>
|
|
122
|
+
y
|
|
123
|
+
.positional('id', { type: 'number', describe: 'Widget ID' })
|
|
124
|
+
.positional('team-id', { type: 'number', describe: 'Team ID to route to' }),
|
|
125
|
+
handler: runCommand(async (argv) => {
|
|
126
|
+
const [dep, teams] = await Promise.all([
|
|
127
|
+
get(`/Deployment/${argv.id}`),
|
|
128
|
+
get('/Teams/List'),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
|
|
132
|
+
|
|
133
|
+
const team = (teams ?? []).find(t => t.id === argv['team-id']);
|
|
134
|
+
if (!team) {
|
|
135
|
+
console.error(`Team ${argv['team-id']} not found.`);
|
|
136
|
+
console.error('Run: velaro team list');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
|
|
141
|
+
console.log(`Widget "${dep.displayName ?? dep.id}" now routes to team "${team.name}".`);
|
|
142
|
+
console.log('Your embed snippet is unchanged — no website edits needed.');
|
|
143
|
+
}),
|
|
144
|
+
};
|