@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.
Files changed (98) hide show
  1. package/README.md +161 -138
  2. package/bin/velaro.js +177 -62
  3. package/lib/api.js +91 -52
  4. package/lib/api.test.js +46 -0
  5. package/lib/banner.js +76 -0
  6. package/lib/commands/activity.js +133 -0
  7. package/lib/commands/acuity.js +66 -0
  8. package/lib/commands/agent.js +204 -50
  9. package/lib/commands/ai-config.js +193 -0
  10. package/lib/commands/ai-models.js +159 -0
  11. package/lib/commands/appointments.js +198 -0
  12. package/lib/commands/article.js +668 -388
  13. package/lib/commands/automation-draft.js +134 -0
  14. package/lib/commands/avatar.js +75 -0
  15. package/lib/commands/bigcommerce.js +50 -0
  16. package/lib/commands/billing-contacts.js +62 -0
  17. package/lib/commands/billing-email-preference.js +64 -0
  18. package/lib/commands/billing-subscription.js +265 -0
  19. package/lib/commands/billing.js +138 -0
  20. package/lib/commands/bot.js +141 -137
  21. package/lib/commands/bundle.js +168 -0
  22. package/lib/commands/calendly.js +62 -0
  23. package/lib/commands/callback.js +125 -0
  24. package/lib/commands/callrail.js +88 -0
  25. package/lib/commands/campaigns.js +44 -0
  26. package/lib/commands/case.js +102 -0
  27. package/lib/commands/check.js +163 -163
  28. package/lib/commands/compliance.js +229 -0
  29. package/lib/commands/conversation-efficiency.js +178 -0
  30. package/lib/commands/copilotstudio.js +114 -0
  31. package/lib/commands/coupon-grant.js +192 -0
  32. package/lib/commands/db.js +101 -0
  33. package/lib/commands/deployment.js +107 -107
  34. package/lib/commands/diagnostics.js +298 -0
  35. package/lib/commands/email-campaign.js +47 -0
  36. package/lib/commands/email-inbox.js +88 -0
  37. package/lib/commands/entitlement.js +176 -0
  38. package/lib/commands/env.js +45 -45
  39. package/lib/commands/feature-discovery.js +40 -0
  40. package/lib/commands/focus.js +278 -0
  41. package/lib/commands/index.js +38 -5
  42. package/lib/commands/ingest.js +31 -31
  43. package/lib/commands/inline-widget-config.js +126 -0
  44. package/lib/commands/integration.js +93 -0
  45. package/lib/commands/kb.js +450 -309
  46. package/lib/commands/login.js +86 -86
  47. package/lib/commands/logs.js +680 -0
  48. package/lib/commands/magento.js +210 -0
  49. package/lib/commands/mcp-key.js +188 -159
  50. package/lib/commands/migrate.js +134 -0
  51. package/lib/commands/migration-status.js +66 -0
  52. package/lib/commands/monday.js +137 -0
  53. package/lib/commands/netsuite.js +87 -0
  54. package/lib/commands/notifications.js +63 -0
  55. package/lib/commands/notion.js +70 -0
  56. package/lib/commands/ops.js +267 -173
  57. package/lib/commands/payment-recovery.js +170 -0
  58. package/lib/commands/pickup.js +172 -0
  59. package/lib/commands/pricing.js +132 -0
  60. package/lib/commands/product.js +55 -0
  61. package/lib/commands/recruiting.js +374 -0
  62. package/lib/commands/report.js +462 -0
  63. package/lib/commands/routing.js +304 -0
  64. package/lib/commands/rule.js +85 -85
  65. package/lib/commands/sharepoint.js +167 -0
  66. package/lib/commands/site-provision.js +68 -0
  67. package/lib/commands/site.js +62 -62
  68. package/lib/commands/sitesync.js +158 -0
  69. package/lib/commands/slack.js +64 -0
  70. package/lib/commands/squarespace.js +108 -0
  71. package/lib/commands/status.js +24 -24
  72. package/lib/commands/subscription.js +43 -0
  73. package/lib/commands/support.js +128 -0
  74. package/lib/commands/survey.js +216 -0
  75. package/lib/commands/team.js +144 -144
  76. package/lib/commands/teams-phone.js +131 -0
  77. package/lib/commands/teams.js +106 -0
  78. package/lib/commands/telephony.js +99 -0
  79. package/lib/commands/update.js +47 -47
  80. package/lib/commands/webflow.js +128 -0
  81. package/lib/commands/whoami.js +25 -22
  82. package/lib/commands/widget-container.js +152 -0
  83. package/lib/commands/woocommerce.js +240 -0
  84. package/lib/commands/workflow.js +233 -98
  85. package/lib/config.js +85 -83
  86. package/lib/kb-screenshot.js +320 -0
  87. package/lib/migrations/amscro.json +72 -0
  88. package/lib/migrations/azenta.json +68 -0
  89. package/lib/migrations/bluefire.json +49 -0
  90. package/lib/migrations/donaldson.json +75 -0
  91. package/lib/oauth.js +149 -135
  92. package/lib/run.js +21 -16
  93. package/lib/sharepoint-auth.js +138 -0
  94. package/lib/subscription.js +41 -39
  95. package/lib/track.js +35 -35
  96. package/lib/update-check.js +64 -64
  97. package/package.json +34 -19
  98. package/scripts/postinstall.js +12 -0
@@ -0,0 +1,304 @@
1
+ import { messagingGet, messagingPut, messagingDel, get as adminGet } from '../api.js';
2
+
3
+ // ── helpers ───────────────────────────────────────────────────────────────────
4
+
5
+ const RESET = '\x1b[0m';
6
+ const BOLD = '\x1b[1m';
7
+ const DIM = '\x1b[2m';
8
+ const CYN = '\x1b[36m';
9
+ const YEL = '\x1b[33m';
10
+ const GRN = '\x1b[32m';
11
+ const RED = '\x1b[31m';
12
+
13
+ function fmtDate(iso) {
14
+ if (!iso) return '—';
15
+ return new Date(iso).toLocaleString('en-US', {
16
+ timeZone: 'America/Los_Angeles',
17
+ dateStyle: 'short',
18
+ timeStyle: 'short',
19
+ });
20
+ }
21
+
22
+ const TERMINAL_LABELS = {
23
+ 0: 'Survey',
24
+ 1: 'Create Ticket',
25
+ 2: 'Survey + Create Ticket',
26
+ 3: 'Queue',
27
+ 4: 'Offline Message',
28
+ 5: 'No Action',
29
+ };
30
+
31
+ // ── velaro routing engine get ─────────────────────────────────────────────────
32
+
33
+ const engineGetCommand = {
34
+ command: 'get',
35
+ describe: 'Show the current routing engine version and circuit breaker status',
36
+ builder: (y) => y,
37
+ handler: async () => {
38
+ const data = await messagingGet('/RoutingEngine');
39
+
40
+ const ver = data.engineVersion ?? 'unknown';
41
+ const cb = data.circuitBreakerOpen ?? false;
42
+ const verColor = ver === 'modern' ? GRN : YEL;
43
+
44
+ console.log(`\n${BOLD}── Routing Engine ──────────────────────────────────────────────────────${RESET}`);
45
+ console.log(` Engine version: ${verColor}${ver}${RESET}`);
46
+ console.log(` Circuit breaker: ${cb ? `${RED}OPEN (auto-reverted to legacy)${RESET}` : `${GRN}closed${RESET}`}`);
47
+ console.log();
48
+
49
+ if (cb) {
50
+ console.log(`${YEL} ⚠ Circuit breaker is open.${RESET}`);
51
+ console.log(` Run ${CYN}velaro routing circuit-breaker reset${RESET} to re-enable smart routing.\n`);
52
+ }
53
+ },
54
+ };
55
+
56
+ // ── velaro routing engine set ─────────────────────────────────────────────────
57
+
58
+ const engineSetCommand = {
59
+ command: 'set',
60
+ describe: 'Set the routing engine version',
61
+ builder: (y) => y
62
+ .option('version', {
63
+ alias: 'v',
64
+ type: 'string',
65
+ describe: 'Engine version: legacy or modern',
66
+ choices: ['legacy', 'modern'],
67
+ demandOption: true,
68
+ }),
69
+ handler: async (argv) => {
70
+ const { version } = argv;
71
+ await messagingPut('/RoutingEngine', { engineVersion: version });
72
+ const color = version === 'modern' ? GRN : YEL;
73
+ console.log(`\n ${GRN}✓${RESET} Routing engine set to ${color}${version}${RESET}\n`);
74
+ },
75
+ };
76
+
77
+ // ── velaro routing engine ─────────────────────────────────────────────────────
78
+
79
+ const engineCommand = {
80
+ command: 'engine <subcommand>',
81
+ describe: 'Manage the routing engine version',
82
+ builder: (y) => y
83
+ .command(engineGetCommand)
84
+ .command(engineSetCommand)
85
+ .demandCommand(1, 'Specify a subcommand: get, set')
86
+ .strict(),
87
+ handler: () => {},
88
+ };
89
+
90
+ // ── velaro routing circuit-breaker reset ──────────────────────────────────────
91
+
92
+ const cbResetCommand = {
93
+ command: 'reset',
94
+ describe: 'Reset the routing circuit breaker to re-enable smart routing',
95
+ builder: (y) => y,
96
+ handler: async () => {
97
+ await messagingDel('/RoutingEngine/circuit-breaker');
98
+ console.log(`\n ${GRN}✓${RESET} Circuit breaker reset. Smart routing is active again.\n`);
99
+ },
100
+ };
101
+
102
+ const circuitBreakerCommand = {
103
+ command: 'circuit-breaker <subcommand>',
104
+ describe: 'Manage the routing circuit breaker',
105
+ builder: (y) => y
106
+ .command(cbResetCommand)
107
+ .demandCommand(1, 'Specify a subcommand: reset')
108
+ .strict(),
109
+ handler: () => {},
110
+ };
111
+
112
+ // ── velaro routing policy get ─────────────────────────────────────────────────
113
+
114
+ const policyGetCommand = {
115
+ command: 'get',
116
+ describe: 'Show the site-wide auto-routing policy',
117
+ builder: (y) => y,
118
+ handler: async () => {
119
+ const p = await messagingGet('/AutoRoutingPolicy');
120
+
121
+ console.log(`\n${BOLD}── Auto-Routing Policy ─────────────────────────────────────────────────${RESET}`);
122
+ console.log(` ${DIM}Enabled: ${RESET}${p.enabled ? `${GRN}yes${RESET}` : `${RED}no${RESET}`}`);
123
+ console.log(` ${DIM}Initial response timeout: ${RESET}${p.initialResponseTimeoutSeconds}s`);
124
+ console.log(` ${DIM}Idle after response: ${RESET}${p.idleAfterResponseTimeoutSeconds}s`);
125
+ console.log(` ${DIM}Idle alert grace period: ${RESET}${p.idleAlertGraceSeconds}s`);
126
+ console.log(` ${DIM}Max reroute attempts: ${RESET}${p.maxRerouteAttempts}`);
127
+ console.log(` ${DIM}Allow bounceback: ${RESET}${p.allowBounceBack ? `${YEL}yes${RESET}` : 'no'}`);
128
+ console.log(` ${DIM}Prefer previous agent: ${RESET}${p.preferPreviousAgentOnTimeout ? `${GRN}yes${RESET}` : 'no'}`);
129
+ console.log(` ${DIM}Agent follow-up flag: ${RESET}${p.agentFollowUpFlagEnabled ? `${GRN}yes${RESET}` : 'no'}`);
130
+ console.log(` ${DIM}Queue unavailable grace: ${RESET}${p.queueUnavailableGracePeriodSeconds}s`);
131
+
132
+ const terminalLabel = p.terminalActionOverride != null
133
+ ? (TERMINAL_LABELS[p.terminalActionOverride] ?? String(p.terminalActionOverride))
134
+ : 'Default (Survey)';
135
+ console.log(` ${DIM}Terminal action: ${RESET}${terminalLabel}`);
136
+ console.log();
137
+ },
138
+ };
139
+
140
+ // ── velaro routing policy set ─────────────────────────────────────────────────
141
+
142
+ const policySetCommand = {
143
+ command: 'set',
144
+ describe: 'Update the site-wide auto-routing policy fields',
145
+ builder: (y) => y
146
+ .option('initial-timeout', {
147
+ type: 'number',
148
+ describe: 'Initial response timeout in seconds (min 30)',
149
+ })
150
+ .option('idle-timeout', {
151
+ type: 'number',
152
+ describe: 'Idle after response timeout in seconds (min 30)',
153
+ })
154
+ .option('grace', {
155
+ type: 'number',
156
+ describe: 'Idle alert grace period in seconds',
157
+ })
158
+ .option('max-reroutes', {
159
+ type: 'number',
160
+ describe: 'Max reroute attempts (min 1)',
161
+ })
162
+ .option('allow-bounceback', {
163
+ type: 'boolean',
164
+ describe: 'Allow agent bounceback (true/false)',
165
+ })
166
+ .option('prefer-previous', {
167
+ type: 'boolean',
168
+ describe: 'Prefer previous agent on timeout (true/false)',
169
+ })
170
+ .option('terminal-action', {
171
+ type: 'number',
172
+ describe: 'Terminal action: 0=Survey 1=CreateTicket 2=SurveyAndCreateTicket 3=Queue 4=OfflineMessage 5=NoOp',
173
+ }),
174
+ handler: async (argv) => {
175
+ // Load current policy first, then merge
176
+ const current = await messagingGet('/AutoRoutingPolicy');
177
+
178
+ const updated = {
179
+ ...current,
180
+ initialResponseTimeoutSeconds: argv['initial-timeout'] ?? current.initialResponseTimeoutSeconds,
181
+ idleAfterResponseTimeoutSeconds: argv['idle-timeout'] ?? current.idleAfterResponseTimeoutSeconds,
182
+ idleAlertGraceSeconds: argv['grace'] ?? current.idleAlertGraceSeconds,
183
+ maxRerouteAttempts: argv['max-reroutes'] ?? current.maxRerouteAttempts,
184
+ allowBounceBack: argv['allow-bounceback'] ?? current.allowBounceBack,
185
+ preferPreviousAgentOnTimeout: argv['prefer-previous'] ?? current.preferPreviousAgentOnTimeout,
186
+ terminalActionOverride:
187
+ argv['terminal-action'] !== undefined
188
+ ? argv['terminal-action']
189
+ : current.terminalActionOverride,
190
+ };
191
+
192
+ await messagingPut('/AutoRoutingPolicy', updated);
193
+ console.log(`\n ${GRN}✓${RESET} Routing policy updated.\n`);
194
+ },
195
+ };
196
+
197
+ // ── velaro routing policy ─────────────────────────────────────────────────────
198
+
199
+ const policyCommand = {
200
+ command: 'policy <subcommand>',
201
+ describe: 'Manage the site-wide auto-routing policy',
202
+ builder: (y) => y
203
+ .command(policyGetCommand)
204
+ .command(policySetCommand)
205
+ .demandCommand(1, 'Specify a subcommand: get, set')
206
+ .strict(),
207
+ handler: () => {},
208
+ };
209
+
210
+ // ── velaro routing decision-log ───────────────────────────────────────────────
211
+
212
+ const decisionLogCommand = {
213
+ command: 'decision-log <conversationId>',
214
+ describe: 'Show the routing decision log for a conversation',
215
+ builder: (y) => y
216
+ .positional('conversationId', {
217
+ type: 'string',
218
+ describe: 'Conversation ID',
219
+ }),
220
+ handler: async (argv) => {
221
+ const { conversationId } = argv;
222
+ const data = await messagingGet(`/RoutingEngine/decision-log/${conversationId}`);
223
+ const items = Array.isArray(data) ? data : [];
224
+
225
+ console.log(`\n${BOLD}── Routing Decision Log — Conversation ${conversationId} ────────────────────${RESET}`);
226
+
227
+ if (!items.length) {
228
+ console.log(` ${DIM}No routing events found for this conversation.${RESET}\n`);
229
+ return;
230
+ }
231
+
232
+ for (const ev of items) {
233
+ const ts = fmtDate(ev.timestamp);
234
+ const type = (ev.eventType ?? '—').padEnd(24);
235
+ const source = ev.ruleSource ? `${DIM}[${ev.ruleSource}]${RESET} ` : '';
236
+ const agents = ev.fromAgentId
237
+ ? ` ${DIM}agent ${ev.fromAgentId}${ev.toAgentId ? ` → ${ev.toAgentId}` : ''}${RESET}`
238
+ : '';
239
+ const attempt = ev.rerouteAttemptNumber != null ? ` ${DIM}attempt #${ev.rerouteAttemptNumber}${RESET}` : '';
240
+ console.log(` ${CYN}${ts}${RESET} ${BOLD}${type}${RESET} ${source}${ev.reason ?? ''}${agents}${attempt}`);
241
+ }
242
+ console.log();
243
+ },
244
+ };
245
+
246
+ // ── velaro routing explain ────────────────────────────────────────────────────
247
+
248
+ const explainCommand = {
249
+ command: 'explain <conversationId>',
250
+ describe: 'Plain-English explanation of every routing decision for a conversation — agent names, policy settings, and timeline',
251
+ builder: (y) => y
252
+ .positional('conversationId', {
253
+ type: 'string',
254
+ describe: 'Conversation ID',
255
+ }),
256
+ handler: async (argv) => {
257
+ const { conversationId } = argv;
258
+ let result;
259
+ try {
260
+ result = await adminGet(`RoutingDiagnostics/explain/${conversationId}`);
261
+ } catch (err) {
262
+ console.error(`\n${RED}Error:${RESET} ${err.message}\n`);
263
+ process.exit(1);
264
+ }
265
+
266
+ console.log(`\n${BOLD}── Routing Explanation — Conversation ${conversationId} ─────────────────────${RESET}`);
267
+ console.log(` Engine: ${result.engineVersion === 'modern' ? GRN : YEL}${result.engineVersion}${RESET} · ${result.events?.length ?? 0} routing events\n`);
268
+
269
+ if (result.summary) {
270
+ const lines = result.summary.split('\n');
271
+ for (const line of lines) {
272
+ console.log(` ${line}`);
273
+ }
274
+ }
275
+
276
+ if (result.policySnapshot) {
277
+ const p = result.policySnapshot;
278
+ console.log(`\n${DIM} Policy settings:${RESET}`);
279
+ console.log(` Initial timeout: ${p.initialResponseTimeoutSeconds}s`);
280
+ console.log(` Idle timeout: ${p.idleAfterResponseTimeoutSeconds}s`);
281
+ console.log(` Max reroutes: ${p.maxRerouteAttempts}`);
282
+ console.log(` Terminal action: ${p.terminalActionOverride != null ? TERMINAL_LABELS[p.terminalActionOverride] ?? p.terminalActionOverride : 'default (Survey)'}`);
283
+ console.log(` Anti-bounce: ${!p.allowBounceBack ? `${GRN}ON${RESET}` : `${DIM}off${RESET}`}`);
284
+ console.log(` Prefer prev agent: ${p.preferPreviousAgentOnTimeout ? `${GRN}yes${RESET}` : 'no'}`);
285
+ }
286
+ console.log();
287
+ },
288
+ };
289
+
290
+ // ── top-level routing command ─────────────────────────────────────────────────
291
+
292
+ export const routingCommand = {
293
+ command: 'routing <subcommand>',
294
+ describe: 'Manage smart routing engine, policy, and decision logs',
295
+ builder: (y) => y
296
+ .command(engineCommand)
297
+ .command(circuitBreakerCommand)
298
+ .command(policyCommand)
299
+ .command(decisionLogCommand)
300
+ .command(explainCommand)
301
+ .demandCommand(1, 'Specify a subcommand: engine, circuit-breaker, policy, decision-log, explain')
302
+ .strict(),
303
+ handler: () => {},
304
+ };
@@ -1,85 +1,85 @@
1
- import { get, post, del } from '../api.js';
2
- import { requireFeature } from '../subscription.js';
3
- import { runCommand } from '../run.js';
4
-
5
- export const ruleCommand = {
6
- command: 'rule <subcommand>',
7
- describe: 'Manage routing rules',
8
- builder: (yargs) =>
9
- yargs
10
- .command(ruleListCommand)
11
- .command(ruleCreateCommand)
12
- .command(ruleDeleteCommand)
13
- .demandCommand(1, 'Specify a subcommand: list, create, delete'),
14
- handler: () => {},
15
- };
16
-
17
- // ── list ───────────────────────────────────────────────────────────────────────
18
-
19
- const ruleListCommand = {
20
- command: 'list',
21
- describe: 'List all routing rules',
22
- handler: runCommand(async () => {
23
- await requireFeature('enableWorkflowRules', 'Routing Rules');
24
-
25
- const rules = await get('/Rules/List');
26
- if (!rules?.length) { console.log('No routing rules found.'); return; }
27
-
28
- const sorted = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
29
- const idW = Math.max(...sorted.map(r => String(r.id).length + 2));
30
- const priW = Math.max('pri'.length, ...sorted.map(r => String(r.priority ?? 0).length));
31
- const nameW = Math.max('name'.length, ...sorted.map(r => (r.name ?? r.trigger ?? '—').length));
32
-
33
- console.log(`\n${'id'.padStart(idW)} ${'pri'.padEnd(priW)} ${'name'.padEnd(nameW)} trigger`);
34
- console.log(`${'-'.repeat(idW)} ${'-'.repeat(priW)} ${'-'.repeat(nameW)} -------`);
35
- for (const r of sorted) {
36
- const id = `[${r.id}]`.padStart(idW);
37
- const pri = String(r.priority ?? 0).padEnd(priW);
38
- const name = (r.name ?? r.trigger ?? '—').padEnd(nameW);
39
- console.log(`${id} ${pri} ${name} ${r.trigger ?? '—'}`);
40
- }
41
- console.log(`\n${sorted.length} rule(s)`);
42
- }),
43
- };
44
-
45
- // ── create ─────────────────────────────────────────────────────────────────────
46
-
47
- const ruleCreateCommand = {
48
- command: 'create',
49
- describe: 'Create a routing rule',
50
- builder: (y) =>
51
- y
52
- .option('name', { type: 'string', demandOption: true, describe: 'Rule name' })
53
- .option('trigger', { type: 'string', demandOption: true, describe: 'Trigger type (e.g. pageUrl, country)' })
54
- .option('value', { type: 'string', demandOption: true, describe: 'Trigger value to match' })
55
- .option('team-id', { type: 'number', demandOption: true, describe: 'Team ID to route to' })
56
- .option('priority', { type: 'number', default: 0, describe: 'Rule priority (lower = higher priority)' }),
57
-
58
- handler: runCommand(async (argv) => {
59
- await requireFeature('enableWorkflowRules', 'Routing Rules');
60
-
61
- const result = await post('/Rules', {
62
- name: argv.name,
63
- trigger: argv.trigger,
64
- value: argv.value,
65
- teamId: argv['team-id'],
66
- priority: argv.priority,
67
- isEnabled: true,
68
- });
69
-
70
- console.log(`Rule created: [${result.id}] ${result.name}`);
71
- }),
72
- };
73
-
74
- // ── delete ─────────────────────────────────────────────────────────────────────
75
-
76
- const ruleDeleteCommand = {
77
- command: 'delete <id>',
78
- describe: 'Delete a routing rule',
79
- builder: (y) => y.positional('id', { type: 'number', describe: 'Rule ID' }),
80
- handler: runCommand(async (argv) => {
81
- await requireFeature('enableWorkflowRules', 'Routing Rules');
82
- await del(`/Rules/${argv.id}`);
83
- console.log(`Rule ${argv.id} deleted.`);
84
- }),
85
- };
1
+ import { get, post, del } from '../api.js';
2
+ import { requireFeature } from '../subscription.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const ruleCommand = {
6
+ command: 'rule <subcommand>',
7
+ describe: 'Manage routing rules',
8
+ builder: (yargs) =>
9
+ yargs
10
+ .command(ruleListCommand)
11
+ .command(ruleCreateCommand)
12
+ .command(ruleDeleteCommand)
13
+ .demandCommand(1, 'Specify a subcommand: list, create, delete'),
14
+ handler: () => {},
15
+ };
16
+
17
+ // ── list ───────────────────────────────────────────────────────────────────────
18
+
19
+ const ruleListCommand = {
20
+ command: 'list',
21
+ describe: 'List all routing rules',
22
+ handler: runCommand(async () => {
23
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
24
+
25
+ const rules = await get('/Rules/List');
26
+ if (!rules?.length) { console.log('No routing rules found.'); return; }
27
+
28
+ const sorted = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
29
+ const idW = Math.max(...sorted.map(r => String(r.id).length + 2));
30
+ const priW = Math.max('pri'.length, ...sorted.map(r => String(r.priority ?? 0).length));
31
+ const nameW = Math.max('name'.length, ...sorted.map(r => (r.name ?? r.trigger ?? '—').length));
32
+
33
+ console.log(`\n${'id'.padStart(idW)} ${'pri'.padEnd(priW)} ${'name'.padEnd(nameW)} trigger`);
34
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(priW)} ${'-'.repeat(nameW)} -------`);
35
+ for (const r of sorted) {
36
+ const id = `[${r.id}]`.padStart(idW);
37
+ const pri = String(r.priority ?? 0).padEnd(priW);
38
+ const name = (r.name ?? r.trigger ?? '—').padEnd(nameW);
39
+ console.log(`${id} ${pri} ${name} ${r.trigger ?? '—'}`);
40
+ }
41
+ console.log(`\n${sorted.length} rule(s)`);
42
+ }),
43
+ };
44
+
45
+ // ── create ─────────────────────────────────────────────────────────────────────
46
+
47
+ const ruleCreateCommand = {
48
+ command: 'create',
49
+ describe: 'Create a routing rule',
50
+ builder: (y) =>
51
+ y
52
+ .option('name', { type: 'string', demandOption: true, describe: 'Rule name' })
53
+ .option('trigger', { type: 'string', demandOption: true, describe: 'Trigger type (e.g. pageUrl, country)' })
54
+ .option('value', { type: 'string', demandOption: true, describe: 'Trigger value to match' })
55
+ .option('team-id', { type: 'number', demandOption: true, describe: 'Team ID to route to' })
56
+ .option('priority', { type: 'number', default: 0, describe: 'Rule priority (lower = higher priority)' }),
57
+
58
+ handler: runCommand(async (argv) => {
59
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
60
+
61
+ const result = await post('/Rules', {
62
+ name: argv.name,
63
+ trigger: argv.trigger,
64
+ value: argv.value,
65
+ teamId: argv['team-id'],
66
+ priority: argv.priority,
67
+ isEnabled: true,
68
+ });
69
+
70
+ console.log(`Rule created: [${result.id}] ${result.name}`);
71
+ }),
72
+ };
73
+
74
+ // ── delete ─────────────────────────────────────────────────────────────────────
75
+
76
+ const ruleDeleteCommand = {
77
+ command: 'delete <id>',
78
+ describe: 'Delete a routing rule',
79
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Rule ID' }),
80
+ handler: runCommand(async (argv) => {
81
+ await requireFeature('enableWorkflowRules', 'Routing Rules');
82
+ await del(`/Rules/${argv.id}`);
83
+ console.log(`Rule ${argv.id} deleted.`);
84
+ }),
85
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * velaro sharepoint — SharePoint / tracker access commands
3
+ *
4
+ * velaro sharepoint login One-time device-code auth against nocvelaro M365 tenant
5
+ * velaro sharepoint read-tracker Download + parse the Integrations Status Tracker Excel file
6
+ * velaro sharepoint whoami Show the currently logged-in M365 account
7
+ */
8
+
9
+ import { readConfig, writeConfig } from '../config.js';
10
+ import { spRequestDeviceCode, spPollForToken, getSpAccessToken, downloadSharePointFile } from '../sharepoint-auth.js';
11
+
12
+ // The tracker file. Update the URL here if SharePoint ever moves it.
13
+ const TRACKER_URL = 'https://nocvelaro.sharepoint.com/sites/DevOps/Shared%20Documents/DevOps/Integrations%20and%20Partnerships/Velaro%20Integrations%20Status%20Tracker.xlsx';
14
+
15
+ // ── login ─────────────────────────────────────────────────────────────────────
16
+
17
+ const loginCommand = {
18
+ command: 'login',
19
+ describe: 'Authenticate with SharePoint (nocvelaro M365 tenant). Run once.',
20
+ handler: async () => {
21
+ const deviceData = await spRequestDeviceCode();
22
+ console.log('\n\x1b[1mSign in to access the Velaro SharePoint tracker\x1b[0m');
23
+ console.log(`\n Go to: \x1b[36m${deviceData.verification_uri}\x1b[0m`);
24
+ console.log(` Enter code: \x1b[33m${deviceData.user_code}\x1b[0m`);
25
+ console.log('\nWaiting for sign-in...');
26
+
27
+ const tokenData = await spPollForToken(deviceData.device_code, deviceData.interval || 5);
28
+
29
+ const sp = {
30
+ accessToken: tokenData.access_token,
31
+ refreshToken: tokenData.refresh_token,
32
+ expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString(),
33
+ };
34
+
35
+ const cfg = readConfig();
36
+ cfg.sharepoint = sp;
37
+ writeConfig(cfg);
38
+
39
+ // Get display name via Graph
40
+ try {
41
+ const meRes = await fetch('https://graph.microsoft.com/v1.0/me', {
42
+ headers: { Authorization: `Bearer ${tokenData.access_token}` },
43
+ });
44
+ const me = await meRes.json();
45
+ console.log(`\n\x1b[32m✓ Logged in as ${me.displayName} (${me.userPrincipalName})\x1b[0m`);
46
+ } catch {
47
+ console.log('\n\x1b[32m✓ SharePoint login saved.\x1b[0m');
48
+ }
49
+ console.log(' Run: velaro sharepoint read-tracker\n');
50
+ },
51
+ };
52
+
53
+ // ── whoami ────────────────────────────────────────────────────────────────────
54
+
55
+ const whoamiCommand = {
56
+ command: 'whoami',
57
+ describe: 'Show the M365 account currently logged into SharePoint',
58
+ handler: async () => {
59
+ const cfg = readConfig();
60
+ let token;
61
+ try {
62
+ const result = await getSpAccessToken(cfg);
63
+ token = result.accessToken;
64
+ if (result.refreshed) { cfg.sharepoint = { ...cfg.sharepoint, ...result.refreshed }; writeConfig(cfg); }
65
+ } catch (e) {
66
+ console.error(`\x1b[31m✗ ${e.message}\x1b[0m`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const res = await fetch('https://graph.microsoft.com/v1.0/me', {
71
+ headers: { Authorization: `Bearer ${token}` },
72
+ });
73
+ const me = await res.json();
74
+ console.log(`\n \x1b[1m${me.displayName}\x1b[0m (${me.userPrincipalName})`);
75
+ console.log(` Token expires: ${cfg.sharepoint?.expiresAt ?? 'unknown'}\n`);
76
+ },
77
+ };
78
+
79
+ // ── read-tracker ──────────────────────────────────────────────────────────────
80
+
81
+ const readTrackerCommand = {
82
+ command: 'read-tracker',
83
+ describe: 'Download and parse the Velaro Integrations Status Tracker spreadsheet',
84
+ builder: (y) =>
85
+ y
86
+ .option('sheet', { type: 'string', describe: 'Sheet name to show (default: all)' })
87
+ .option('json', { type: 'boolean', describe: 'Output raw JSON instead of formatted table' }),
88
+ handler: async (argv) => {
89
+ const cfg = readConfig();
90
+ let accessToken;
91
+ try {
92
+ const result = await getSpAccessToken(cfg);
93
+ accessToken = result.accessToken;
94
+ if (result.refreshed) { cfg.sharepoint = { ...cfg.sharepoint, ...result.refreshed }; writeConfig(cfg); }
95
+ } catch (e) {
96
+ console.error(`\x1b[31m✗ ${e.message}\x1b[0m`);
97
+ process.exit(1);
98
+ }
99
+
100
+ console.log('Downloading tracker...');
101
+ let buf;
102
+ try {
103
+ buf = await downloadSharePointFile(TRACKER_URL, accessToken);
104
+ } catch (e) {
105
+ console.error(`\x1b[31m✗ ${e.message}\x1b[0m`);
106
+ process.exit(1);
107
+ }
108
+
109
+ // Dynamically import xlsx (ESM-compatible)
110
+ let XLSX;
111
+ try {
112
+ XLSX = (await import('xlsx')).default;
113
+ } catch {
114
+ console.error('\x1b[31m✗ xlsx package not installed. Run: npm install xlsx --prefix cli\x1b[0m');
115
+ process.exit(1);
116
+ }
117
+
118
+ const workbook = XLSX.read(buf, { type: 'buffer', cellDates: true });
119
+ const targetSheets = argv.sheet
120
+ ? workbook.SheetNames.filter(n => n.toLowerCase().includes(argv.sheet.toLowerCase()))
121
+ : workbook.SheetNames;
122
+
123
+ if (!targetSheets.length) {
124
+ console.error(`\x1b[31m✗ No sheet matching "${argv.sheet}". Available: ${workbook.SheetNames.join(', ')}\x1b[0m`);
125
+ process.exit(1);
126
+ }
127
+
128
+ for (const sheetName of targetSheets) {
129
+ const ws = workbook.Sheets[sheetName];
130
+ const rows = XLSX.utils.sheet_to_json(ws, { defval: '' });
131
+
132
+ if (argv.json) {
133
+ console.log(JSON.stringify({ sheet: sheetName, rows }, null, 2));
134
+ continue;
135
+ }
136
+
137
+ console.log(`\n\x1b[1m━━ ${sheetName} \x1b[0m(${rows.length} rows)\n`);
138
+ if (!rows.length) { console.log(' (empty)\n'); continue; }
139
+
140
+ // Print as simple key: value blocks — avoids terminal width issues with wide tables
141
+ for (const [i, row] of rows.entries()) {
142
+ const keys = Object.keys(row).filter(k => row[k] !== '');
143
+ if (!keys.length) continue;
144
+ console.log(` \x1b[36m[${i + 1}]\x1b[0m`);
145
+ for (const k of keys) {
146
+ const val = String(row[k]).replace(/\n/g, ' ↵ ').slice(0, 200);
147
+ console.log(` \x1b[33m${k}\x1b[0m: ${val}`);
148
+ }
149
+ console.log('');
150
+ }
151
+ }
152
+ },
153
+ };
154
+
155
+ // ── export ────────────────────────────────────────────────────────────────────
156
+
157
+ export const sharepointCommand = {
158
+ command: 'sharepoint <subcommand>',
159
+ describe: 'Access the Velaro SharePoint tracker and M365 files',
160
+ builder: (yargs) =>
161
+ yargs
162
+ .command(loginCommand)
163
+ .command(whoamiCommand)
164
+ .command(readTrackerCommand)
165
+ .demandCommand(1, 'Specify a subcommand: login, whoami, read-tracker'),
166
+ handler: () => {},
167
+ };