@velaro/cli 1.4.85 → 1.4.87
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 +10 -2
- package/lib/banner.js +34 -23
- package/lib/banner.test.js +24 -0
- package/lib/commands/agent.js +4 -4
- package/lib/commands/check.js +8 -8
- package/lib/commands/deployment.js +1 -1
- package/lib/commands/kb.js +3 -3
- package/lib/commands/linked-identity.js +41 -0
- package/lib/commands/magento.js +2 -2
- package/lib/commands/rule.js +1 -1
- package/lib/commands/site.js +1 -1
- package/lib/commands/team.js +1 -1
- package/lib/commands/woocommerce.js +6 -6
- package/lib/messaging-routes.test.js +28 -0
- package/lib/update-check.js +30 -2
- package/package.json +1 -1
- package/scripts/postinstall.js +18 -5
package/bin/velaro.js
CHANGED
|
@@ -29,6 +29,7 @@ import { aiConfigCommand } from '../lib/commands/ai-config.js';
|
|
|
29
29
|
import { deploymentCommand } from '../lib/commands/deployment.js';
|
|
30
30
|
import { updateCommand } from '../lib/commands/update.js';
|
|
31
31
|
import { agentCommand } from '../lib/commands/agent.js';
|
|
32
|
+
import { linkedIdentityCommand } from '../lib/commands/linked-identity.js';
|
|
32
33
|
import { notificationsCommand } from '../lib/commands/notifications.js';
|
|
33
34
|
import { articleCommand } from '../lib/commands/article.js';
|
|
34
35
|
import { sharepointCommand } from '../lib/commands/sharepoint.js';
|
|
@@ -110,9 +111,15 @@ import { senderDomainHealthCommand } from '../lib/commands/sender-domain-health.
|
|
|
110
111
|
import { contactsCommand } from '../lib/commands/contacts.js';
|
|
111
112
|
import { contactListCommand } from '../lib/commands/contact-list.js';
|
|
112
113
|
import { startUpdateCheck } from '../lib/update-check.js';
|
|
113
|
-
import { printBannerIfFirstRun } from '../lib/banner.js';
|
|
114
|
+
import { printBanner, printBannerIfFirstRun } from '../lib/banner.js';
|
|
115
|
+
import { createRequire } from 'module';
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
// Banner on first run, and on the "front door" commands (bare `velaro`, help, login, update).
|
|
118
|
+
// Never on data commands or --version: their output is parsed by scripts.
|
|
119
|
+
const cliVersion = createRequire(import.meta.url)('../package.json').version;
|
|
120
|
+
const firstArg = process.argv[2];
|
|
121
|
+
const frontDoor = !firstArg || ['help', '--help', '-h', 'login', 'update'].includes(firstArg);
|
|
122
|
+
if (!printBannerIfFirstRun() && frontDoor) printBanner({ version: cliVersion });
|
|
116
123
|
|
|
117
124
|
const printUpdateNoticeIfStale = startUpdateCheck();
|
|
118
125
|
|
|
@@ -147,6 +154,7 @@ await yargs(hideBin(process.argv))
|
|
|
147
154
|
.command(deploymentCommand)
|
|
148
155
|
.command(updateCommand)
|
|
149
156
|
.command(agentCommand)
|
|
157
|
+
.command(linkedIdentityCommand)
|
|
150
158
|
.command(notificationsCommand)
|
|
151
159
|
.command(articleCommand)
|
|
152
160
|
.command(sharepointCommand)
|
package/lib/banner.js
CHANGED
|
@@ -20,42 +20,52 @@ function configFileIsSafeToOverwrite() {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Pure ASCII (0-127)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
]
|
|
23
|
+
// Pure ASCII (0-127) so it renders in every Windows/PowerShell codepage. Letters are composed from
|
|
24
|
+
// per-letter rows so the wordmark can never misalign; each row gets its own gradient colour.
|
|
25
|
+
const GLYPHS = {
|
|
26
|
+
V: ['__ __', '\\ \\ / /', ' \\ \\ / / ', ' \\ \\/ / ', ' \\ / ', ' \\/ '],
|
|
27
|
+
E: [' ______ ', '| ____|', '| |__ ', '| __| ', '| |____ ', '|______|'],
|
|
28
|
+
L: [' _ ', '| | ', '| | ', '| | ', '| |____ ', '|______| '],
|
|
29
|
+
A: [' ', ' /\\ ', ' / \\ ', ' / /\\ \\ ', ' / ____ \\ ', '/_/ \\_\\'],
|
|
30
|
+
R: [' _____ ', '| __ \\ ', '| |__) | ', '| _ / ', '| | \\ \\ ', '|_| \\_\\ '],
|
|
31
|
+
O: [' ____ ', ' / __ \\ ', '| | | | ', '| | | | ', '| |__| | ', ' \\____/ '],
|
|
32
|
+
};
|
|
33
|
+
const WORD = 'VELARO';
|
|
34
|
+
const ART = Array.from({ length: 6 }, (_, row) => WORD.split('').map((ch) => GLYPHS[ch][row]).join(' ').replace(/\s+$/, ''));
|
|
31
35
|
|
|
36
|
+
// Brand gradient, top to bottom: Velaro blue -> cyan -> green (ANSI 256 colours).
|
|
37
|
+
const GRADIENT = [27, 33, 39, 45, 49, 48];
|
|
32
38
|
const TAGLINE = 'AI-native customer engagement -- command line';
|
|
33
39
|
|
|
34
|
-
function colorize(line) {
|
|
35
|
-
|
|
36
|
-
return `\x1b[38;5;39m${line}\x1b[0m`;
|
|
40
|
+
function colorize(line, row) {
|
|
41
|
+
return `\x1b[1;38;5;${GRADIENT[row % GRADIENT.length]}m${line}\x1b[0m`;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Print the banner on an interactive terminal. Non-TTY (piped/redirected output, e.g.
|
|
46
|
+
* `velaro ... | jq`) stays completely silent on stdout - machine-readable output must never be
|
|
47
|
+
* prefixed with banner text.
|
|
48
|
+
*/
|
|
49
|
+
export function printBanner({ version } = {}) {
|
|
44
50
|
if (!process.stdout.isTTY) return;
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const out = ['', ...ART.map((line, i) => colorize(line, i)),
|
|
53
|
+
`\x1b[38;5;245m ${TAGLINE}${version ? ` v${version}` : ''}\x1b[0m`, ''];
|
|
54
|
+
console.log(out.join('\n'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Banner as a plain string for postinstall, which writes straight to the terminal device. */
|
|
58
|
+
export function bannerText(version) {
|
|
59
|
+
return ['', ...ART.map((line, i) => colorize(line, i)),
|
|
60
|
+
`\x1b[38;5;245m ${TAGLINE}${version ? ` v${version}` : ''}\x1b[0m`,
|
|
61
|
+
'\x1b[38;5;39m Installed. Run `velaro login` to get started, `velaro --help` for everything else.\x1b[0m', ''].join('\n');
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export function printBannerIfFirstRun() {
|
|
55
65
|
const cfg = readConfig();
|
|
56
66
|
|
|
57
67
|
if (cfg && cfg._bannerShown === true) {
|
|
58
|
-
return;
|
|
68
|
+
return false;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
71
|
printBanner();
|
|
@@ -73,4 +83,5 @@ export function printBannerIfFirstRun() {
|
|
|
73
83
|
// Non-fatal — worst case the banner shows again next run.
|
|
74
84
|
}
|
|
75
85
|
}
|
|
86
|
+
return true;
|
|
76
87
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { bannerText } from './banner.js';
|
|
4
|
+
|
|
5
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
6
|
+
|
|
7
|
+
test('banner is pure ASCII and spells VELARO in six rows', () => {
|
|
8
|
+
const text = strip(bannerText('9.9.9'));
|
|
9
|
+
assert.ok(/^[\x00-\x7f]*$/.test(text), 'banner must stay ASCII so every Windows codepage renders it');
|
|
10
|
+
const art = text.split('\n').filter((l) => /[\\/|_]/.test(l)).slice(0, 6);
|
|
11
|
+
assert.equal(art.length, 6);
|
|
12
|
+
assert.ok(text.includes('v9.9.9'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('banner stays silent when stdout is not a TTY (piped output must be machine-readable)', async () => {
|
|
16
|
+
const { printBanner } = await import('./banner.js');
|
|
17
|
+
const lines = [];
|
|
18
|
+
const orig = console.log;
|
|
19
|
+
const tty = process.stdout.isTTY;
|
|
20
|
+
console.log = (...a) => lines.push(a.join(' '));
|
|
21
|
+
process.stdout.isTTY = false;
|
|
22
|
+
try { printBanner({ version: '1' }); } finally { console.log = orig; process.stdout.isTTY = tty; }
|
|
23
|
+
assert.deepEqual(lines, []);
|
|
24
|
+
});
|
package/lib/commands/agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, post, messagingPost, getCredentials } from '../api.js';
|
|
1
|
+
import { get, post, messagingGet, messagingPost, getCredentials } from '../api.js';
|
|
2
2
|
import { runCommand } from '../run.js';
|
|
3
3
|
|
|
4
4
|
const VALID_STATUSES = ['Available', 'Away', 'Offline'];
|
|
@@ -32,8 +32,8 @@ export const agentCommand = {
|
|
|
32
32
|
describe: 'List agents and their current availability',
|
|
33
33
|
handler: runCommand(async () => {
|
|
34
34
|
const [agents, teams] = await Promise.all([
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
messagingGet('/Users/List'),
|
|
36
|
+
messagingGet('/Teams/List'),
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
39
|
const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
|
|
@@ -104,7 +104,7 @@ export const agentCommand = {
|
|
|
104
104
|
.option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
|
|
105
105
|
.option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
|
|
106
106
|
handler: runCommand(async (argv) => {
|
|
107
|
-
await
|
|
107
|
+
await messagingPost('/UserStatus/admin', { userId: argv.id, status: argv.status });
|
|
108
108
|
console.log(`Agent ${argv.id} set to ${argv.status}.`);
|
|
109
109
|
}),
|
|
110
110
|
})
|
package/lib/commands/check.js
CHANGED
|
@@ -21,7 +21,7 @@ export const checkCommand = {
|
|
|
21
21
|
handler: runCommand(async () => {
|
|
22
22
|
const [sub, apiStatus] = await Promise.all([
|
|
23
23
|
getSubscription().catch(() => null),
|
|
24
|
-
|
|
24
|
+
messagingGet('/api/health').catch(() => null),
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
if (!sub) {
|
|
@@ -37,13 +37,13 @@ export const checkCommand = {
|
|
|
37
37
|
if (apiStatus) {
|
|
38
38
|
ok(`API reachable`);
|
|
39
39
|
} else {
|
|
40
|
-
warn('API did not respond to /
|
|
40
|
+
warn('Messaging API did not respond to /api/health');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// ── Deployments ─────────────────────────────────────────────────────────
|
|
44
44
|
console.log('\n── Deployments ───────────────────────────────────────────────');
|
|
45
45
|
try {
|
|
46
|
-
const [deps, teams] = await Promise.all([
|
|
46
|
+
const [deps, teams] = await Promise.all([messagingGet('/Deployment'), messagingGet('/Teams/List')]);
|
|
47
47
|
const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
|
|
48
48
|
if (deps?.length) {
|
|
49
49
|
for (const d of deps) {
|
|
@@ -75,7 +75,7 @@ export const checkCommand = {
|
|
|
75
75
|
try {
|
|
76
76
|
const [qna, overrides] = await Promise.all([
|
|
77
77
|
messagingGet('/BotQnA').catch(() => []),
|
|
78
|
-
|
|
78
|
+
messagingGet('/Ticket/knowledge-overrides').catch(() => []),
|
|
79
79
|
]);
|
|
80
80
|
ok(`${qna?.length ?? 0} Q&A pair(s) · ${overrides?.length ?? 0} override(s)`);
|
|
81
81
|
} catch { warn('Could not load knowledge base'); }
|
|
@@ -87,7 +87,7 @@ export const checkCommand = {
|
|
|
87
87
|
console.log('\n── Workflows ─────────────────────────────────────────────────');
|
|
88
88
|
if (sub.enableAutomation) {
|
|
89
89
|
try {
|
|
90
|
-
const workflows = (await
|
|
90
|
+
const workflows = (await messagingGet('/Workflows/List') ?? []).filter(w => !w.isTemplate);
|
|
91
91
|
const enabled = workflows.filter(w => w.enabled).length;
|
|
92
92
|
const disabled = workflows.length - enabled;
|
|
93
93
|
if (workflows.length) {
|
|
@@ -108,7 +108,7 @@ export const checkCommand = {
|
|
|
108
108
|
console.log('\n── Routing Rules ─────────────────────────────────────────────');
|
|
109
109
|
if (sub.enableWorkflowRules) {
|
|
110
110
|
try {
|
|
111
|
-
const rules = await
|
|
111
|
+
const rules = await messagingGet('/Rules/List');
|
|
112
112
|
if (rules?.length) {
|
|
113
113
|
ok(`${rules.length} routing rule(s) configured`);
|
|
114
114
|
} else {
|
|
@@ -138,7 +138,7 @@ export const checkCommand = {
|
|
|
138
138
|
// ── IVR detail ───────────────────────────────────────────────────────────
|
|
139
139
|
if (sub.enableIvr) {
|
|
140
140
|
try {
|
|
141
|
-
const ivr = await
|
|
141
|
+
const ivr = await messagingGet('/api/ivr/config');
|
|
142
142
|
if (ivr?.phoneNumber) {
|
|
143
143
|
console.log(`\n── IVR Config ────────────────────────────────────────────────`);
|
|
144
144
|
ok(`Phone: ${ivr.phoneNumber}`);
|
|
@@ -151,7 +151,7 @@ export const checkCommand = {
|
|
|
151
151
|
// ── Agents ───────────────────────────────────────────────────────────────
|
|
152
152
|
console.log('\n── Agents ────────────────────────────────────────────────────');
|
|
153
153
|
try {
|
|
154
|
-
const agents = await
|
|
154
|
+
const agents = await messagingGet('/Users/List');
|
|
155
155
|
const online = (agents ?? []).filter(a => a.status === 'Available').length;
|
|
156
156
|
const away = (agents ?? []).filter(a => a.status === 'Away').length;
|
|
157
157
|
const offline = (agents ?? []).length - online - away;
|
package/lib/commands/kb.js
CHANGED
|
@@ -294,7 +294,7 @@ const overrideCommand = {
|
|
|
294
294
|
describe: 'List knowledge overrides',
|
|
295
295
|
handler: runCommand(async () => {
|
|
296
296
|
await requireFeature('enableKnowledgeBase', 'Knowledge Base');
|
|
297
|
-
const items = await
|
|
297
|
+
const items = await messagingGet('/Ticket/knowledge-overrides');
|
|
298
298
|
if (!items?.length) { console.log('No overrides found.'); return; }
|
|
299
299
|
|
|
300
300
|
console.log(`\nFound ${items.length} override(s):\n`);
|
|
@@ -317,7 +317,7 @@ const overrideCommand = {
|
|
|
317
317
|
await requireFeature('enableKnowledgeBase', 'Knowledge Base');
|
|
318
318
|
const payload = { title: argv.title, content: argv.content, isActive: true };
|
|
319
319
|
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
320
|
-
const result = await
|
|
320
|
+
const result = await messagingPost('/Ticket/knowledge-overrides', payload);
|
|
321
321
|
console.log(`Override added: [${result.id}] "${argv.title}"`);
|
|
322
322
|
}),
|
|
323
323
|
})
|
|
@@ -327,7 +327,7 @@ const overrideCommand = {
|
|
|
327
327
|
builder: (y) => y.positional('id', { type: 'number', describe: 'Override ID' }),
|
|
328
328
|
handler: runCommand(async (argv) => {
|
|
329
329
|
await requireFeature('enableKnowledgeBase', 'Knowledge Base');
|
|
330
|
-
await
|
|
330
|
+
await messagingDel(`/Ticket/knowledge-overrides/${argv.id}`);
|
|
331
331
|
console.log(`Override ${argv.id} deleted.`);
|
|
332
332
|
}),
|
|
333
333
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { messagingGet, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
function fmtDate(value) {
|
|
5
|
+
if (!value) return '—';
|
|
6
|
+
const d = new Date(value);
|
|
7
|
+
return Number.isNaN(d.getTime()) ? String(value) : d.toISOString().replace('T', ' ').slice(0, 16) + 'Z';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const linkedIdentityCommand = {
|
|
11
|
+
command: 'linked-identity <subcommand>',
|
|
12
|
+
describe: 'See and remove the Google / Microsoft / company sign-in methods linked to your site\'s users',
|
|
13
|
+
builder: (yargs) => yargs
|
|
14
|
+
.command({
|
|
15
|
+
command: 'list',
|
|
16
|
+
describe: 'List linked sign-in methods. Use when offboarding: disabling a person does not stop a linked Google sign-in.',
|
|
17
|
+
handler: runCommand(async () => {
|
|
18
|
+
const rows = await messagingGet('/admin/security/linked-identities');
|
|
19
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
20
|
+
console.log('No linked sign-in methods.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const emailW = Math.max(...rows.map(r => (r.userEmail ?? '').length), 5);
|
|
24
|
+
for (const r of rows) {
|
|
25
|
+
console.log(`[${String(r.id).padStart(4)}] ${(r.userEmail ?? '—').padEnd(emailW)} ${(r.method ?? r.idpKind).padEnd(24)} linked ${fmtDate(r.linkedAt)} last used ${fmtDate(r.lastLoginAt)}`);
|
|
26
|
+
}
|
|
27
|
+
console.log(`\n${rows.length} linked sign-in method(s)`);
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
.command({
|
|
31
|
+
command: 'remove <id>',
|
|
32
|
+
describe: 'Remove one linked sign-in method by id (from `list`). The person keeps any other way they can sign in.',
|
|
33
|
+
builder: (y) => y.positional('id', { describe: 'Link id from `linked-identity list`', type: 'number' }),
|
|
34
|
+
handler: runCommand(async (argv) => {
|
|
35
|
+
if (!Number.isInteger(argv.id) || argv.id <= 0) throw new Error('id must be a positive whole number.');
|
|
36
|
+
await messagingDel(`/admin/security/linked-identities/${argv.id}`);
|
|
37
|
+
console.log(`Removed linked sign-in method ${argv.id}.`);
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
.demandCommand(1),
|
|
41
|
+
};
|
package/lib/commands/magento.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* velaro magento diagnose Live connection diagnostic (token, /rest/V1, scopes)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { request } from '../api.js';
|
|
13
|
+
import { request, messagingRequest } from '../api.js';
|
|
14
14
|
|
|
15
15
|
const GRN = '\x1b[32m';
|
|
16
16
|
const RED = '\x1b[31m';
|
|
@@ -164,7 +164,7 @@ async function diagnoseHandler(argv) {
|
|
|
164
164
|
}
|
|
165
165
|
try {
|
|
166
166
|
// Messaging DiagController: GET diagnostics/test-magento?siteId=<id>
|
|
167
|
-
const r = await
|
|
167
|
+
const r = await messagingRequest('GET', `/diagnostics/test-magento?siteId=${encodeURIComponent(siteId)}`);
|
|
168
168
|
console.log(`\n${BOLD}Magento diagnostic (site ${siteId})${RST}\n`);
|
|
169
169
|
console.log(JSON.stringify(r, null, 2));
|
|
170
170
|
console.log('');
|
package/lib/commands/rule.js
CHANGED
package/lib/commands/site.js
CHANGED
|
@@ -32,7 +32,7 @@ export const siteCommand = {
|
|
|
32
32
|
handler: runCommand(async () => {
|
|
33
33
|
const [sub, agents] = await Promise.all([
|
|
34
34
|
messagingGet('/Subscription'),
|
|
35
|
-
|
|
35
|
+
messagingGet('/Users/List'),
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
38
|
// Active features — friendly names only, no internal flag names
|
package/lib/commands/team.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* velaro woocommerce logs View integration activity logs
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { request } from '../api.js';
|
|
13
|
+
import { request, messagingRequest } from '../api.js';
|
|
14
14
|
|
|
15
15
|
const GRN = '\x1b[32m';
|
|
16
16
|
const RED = '\x1b[31m';
|
|
@@ -25,7 +25,7 @@ const RST = '\x1b[0m';
|
|
|
25
25
|
async function statusHandler(_argv) {
|
|
26
26
|
// /WooCommerce/billing/status resolves site from the JWT — no siteId param needed.
|
|
27
27
|
// Returns: billingStatus, planName, oauthConnected, hasActiveSubscription.
|
|
28
|
-
const r = await
|
|
28
|
+
const r = await messagingRequest('GET', `/WooCommerce/billing/status`);
|
|
29
29
|
if (!r) {
|
|
30
30
|
console.log('\nNo WooCommerce connection found for this site.\n');
|
|
31
31
|
return;
|
|
@@ -49,7 +49,7 @@ async function connectHandler(argv) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
|
-
const r = await
|
|
52
|
+
const r = await messagingRequest('GET', `/WooCommerce/auth/start?storeUrl=${encodeURIComponent(argv.storeUrl)}`);
|
|
53
53
|
if (!r?.authUrl) {
|
|
54
54
|
console.error(`${RED}✗ Server did not return an authUrl — check that the store URL is correct${RST}`);
|
|
55
55
|
process.exitCode = 1;
|
|
@@ -70,7 +70,7 @@ async function connectHandler(argv) {
|
|
|
70
70
|
async function disconnectHandler(argv) {
|
|
71
71
|
const params = argv.site ? `?siteId=${argv.site}` : '';
|
|
72
72
|
try {
|
|
73
|
-
await
|
|
73
|
+
await messagingRequest('POST', `/WooCommerce/auth/disconnect${params}`, {});
|
|
74
74
|
console.log(`\n${GRN}✓ WooCommerce store disconnected${RST}\n`);
|
|
75
75
|
} catch (e) {
|
|
76
76
|
console.error(`\n${RED}✗ Disconnect failed: ${e.message}${RST}\n`);
|
|
@@ -110,7 +110,7 @@ async function ordersHandler(argv) {
|
|
|
110
110
|
}
|
|
111
111
|
const q = argv.id ?? argv.email;
|
|
112
112
|
const limit = Math.min(50, argv.limit ?? 10);
|
|
113
|
-
const orders = await
|
|
113
|
+
const orders = await messagingRequest('GET', `/WooCommerce/data/orders?email=${encodeURIComponent(q)}&limit=${limit}`);
|
|
114
114
|
if (!Array.isArray(orders) || orders.length === 0) {
|
|
115
115
|
console.log(`\nNo orders found for "${q}".\n`);
|
|
116
116
|
return;
|
|
@@ -135,7 +135,7 @@ async function productsHandler(argv) {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
const limit = Math.min(50, argv.limit ?? 10);
|
|
138
|
-
const products = await
|
|
138
|
+
const products = await messagingRequest('GET', `/WooCommerce/data/products?query=${encodeURIComponent(argv.query)}&limit=${limit}`);
|
|
139
139
|
if (!Array.isArray(products) || products.length === 0) {
|
|
140
140
|
console.log(`\nNo products found for "${argv.query}".\n`);
|
|
141
141
|
return;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Regression (2026-10-11): `velaro team list` / `deployment list` / `rule list` / `agent list` etc.
|
|
2
|
+
// 404'd on every environment because they called the ADMIN api for routes that only exist on the
|
|
3
|
+
// MESSAGING api. Admin has no controller for these prefixes; any command using admin get/post/del
|
|
4
|
+
// (or request()) for one of them is broken.
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'commands');
|
|
12
|
+
const MESSAGING_ONLY = ['/Teams/List', '/Deployment', '/Users/List', '/Workflows/List', '/Rules', '/Ticket/knowledge-overrides', '/UserStatus/admin', '/api/ivr/config', '/WooCommerce/billing', '/WooCommerce/auth', '/diagnostics/test-magento'];
|
|
13
|
+
|
|
14
|
+
test('messaging-only routes are never called through the admin api helpers', () => {
|
|
15
|
+
const offenders = [];
|
|
16
|
+
for (const f of fs.readdirSync(dir).filter(n => n.endsWith('.js'))) {
|
|
17
|
+
const src = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
18
|
+
// `get`/`post`/`del` imported unaliased from api.js are the ADMIN helpers.
|
|
19
|
+
const imp = /import\s*\{([^}]*)\}\s*from\s*'\.\.\/api\.js'/.exec(src);
|
|
20
|
+
const specifiers = imp ? imp[1].split(',').map(s => s.trim()) : [];
|
|
21
|
+
const importsAdminHelpers = specifiers.some(s => ['get', 'post', 'del'].includes(s));
|
|
22
|
+
for (const route of MESSAGING_ONLY) {
|
|
23
|
+
const admin = new RegExp(String.raw`(?<![A-Za-z])(${importsAdminHelpers ? 'get|post|del|' : ''}request\('(?:GET|POST|PUT|DELETE)',)\s*\(?\s*['"\`]${route.replace(/[/.]/g, '\\$&')}`);
|
|
24
|
+
if (admin.test(src)) offenders.push(`${f}: ${route}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
assert.deepEqual(offenders, [], `messaging-only routes called via the admin api:\n${offenders.join('\n')}`);
|
|
28
|
+
});
|
package/lib/update-check.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { readConfig, writeConfig } from './config.js';
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { realpathSync } from 'fs';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
9
12
|
|
|
10
13
|
const require = createRequire(import.meta.url);
|
|
11
14
|
const { version: currentVersion } = require('../package.json');
|
|
@@ -43,16 +46,41 @@ export function startUpdateCheck() {
|
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
// A registry install lives under node_modules; a dev checkout (npm link) resolves to the repo.
|
|
50
|
+
export function isRegistryInstall() {
|
|
51
|
+
try {
|
|
52
|
+
return /[\\/]node_modules[\\/]@velaro[\\/]cli[\\/]/.test(realpathSync(fileURLToPath(import.meta.url)));
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
function printNoticeIfStale(latest) {
|
|
47
59
|
if (!latest || latest === currentVersion) return;
|
|
48
60
|
if (!isNewer(latest, currentVersion)) return;
|
|
49
61
|
|
|
62
|
+
// Keep registry installs current automatically (opt out: VELARO_NO_AUTO_UPDATE=1). Detached and
|
|
63
|
+
// silent so it never slows or breaks the command that just ran; the next run is the new version.
|
|
64
|
+
if (isRegistryInstall() && !process.env.VELARO_NO_AUTO_UPDATE && !process.env.CI) {
|
|
65
|
+
try {
|
|
66
|
+
const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['install', '-g', '@velaro/cli@latest'],
|
|
67
|
+
{ detached: true, stdio: 'ignore', shell: process.platform === 'win32' });
|
|
68
|
+
child.on('error', () => {});
|
|
69
|
+
child.unref();
|
|
70
|
+
process.stderr.write(`\n Velaro CLI ${currentVersion} → ${latest}: updating in the background (VELARO_NO_AUTO_UPDATE=1 to disable).\n\n`);
|
|
71
|
+
return;
|
|
72
|
+
} catch {
|
|
73
|
+
// fall through to the manual notice
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
process.stderr.write(
|
|
51
78
|
`\n Update available: ${currentVersion} → ${latest}\n` +
|
|
52
|
-
|
|
79
|
+
(isRegistryInstall()
|
|
80
|
+
? ` Run: npm install -g @velaro/cli@latest\n\n`
|
|
81
|
+
: ` This is a dev checkout (npm link): git pull in velaro-admin/cli, or switch to the registry install: npm install -g @velaro/cli@latest\n\n`)
|
|
53
82
|
);
|
|
54
83
|
}
|
|
55
|
-
|
|
56
84
|
function isNewer(a, b) {
|
|
57
85
|
const pa = a.split('.').map(Number);
|
|
58
86
|
const pb = b.split('.').map(Number);
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
|
-
// Best-effort banner on install. Must NEVER throw
|
|
2
|
-
//
|
|
1
|
+
// Best-effort banner on install/upgrade. Must NEVER throw - a postinstall failure can fail the
|
|
2
|
+
// whole `npm install`. npm hides a lifecycle script's stdout, so write straight to the terminal
|
|
3
|
+
// device (the same trick other CLIs use) and fall back to stderr.
|
|
4
|
+
import { openSync, writeSync, closeSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
3
7
|
try {
|
|
4
|
-
const {
|
|
5
|
-
|
|
8
|
+
const { bannerText } = await import('../lib/banner.js');
|
|
9
|
+
const { version } = createRequire(import.meta.url)('../package.json');
|
|
10
|
+
const text = bannerText(version) + '\n';
|
|
11
|
+
let wrote = false;
|
|
12
|
+
try {
|
|
13
|
+
const fd = openSync(process.platform === 'win32' ? '\\\\.\\CON' : '/dev/tty', 'w');
|
|
14
|
+
try { writeSync(fd, text); wrote = true; } finally { closeSync(fd); }
|
|
15
|
+
} catch {
|
|
16
|
+
// no controlling terminal (CI, piped install) - fall through
|
|
17
|
+
}
|
|
18
|
+
if (!wrote && !process.env.CI) process.stderr.write(text);
|
|
6
19
|
} catch (err) {
|
|
7
20
|
try {
|
|
8
21
|
process.stderr.write(`[velaro] postinstall banner skipped: ${err && err.message ? err.message : err}\n`);
|
|
9
22
|
} catch {
|
|
10
|
-
// swallow
|
|
23
|
+
// swallow - nothing more we can do
|
|
11
24
|
}
|
|
12
25
|
}
|