@velaro/cli 1.2.0 → 1.4.9
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
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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);
|
package/lib/api.test.js
ADDED
|
@@ -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
|
+
};
|