@velaro/cli 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/velaro.js +7 -1
- package/lib/api.js +52 -48
- package/lib/commands/agent.js +50 -50
- package/lib/commands/article.js +120 -12
- package/lib/commands/check.js +163 -163
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/env.js +45 -0
- package/lib/commands/index.js +212 -0
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/kb.js +69 -1
- package/lib/commands/login.js +46 -20
- package/lib/commands/mcp-key.js +17 -10
- package/lib/commands/ops.js +173 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/status.js +24 -22
- package/lib/commands/team.js +144 -144
- package/lib/commands/update.js +47 -47
- package/lib/commands/whoami.js +22 -22
- package/lib/config.js +63 -8
- package/lib/oauth.js +135 -135
- package/lib/run.js +16 -16
- package/lib/subscription.js +39 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +2 -2
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
// ── ops command ───────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export const opsCommand = {
|
|
59
|
+
command: 'ops',
|
|
60
|
+
describe: 'System health: deploy times, Hangfire, API status, and active alerts (Velaro staff only)',
|
|
61
|
+
handler: async () => {
|
|
62
|
+
const creds = await getCredentials();
|
|
63
|
+
const isAdmin = creds.siteId === 1032;
|
|
64
|
+
|
|
65
|
+
if (!isAdmin) {
|
|
66
|
+
console.error('velaro ops is only available to Velaro staff.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const cfg = readConfig();
|
|
71
|
+
|
|
72
|
+
// ── 1. Deploy versions (anonymous — both envs in parallel) ───────────────
|
|
73
|
+
const prodBase = cfg.envs?.prod?.adminApiBase ?? ENVS.prod.adminApiBase;
|
|
74
|
+
const stagingBase = cfg.envs?.staging?.adminApiBase ?? ENVS.staging.adminApiBase;
|
|
75
|
+
|
|
76
|
+
const [prodVer, stagingVer] = await Promise.all([
|
|
77
|
+
pingVersion('prod', prodBase),
|
|
78
|
+
pingVersion('staging', stagingBase),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
console.log('\n\x1b[1m── Deploy ──────────────────────────────────────────────────\x1b[0m');
|
|
82
|
+
for (const v of [prodVer, stagingVer]) {
|
|
83
|
+
if (v.ok) {
|
|
84
|
+
const sym = OK;
|
|
85
|
+
const ver = `\x1b[36m${v.version}\x1b[0m`;
|
|
86
|
+
const time = `${ago(v.deployedAt).padEnd(12)} (${utcLabel(v.deployedAt)})`;
|
|
87
|
+
console.log(` ${sym} ${v.label.padEnd(8)} ${ver.padEnd(30)} deployed ${time}`);
|
|
88
|
+
} else {
|
|
89
|
+
const detail = v.err ?? `HTTP ${v.status}`;
|
|
90
|
+
console.log(` ${ERR} ${v.label.padEnd(8)} UNREACHABLE — ${detail}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 2. API health (admin + messaging, both envs) ──────────────────────────
|
|
95
|
+
const prodMsgBase = cfg.envs?.prod?.messagingApiBase ?? ENVS.prod.messagingApiBase;
|
|
96
|
+
const stagingMsgBase = cfg.envs?.staging?.messagingApiBase ?? ENVS.staging.messagingApiBase;
|
|
97
|
+
|
|
98
|
+
const [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth] = await Promise.all([
|
|
99
|
+
pingHealth('admin/prod', `${prodBase}/Status`),
|
|
100
|
+
pingHealth('admin/staging', `${stagingBase}/Status`),
|
|
101
|
+
pingHealth('msg/prod', `${prodMsgBase}/api/health`),
|
|
102
|
+
pingHealth('msg/staging', `${stagingMsgBase}/api/health`),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
console.log('\n\x1b[1m── API Health ──────────────────────────────────────────────\x1b[0m');
|
|
106
|
+
for (const h of [adminProdHealth, adminStagingHealth, msgProdHealth, msgStagingHealth]) {
|
|
107
|
+
if (h.ok) {
|
|
108
|
+
console.log(` ${OK} ${h.label}`);
|
|
109
|
+
} else {
|
|
110
|
+
const detail = h.err ?? `HTTP ${h.status}`;
|
|
111
|
+
console.log(` ${ERR} ${h.label.padEnd(20)} ${detail}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 3. Hangfire failed jobs ───────────────────────────────────────────────
|
|
116
|
+
console.log('\n\x1b[1m── Hangfire ────────────────────────────────────────────────\x1b[0m');
|
|
117
|
+
try {
|
|
118
|
+
const jobs = await get('/DatabaseTool/FailedJobs');
|
|
119
|
+
if (!jobs?.length) {
|
|
120
|
+
console.log(` ${OK} 0 failed jobs`);
|
|
121
|
+
} else {
|
|
122
|
+
const sym = jobs.length >= 20 ? ERR : jobs.length >= 5 ? WARN : WARN;
|
|
123
|
+
console.log(` ${sym} ${jobs.length} failed job(s)`);
|
|
124
|
+
for (const j of jobs.slice(0, 5)) {
|
|
125
|
+
const when = j.failedAt ? ago(j.failedAt) : '?';
|
|
126
|
+
const name = [j.typeName, j.methodName].filter(Boolean).join('.');
|
|
127
|
+
const msg = j.exceptionMessage?.split('\n')[0]?.slice(0, 80) ?? j.reason?.slice(0, 80) ?? '';
|
|
128
|
+
console.log(` ${WARN} ${name.padEnd(35)} ${when.padEnd(10)} ${msg}`);
|
|
129
|
+
}
|
|
130
|
+
if (jobs.length > 5) console.log(` ... and ${jobs.length - 5} more`);
|
|
131
|
+
console.log(' Open /hangfire to inspect and retry.');
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.log(` ${WARN} Could not load Hangfire jobs: ${e.message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── 4. Active alerts (needs-attention via messaging API) ──────────────────
|
|
138
|
+
console.log('\n\x1b[1m── Alerts ──────────────────────────────────────────────────\x1b[0m');
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(`${prodMsgBase}/SuperAdmin/needs-attention`, {
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${creds.velaroToken}`,
|
|
143
|
+
'X-Internal-SiteId': String(creds.siteId),
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
},
|
|
146
|
+
signal: AbortSignal.timeout(15000),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
console.log(` ${WARN} Could not reach alerts endpoint (HTTP ${res.status})`);
|
|
151
|
+
} else {
|
|
152
|
+
const items = await res.json();
|
|
153
|
+
const alertItems = Array.isArray(items)
|
|
154
|
+
? items.filter(i => i.type !== 'hangfire_failed') // shown above
|
|
155
|
+
: [];
|
|
156
|
+
if (!alertItems.length) {
|
|
157
|
+
console.log(` ${OK} Nothing needs attention`);
|
|
158
|
+
} else {
|
|
159
|
+
for (const item of alertItems) {
|
|
160
|
+
console.log(` ${severityPrefix(item.severity)}${item.title}`);
|
|
161
|
+
if (item.recommendedFix) {
|
|
162
|
+
console.log(` \x1b[2m${item.recommendedFix.slice(0, 100)}\x1b[0m`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log(` ${WARN} Alerts unavailable: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log('\n──────────────────────────────────────────────────────────────\n');
|
|
172
|
+
},
|
|
173
|
+
};
|
package/lib/commands/site.js
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { get } from '../api.js';
|
|
2
|
-
import { runCommand } from '../run.js';
|
|
3
|
-
|
|
4
|
-
const FEATURE_MAP = {
|
|
5
|
-
enableWeb: 'Web Chat',
|
|
6
|
-
enableSms: 'SMS',
|
|
7
|
-
enableEmail: 'Email',
|
|
8
|
-
enableWhatsapp: 'WhatsApp',
|
|
9
|
-
enableIvr: 'IVR / Voice',
|
|
10
|
-
enableFacebook: 'Facebook Messenger',
|
|
11
|
-
enableInstagram: 'Instagram',
|
|
12
|
-
enableAI: 'AI Bots',
|
|
13
|
-
enableKnowledgeBase: 'Knowledge Base',
|
|
14
|
-
enableAutomation: 'Workflows',
|
|
15
|
-
enableWorkflowRules: 'Routing Rules',
|
|
16
|
-
enableChatTranslations: 'Chat Translation',
|
|
17
|
-
enableVideoChat: 'Video Chat',
|
|
18
|
-
enableTicketing: 'Ticketing',
|
|
19
|
-
enableAgentDashboard: 'Agent Dashboard',
|
|
20
|
-
enableMcpApi: 'MCP API Gateway',
|
|
21
|
-
enableOutboundCampaigns: 'Email Campaigns',
|
|
22
|
-
enableRcs: 'RCS Messaging',
|
|
23
|
-
enablePageWidgets: 'Page Widgets',
|
|
24
|
-
enableVisitorTracking: 'Visitor Tracking',
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const STATUS_LABEL = { Available: 'online', Away: 'away', Offline: 'offline' };
|
|
28
|
-
|
|
29
|
-
export const siteCommand = {
|
|
30
|
-
command: 'site',
|
|
31
|
-
describe: 'Show site overview: active features and agent availability',
|
|
32
|
-
handler: runCommand(async () => {
|
|
33
|
-
const [sub, agents] = await Promise.all([
|
|
34
|
-
get('/Subscription'),
|
|
35
|
-
get('/Users/List'),
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
// Active features — friendly names only, no internal flag names
|
|
39
|
-
const active = Object.entries(FEATURE_MAP)
|
|
40
|
-
.filter(([key]) => sub[key] === true)
|
|
41
|
-
.map(([, label]) => label);
|
|
42
|
-
|
|
43
|
-
console.log('\nActive features:');
|
|
44
|
-
if (active.length) {
|
|
45
|
-
console.log(' ' + active.join(' · '));
|
|
46
|
-
} else {
|
|
47
|
-
console.log(' (none)');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (sub.maxCreatedUsers) {
|
|
51
|
-
console.log(`\nAgent seats: ${sub.maxCreatedUsers} licensed`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Agent availability summary
|
|
55
|
-
const counts = { online: 0, away: 0, offline: 0 };
|
|
56
|
-
for (const a of agents) {
|
|
57
|
-
const key = STATUS_LABEL[a.status] ?? 'offline';
|
|
58
|
-
counts[key]++;
|
|
59
|
-
}
|
|
60
|
-
console.log(`Agent status: ${counts.online} online · ${counts.away} away · ${counts.offline} offline\n`);
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
1
|
+
import { get } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
const FEATURE_MAP = {
|
|
5
|
+
enableWeb: 'Web Chat',
|
|
6
|
+
enableSms: 'SMS',
|
|
7
|
+
enableEmail: 'Email',
|
|
8
|
+
enableWhatsapp: 'WhatsApp',
|
|
9
|
+
enableIvr: 'IVR / Voice',
|
|
10
|
+
enableFacebook: 'Facebook Messenger',
|
|
11
|
+
enableInstagram: 'Instagram',
|
|
12
|
+
enableAI: 'AI Bots',
|
|
13
|
+
enableKnowledgeBase: 'Knowledge Base',
|
|
14
|
+
enableAutomation: 'Workflows',
|
|
15
|
+
enableWorkflowRules: 'Routing Rules',
|
|
16
|
+
enableChatTranslations: 'Chat Translation',
|
|
17
|
+
enableVideoChat: 'Video Chat',
|
|
18
|
+
enableTicketing: 'Ticketing',
|
|
19
|
+
enableAgentDashboard: 'Agent Dashboard',
|
|
20
|
+
enableMcpApi: 'MCP API Gateway',
|
|
21
|
+
enableOutboundCampaigns: 'Email Campaigns',
|
|
22
|
+
enableRcs: 'RCS Messaging',
|
|
23
|
+
enablePageWidgets: 'Page Widgets',
|
|
24
|
+
enableVisitorTracking: 'Visitor Tracking',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const STATUS_LABEL = { Available: 'online', Away: 'away', Offline: 'offline' };
|
|
28
|
+
|
|
29
|
+
export const siteCommand = {
|
|
30
|
+
command: 'site',
|
|
31
|
+
describe: 'Show site overview: active features and agent availability',
|
|
32
|
+
handler: runCommand(async () => {
|
|
33
|
+
const [sub, agents] = await Promise.all([
|
|
34
|
+
get('/Subscription'),
|
|
35
|
+
get('/Users/List'),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Active features — friendly names only, no internal flag names
|
|
39
|
+
const active = Object.entries(FEATURE_MAP)
|
|
40
|
+
.filter(([key]) => sub[key] === true)
|
|
41
|
+
.map(([, label]) => label);
|
|
42
|
+
|
|
43
|
+
console.log('\nActive features:');
|
|
44
|
+
if (active.length) {
|
|
45
|
+
console.log(' ' + active.join(' · '));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(' (none)');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sub.maxCreatedUsers) {
|
|
51
|
+
console.log(`\nAgent seats: ${sub.maxCreatedUsers} licensed`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Agent availability summary
|
|
55
|
+
const counts = { online: 0, away: 0, offline: 0 };
|
|
56
|
+
for (const a of agents) {
|
|
57
|
+
const key = STATUS_LABEL[a.status] ?? 'offline';
|
|
58
|
+
counts[key]++;
|
|
59
|
+
}
|
|
60
|
+
console.log(`Agent status: ${counts.online} online · ${counts.away} away · ${counts.offline} offline\n`);
|
|
61
|
+
}),
|
|
62
|
+
};
|
package/lib/commands/status.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { readConfig,
|
|
2
|
-
|
|
3
|
-
export const statusCommand = {
|
|
4
|
-
command: 'status',
|
|
5
|
-
describe: 'Check Velaro API health',
|
|
6
|
-
handler: async () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
console.log(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
1
|
+
import { readConfig, getActiveEnv, ENVS } from '../config.js';
|
|
2
|
+
|
|
3
|
+
export const statusCommand = {
|
|
4
|
+
command: 'status',
|
|
5
|
+
describe: 'Check Velaro API health',
|
|
6
|
+
handler: async () => {
|
|
7
|
+
const cfg = readConfig();
|
|
8
|
+
const env = getActiveEnv();
|
|
9
|
+
const apiBase = cfg.envs?.[env]?.adminApiBase ?? ENVS[env].adminApiBase;
|
|
10
|
+
process.stdout.write(`Checking ${apiBase}/Status ... `);
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${apiBase}/Status`);
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
console.log('OK');
|
|
15
|
+
} else {
|
|
16
|
+
console.log(`DEGRADED (HTTP ${res.status})`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log(`UNREACHABLE: ${err.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
};
|
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
|
+
};
|