@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
package/lib/api.js CHANGED
@@ -1,52 +1,91 @@
1
- import { readConfig, writeConfig, getActiveEnv, setEnvCredentials, ENVS } from './config.js';
2
- import { refreshVelaroToken } from './oauth.js';
3
-
4
- const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
-
6
- export async function getCredentials(envOverride) {
7
- const cfg = readConfig();
8
- const env = envOverride || cfg.activeEnv || 'prod';
9
- let creds = cfg.envs?.[env];
10
-
11
- if (!creds?.velaroToken) {
12
- throw new Error(`Not logged in to ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
13
- }
14
-
15
- if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
16
- if (!creds.entraRefreshToken) {
17
- throw new Error(`Session expired for ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
18
- }
19
- const refreshed = await refreshVelaroToken({ ...creds, apiBase: creds.adminApiBase });
20
- const updated = { ...creds, ...refreshed };
21
- setEnvCredentials(env, updated);
22
- creds = updated;
23
- }
24
-
25
- return { ...creds, env };
26
- }
27
-
28
- export async function request(method, path, body, envOverride) {
29
- const creds = await getCredentials(envOverride);
30
-
31
- const res = await fetch(`${creds.adminApiBase}${path}`, {
32
- method,
33
- headers: {
34
- Authorization: `Bearer ${creds.velaroToken}`,
35
- 'Content-Type': 'application/json',
36
- },
37
- body: body !== undefined ? JSON.stringify(body) : undefined,
38
- });
39
-
40
- if (!res.ok) {
41
- let msg = `${method} ${path} → ${res.status}`;
42
- try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
43
- throw new Error(msg);
44
- }
45
-
46
- const text = await res.text();
47
- return text ? JSON.parse(text) : null;
48
- }
49
-
50
- export const get = (path) => request('GET', path);
51
- export const post = (path, body) => request('POST', path, body);
52
- export const del = (path) => request('DELETE', path);
1
+ import { readConfig, writeConfig, getActiveEnv, setEnvCredentials, ENVS } from './config.js';
2
+ import { refreshVelaroToken } from './oauth.js';
3
+
4
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
+
6
+ export async function getCredentials(envOverride) {
7
+ const cfg = readConfig();
8
+ const env = envOverride || cfg.activeEnv || 'prod';
9
+ let creds = cfg.envs?.[env];
10
+
11
+ if (!creds?.velaroToken) {
12
+ throw new Error(`Not logged in to ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
13
+ }
14
+
15
+ if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
16
+ if (!creds.entraRefreshToken) {
17
+ throw new Error(`Session expired for ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
18
+ }
19
+ const refreshed = await refreshVelaroToken({ ...creds, apiBase: creds.adminApiBase });
20
+ const updated = { ...creds, ...refreshed };
21
+ setEnvCredentials(env, updated);
22
+ creds = updated;
23
+ }
24
+
25
+ return { ...creds, env };
26
+ }
27
+
28
+ function joinUrl(base, path) {
29
+ return `${base.replace(/\/+$/, '')}/${String(path).replace(/^\/+/, '')}`;
30
+ }
31
+
32
+ // Exported under a distinct name for unit testing only — request()/messagingRequest()
33
+ // remain the real call sites, this is not part of the public CLI API surface.
34
+ export const joinUrlForTest = joinUrl;
35
+
36
+ export async function request(method, path, body, envOverride) {
37
+ const creds = await getCredentials(envOverride);
38
+
39
+ const res = await fetch(joinUrl(creds.adminApiBase, path), {
40
+ method,
41
+ headers: {
42
+ Authorization: `Bearer ${creds.velaroToken}`,
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ body: body !== undefined ? JSON.stringify(body) : undefined,
46
+ });
47
+
48
+ if (!res.ok) {
49
+ let msg = `${method} ${path} → ${res.status}`;
50
+ try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
51
+ throw new Error(msg);
52
+ }
53
+
54
+ const text = await res.text();
55
+ return text ? JSON.parse(text) : null;
56
+ }
57
+
58
+ export const get = (path) => request('GET', path);
59
+ export const post = (path, body) => request('POST', path, body);
60
+ export const put = (path, body) => request('PUT', path, body);
61
+ export const del = (path) => request('DELETE', path);
62
+
63
+ export async function messagingRequest(method, path, body, envOverride) {
64
+ const creds = await getCredentials(envOverride);
65
+ const res = await fetch(joinUrl(creds.messagingApiBase, path), {
66
+ method,
67
+ headers: {
68
+ Authorization: `Bearer ${creds.velaroToken}`,
69
+ 'Content-Type': 'application/json',
70
+ // ClientControllerBase.SiteId reads this header but only honors it when it matches the
71
+ // JWT's own siteId claim, otherwise falling back to the token -- it's a hint, not a
72
+ // trust boundary override. Still required: every messaging* CLI command was silently
73
+ // 401ing with "missing_site_id" until this was added.
74
+ ...(creds.siteId ? { 'vmsg-siteId': String(creds.siteId) } : {}),
75
+ },
76
+ body: body !== undefined ? JSON.stringify(body) : undefined,
77
+ });
78
+ if (!res.ok) {
79
+ let msg = `${method} ${path} → ${res.status}`;
80
+ try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
81
+ throw new Error(msg);
82
+ }
83
+ const text = await res.text();
84
+ return text ? JSON.parse(text) : null;
85
+ }
86
+
87
+ export const messagingGet = (path) => messagingRequest('GET', path);
88
+ export const messagingPost = (path, body) => messagingRequest('POST', path, body);
89
+ export const messagingPut = (path, body) => messagingRequest('PUT', path, body);
90
+ export const messagingPatch = (path, body) => messagingRequest('PATCH', path, body);
91
+ export const messagingDel = (path) => messagingRequest('DELETE', path);
@@ -0,0 +1,46 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { joinUrlForTest as joinUrl } from './api.js';
4
+
5
+ // Regression test for the bug where request()/messagingRequest() concatenated
6
+ // adminApiBase/messagingApiBase directly with `path` (no separator). Config
7
+ // stores the base with no trailing slash and every command file omits the
8
+ // leading slash on its path (e.g. `Entitlements/${site}`), so the raw
9
+ // concatenation produced `https://api-admin-us-east.velaro.comEntitlements/1032`
10
+ // -- a malformed hostname that fails DNS resolution before the request is
11
+ // ever sent. This broke every `velaro entitlement *` and `velaro
12
+ // feature-discovery *` command in prod.
13
+
14
+ test('joinUrl inserts exactly one slash when base has none and path has none', () => {
15
+ assert.equal(
16
+ joinUrl('https://api-admin-us-east.velaro.com', 'Entitlements/1032/override'),
17
+ 'https://api-admin-us-east.velaro.com/Entitlements/1032/override'
18
+ );
19
+ });
20
+
21
+ test('joinUrl does not double the slash when base already has a trailing slash', () => {
22
+ assert.equal(
23
+ joinUrl('https://api-admin-us-east.velaro.com/', 'Entitlements/1032/override'),
24
+ 'https://api-admin-us-east.velaro.com/Entitlements/1032/override'
25
+ );
26
+ });
27
+
28
+ test('joinUrl does not double the slash when path already has a leading slash', () => {
29
+ assert.equal(
30
+ joinUrl('https://api-admin-us-east.velaro.com', '/Entitlements/1032/override'),
31
+ 'https://api-admin-us-east.velaro.com/Entitlements/1032/override'
32
+ );
33
+ });
34
+
35
+ test('joinUrl handles both a trailing and leading slash without doubling', () => {
36
+ assert.equal(
37
+ joinUrl('https://api-admin-us-east.velaro.com/', '/Entitlements/1032/override'),
38
+ 'https://api-admin-us-east.velaro.com/Entitlements/1032/override'
39
+ );
40
+ });
41
+
42
+ test('joinUrl result is always a parseable, correctly-hosted URL', () => {
43
+ const url = new URL(joinUrl('https://api-admin-us-east.velaro.com', 'Entitlements/features'));
44
+ assert.equal(url.hostname, 'api-admin-us-east.velaro.com');
45
+ assert.equal(url.pathname, '/Entitlements/features');
46
+ });
package/lib/banner.js ADDED
@@ -0,0 +1,76 @@
1
+ import { readConfig, writeConfig } from './config.js';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ const CONFIG_FILE = join(homedir(), '.velaro', 'config.json');
7
+
8
+ // True only when there's nothing on disk yet, OR what's on disk parses cleanly.
9
+ // False when a config file exists but is corrupt/unreadable right now (e.g. a
10
+ // concurrent `velaro` process mid-write) — in that case we must NOT let the
11
+ // banner's writeConfig() clobber it with an empty {envs:{}}, which would wipe
12
+ // the user's saved token/siteId.
13
+ function configFileIsSafeToOverwrite() {
14
+ if (!existsSync(CONFIG_FILE)) return true;
15
+ try {
16
+ JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ // Pure ASCII (0-127) block-letter wordmark. Do not add box-drawing/unicode chars.
24
+ const ART = [
25
+ " __ __ ______ __ ______ ______ ______ ",
26
+ "/\\ \\ /\\ \\/\\ ___\\/\\ \\ /\\ __ \\ /\\ == \\ /\\ __ \\ ",
27
+ "\\ \\ \\___\\ \\ \\ \\ __\\\\ \\ \\____\\ \\ __ \\\\ \\ __< \\ \\ \\/\\ \\",
28
+ " \\ \\_____\\ \\_\\ \\_____\\ \\_____\\\\ \\_\\ \\_\\\\ \\_\\ \\_\\\\ \\_____\\",
29
+ " \\/_____/\\/_/\\/_____/\\/_____/ \\/_/\\/_/ \\/_/ /_/ \\/_____/",
30
+ ];
31
+
32
+ const TAGLINE = 'AI-native customer engagement -- command line';
33
+
34
+ function colorize(line) {
35
+ // Velaro brand blue/cyan (ANSI 256 color 39), reset after.
36
+ return `\x1b[38;5;39m${line}\x1b[0m`;
37
+ }
38
+
39
+ export function printBanner() {
40
+ // Non-TTY (piped/redirected output, e.g. `velaro ... | jq`) must stay
41
+ // completely silent on stdout — machine-readable command output (several
42
+ // commands emit JSON via console.log) must never be prefixed with banner
43
+ // text. Only decorate a real interactive terminal.
44
+ if (!process.stdout.isTTY) return;
45
+
46
+ console.log('');
47
+ for (const line of ART) {
48
+ console.log(colorize(line));
49
+ }
50
+ console.log(colorize(` ${TAGLINE}`));
51
+ console.log('');
52
+ }
53
+
54
+ export function printBannerIfFirstRun() {
55
+ const cfg = readConfig();
56
+
57
+ if (cfg && cfg._bannerShown === true) {
58
+ return;
59
+ }
60
+
61
+ printBanner();
62
+
63
+ // Only persist the "shown" flag if the config file on disk is intact —
64
+ // readConfig() silently falls back to an empty {envs:{}} on ANY read/parse
65
+ // failure (corrupt file, concurrent writer), and blindly spreading that
66
+ // fallback back out via writeConfig() would permanently erase the user's
67
+ // saved token/siteId. If the file isn't safely readable right now, just
68
+ // let the banner show again next run instead of risking a destructive write.
69
+ if (configFileIsSafeToOverwrite()) {
70
+ try {
71
+ writeConfig({ ...cfg, _bannerShown: true });
72
+ } catch {
73
+ // Non-fatal — worst case the banner shows again next run.
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,133 @@
1
+ import { get, getCredentials } from '../api.js';
2
+
3
+ // ── helpers ──────────────────────────────────────────────────────────────────
4
+
5
+ const RESET = '\x1b[0m';
6
+ const DIM = '\x1b[2m';
7
+ const RED = '\x1b[31m';
8
+ const YEL = '\x1b[33m';
9
+ const CYN = '\x1b[36m';
10
+
11
+ function formatEntry(item) {
12
+ const ts = new Date(item.createdAt).toISOString().replace('T', ' ').slice(0, 19);
13
+ const sev = (item.severity ?? 'Info').padEnd(8);
14
+ const color = item.severity === 'Error' ? RED : item.severity === 'Warning' ? YEL : RESET;
15
+ const cat = `${DIM}[${item.category ?? '?'}]${RESET}`;
16
+ const int = item.integration ? `${CYN}${item.integration}${RESET} ` : '';
17
+ const lines = [`${DIM}${ts}${RESET} ${color}${sev}${RESET} ${int}${cat} ${item.message ?? ''}`];
18
+ if (item.additionalInfo) lines.push(` ${DIM}${item.additionalInfo}${RESET}`);
19
+ if (item.suppressedCount > 0) lines.push(` ${DIM}(+${item.suppressedCount} suppressed)${RESET}`);
20
+ return lines.join('\n');
21
+ }
22
+
23
+ function formatAuditEntry(e) {
24
+ const ts = new Date(e.createdAt).toISOString().replace('T', ' ').slice(0, 19);
25
+ const actor = e.actorType === 'AI_Moshky'
26
+ ? `${CYN}Moshky${RESET} (on behalf of ${e.initiatedByEmail ?? 'unknown'})`
27
+ : `${e.actorType === 'HumanStaff' ? YEL : RESET}${e.actorType}${RESET} ${e.actorEmail ?? ''}`.trim();
28
+ const lines = [`${DIM}${ts}${RESET} [${e.entityType}#${e.entityId}] ${e.action} ${DIM}by${RESET} ${actor}`];
29
+ if (e.oldValueJson) lines.push(` ${DIM}before:${RESET} ${e.oldValueJson}`);
30
+ if (e.newValueJson) lines.push(` ${DIM}after: ${RESET} ${e.newValueJson}`);
31
+ return lines.join('\n');
32
+ }
33
+
34
+ // ── audit-trail handler (distinct shape/endpoint from integration activity logs) ───
35
+
36
+ async function auditTrailHandler(argv) {
37
+ await getCredentials();
38
+
39
+ const params = new URLSearchParams();
40
+ if (argv.entityType) params.set('entityType', argv.entityType);
41
+ if (argv.entityId) params.set('entityId', argv.entityId);
42
+ params.set('take', String(Math.min(500, argv.take ?? 200)));
43
+
44
+ const qs = params.toString();
45
+ const data = await get(`/AuditTrail${qs ? '?' + qs : ''}`);
46
+
47
+ if (!data?.entries?.length) {
48
+ console.log('\nNo audit trail entries.');
49
+ return;
50
+ }
51
+
52
+ console.log(`\n${data.entries.length} audit trail entries`);
53
+ if (!data.fullDetailAvailable) {
54
+ console.log(`${DIM}${data.complianceUpsellMessage ?? 'Upgrade to a Compliance package for full before/after detail.'}${RESET}`);
55
+ }
56
+ console.log('');
57
+
58
+ for (const e of data.entries) console.log(formatAuditEntry(e));
59
+ console.log('');
60
+ }
61
+
62
+ // ── integration activity handler ────────────────────────────────────────────────
63
+
64
+ async function activityHandler(argv) {
65
+ await getCredentials(); // ensure logged in
66
+
67
+ if (argv.slug === 'audit-trail') return auditTrailHandler(argv);
68
+
69
+ // 'skills' is a convenience alias for AI tool logs
70
+ const isSkills = argv.slug === 'skills';
71
+ let path;
72
+ if (isSkills) {
73
+ path = '/Integration/Logs';
74
+ argv.category = 'AiTool';
75
+ } else if (argv.slug) {
76
+ path = `/Integration/Logs/${encodeURIComponent(argv.slug)}`;
77
+ } else {
78
+ path = '/Integration/Logs';
79
+ }
80
+
81
+ const params = new URLSearchParams();
82
+ if (argv.severity && argv.severity !== 'All') params.set('severity', argv.severity);
83
+ if (argv.category && argv.category !== 'All') params.set('category', argv.category);
84
+ if (argv.search) params.set('search', argv.search);
85
+ params.set('page', String(Math.max(1, argv.page ?? 1)));
86
+ params.set('pageSize', String(Math.min(100, argv.pageSize ?? 25)));
87
+
88
+ const qs = params.toString();
89
+ const data = await get(`${path}${qs ? '?' + qs : ''}`);
90
+
91
+ if (!data?.items?.length) {
92
+ console.log('\nNo activity log entries.');
93
+ return;
94
+ }
95
+
96
+ const label = isSkills ? 'AI skill calls' : argv.slug ? `${argv.slug} activity` : 'integration activity';
97
+ console.log(`\n${data.items.length} of ${data.total} ${label} entries\n`);
98
+
99
+ for (const item of data.items) console.log(formatEntry(item));
100
+
101
+ if (data.total > data.page * data.pageSize) {
102
+ console.log(`\n${DIM}More results — use --page ${data.page + 1}${RESET}`);
103
+ }
104
+
105
+ console.log('');
106
+ }
107
+
108
+ // ── command export ────────────────────────────────────────────────────────────
109
+
110
+ export const activityCommand = {
111
+ command: 'activity [slug]',
112
+ describe: 'View integration activity logs, or "audit-trail" for who-changed-what settings history',
113
+ builder: y => y
114
+ .positional('slug', {
115
+ describe: 'Integration name (HubSpot, Square…), "skills" for AI tool logs, or "audit-trail" for settings change history',
116
+ type: 'string',
117
+ })
118
+ .option('severity', {
119
+ describe: 'Info, Warning, Error',
120
+ type: 'string',
121
+ })
122
+ .option('category', {
123
+ describe: 'Auth, Config, Request, Response, AiTool, Setup, Error',
124
+ type: 'string',
125
+ })
126
+ .option('search', { describe: 'Message search', type: 'string' })
127
+ .option('page', { describe: 'Page number', default: 1, type: 'number' })
128
+ .option('pageSize', { describe: 'Results per page', default: 25, type: 'number' })
129
+ .option('entityType', { describe: '[audit-trail] Filter to one entity type, e.g. Workflow', type: 'string' })
130
+ .option('entityId', { describe: '[audit-trail] Filter to one specific entity', type: 'string' })
131
+ .option('take', { describe: '[audit-trail] Max results (default 200, max 500)', type: 'number' }),
132
+ handler: activityHandler,
133
+ };
@@ -0,0 +1,66 @@
1
+ import { request } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ // ── velaro acuity ─────────────────────────────────────────────────────────────
5
+ // Superadmin commands to configure Acuity Scheduling for any customer site.
6
+ // Requires a Velaro admin JWT (site 1032).
7
+
8
+ export const acuityCommand = {
9
+ command: 'acuity <subcommand>',
10
+ describe: 'Manage Acuity Scheduling integration for any site (Velaro admin only)',
11
+ builder: yargs => yargs
12
+ .command({
13
+ command: 'status',
14
+ describe: 'Show Acuity config for a site',
15
+ builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
16
+ handler: runCommand(async argv => {
17
+ const data = await request('GET', `/Acuity/superadmin/status?siteId=${argv.site}`);
18
+ if (!data.configured) {
19
+ console.log(`\nSite ${argv.site}: Acuity NOT configured.\n`);
20
+ return;
21
+ }
22
+ console.log(`\nSite ${argv.site}: Acuity configured ✓`);
23
+ console.log(` Account: ${data.accountName}`);
24
+ console.log(` Display name: ${data.displayName || '(none)'}`);
25
+ console.log(` Default type: ${data.defaultAppointmentTypeName || '(none)'}`);
26
+ console.log(` Timezone: ${data.timezone || '(none)'}\n`);
27
+ }),
28
+ })
29
+ .command({
30
+ command: 'configure',
31
+ describe: 'Set up or update Acuity for a site',
32
+ builder: y => y
33
+ .option('site', { type: 'number', demandOption: true, describe: 'Site ID' })
34
+ .option('user-id', { type: 'string', demandOption: true, describe: 'Acuity user ID' })
35
+ .option('api-key', { type: 'string', demandOption: true, describe: 'Acuity API key' })
36
+ .option('label', { type: 'string', demandOption: false, describe: 'Display name' })
37
+ .option('type-id', { type: 'number', demandOption: false, describe: 'Default appointment type ID' })
38
+ .option('type-name', { type: 'string', demandOption: false, describe: 'Default appointment type name' })
39
+ .option('calendar-id', { type: 'number', demandOption: false, describe: 'Default calendar ID' })
40
+ .option('timezone', { type: 'string', default: 'America/New_York', describe: 'IANA timezone' }),
41
+ handler: runCommand(async argv => {
42
+ const data = await request('POST', `/Acuity/superadmin/config?siteId=${argv.site}`, {
43
+ userId: argv['user-id'],
44
+ apiKey: argv['api-key'],
45
+ displayName: argv.label || '',
46
+ defaultAppointmentTypeId: argv['type-id'] || null,
47
+ defaultAppointmentTypeName: argv['type-name'] || '',
48
+ defaultCalendarId: argv['calendar-id'] || null,
49
+ timezone: argv.timezone,
50
+ });
51
+ if (!data.success) throw new Error(data.error || 'Configure failed');
52
+ console.log(`\n✓ Acuity configured for site ${argv.site}`);
53
+ console.log(` Account: ${data.accountName}\n`);
54
+ }),
55
+ })
56
+ .command({
57
+ command: 'disconnect',
58
+ describe: 'Remove Acuity config from a site',
59
+ builder: y => y.option('site', { type: 'number', demandOption: true, describe: 'Site ID' }),
60
+ handler: runCommand(async argv => {
61
+ await request('DELETE', `/Acuity/superadmin/config?siteId=${argv.site}`);
62
+ console.log(`\n✓ Acuity disconnected from site ${argv.site}\n`);
63
+ }),
64
+ })
65
+ .demandCommand(1, 'Specify a subcommand: status | configure | disconnect'),
66
+ };