@velaro/cli 0.5.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,62 @@
1
- import { get } from '../api.js';
2
- import { runCommand } from '../run.js';
3
-
4
- const FEATURE_MAP = {
5
- enableWeb: 'Web Chat',
6
- enableSms: 'SMS',
7
- enableEmail: 'Email',
8
- enableWhatsapp: 'WhatsApp',
9
- enableIvr: 'IVR / Voice',
10
- enableFacebook: 'Facebook Messenger',
11
- enableInstagram: 'Instagram',
12
- enableAI: 'AI Bots',
13
- enableKnowledgeBase: 'Knowledge Base',
14
- enableAutomation: 'Workflows',
15
- enableWorkflowRules: 'Routing Rules',
16
- enableChatTranslations: 'Chat Translation',
17
- enableVideoChat: 'Video Chat',
18
- enableTicketing: 'Ticketing',
19
- enableAgentDashboard: 'Agent Dashboard',
20
- enableMcpApi: 'MCP API Gateway',
21
- enableOutboundCampaigns: 'Email Campaigns',
22
- enableRcs: 'RCS Messaging',
23
- enablePageWidgets: 'Page Widgets',
24
- enableVisitorTracking: 'Visitor Tracking',
25
- };
26
-
27
- const STATUS_LABEL = { Available: 'online', Away: 'away', Offline: 'offline' };
28
-
29
- export const siteCommand = {
30
- command: 'site',
31
- describe: 'Show site overview: active features and agent availability',
32
- handler: runCommand(async () => {
33
- const [sub, agents] = await Promise.all([
34
- get('/Subscription'),
35
- get('/Users/List'),
36
- ]);
37
-
38
- // Active features — friendly names only, no internal flag names
39
- const active = Object.entries(FEATURE_MAP)
40
- .filter(([key]) => sub[key] === true)
41
- .map(([, label]) => label);
42
-
43
- console.log('\nActive features:');
44
- if (active.length) {
45
- console.log(' ' + active.join(' · '));
46
- } else {
47
- console.log(' (none)');
48
- }
49
-
50
- if (sub.maxCreatedUsers) {
51
- console.log(`\nAgent seats: ${sub.maxCreatedUsers} licensed`);
52
- }
53
-
54
- // Agent availability summary
55
- const counts = { online: 0, away: 0, offline: 0 };
56
- for (const a of agents) {
57
- const key = STATUS_LABEL[a.status] ?? 'offline';
58
- counts[key]++;
59
- }
60
- console.log(`Agent status: ${counts.online} online · ${counts.away} away · ${counts.offline} offline\n`);
61
- }),
62
- };
1
+ import { get } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const FEATURE_MAP = {
5
+ enableWeb: 'Web Chat',
6
+ enableSms: 'SMS',
7
+ enableEmail: 'Email',
8
+ enableWhatsapp: 'WhatsApp',
9
+ enableIvr: 'IVR / Voice',
10
+ enableFacebook: 'Facebook Messenger',
11
+ enableInstagram: 'Instagram',
12
+ enableAI: 'AI Bots',
13
+ enableKnowledgeBase: 'Knowledge Base',
14
+ enableAutomation: 'Workflows',
15
+ enableWorkflowRules: 'Routing Rules',
16
+ enableChatTranslations: 'Chat Translation',
17
+ enableVideoChat: 'Video Chat',
18
+ enableTicketing: 'Ticketing',
19
+ enableAgentDashboard: 'Agent Dashboard',
20
+ enableMcpApi: 'MCP API Gateway',
21
+ enableOutboundCampaigns: 'Email Campaigns',
22
+ enableRcs: 'RCS Messaging',
23
+ enablePageWidgets: 'Page Widgets',
24
+ enableVisitorTracking: 'Visitor Tracking',
25
+ };
26
+
27
+ const STATUS_LABEL = { Available: 'online', Away: 'away', Offline: 'offline' };
28
+
29
+ export const siteCommand = {
30
+ command: 'site',
31
+ describe: 'Show site overview: active features and agent availability',
32
+ handler: runCommand(async () => {
33
+ const [sub, agents] = await Promise.all([
34
+ get('/Subscription'),
35
+ get('/Users/List'),
36
+ ]);
37
+
38
+ // Active features — friendly names only, no internal flag names
39
+ const active = Object.entries(FEATURE_MAP)
40
+ .filter(([key]) => sub[key] === true)
41
+ .map(([, label]) => label);
42
+
43
+ console.log('\nActive features:');
44
+ if (active.length) {
45
+ console.log(' ' + active.join(' · '));
46
+ } else {
47
+ console.log(' (none)');
48
+ }
49
+
50
+ if (sub.maxCreatedUsers) {
51
+ console.log(`\nAgent seats: ${sub.maxCreatedUsers} licensed`);
52
+ }
53
+
54
+ // Agent availability summary
55
+ const counts = { online: 0, away: 0, offline: 0 };
56
+ for (const a of agents) {
57
+ const key = STATUS_LABEL[a.status] ?? 'offline';
58
+ counts[key]++;
59
+ }
60
+ console.log(`Agent status: ${counts.online} online · ${counts.away} away · ${counts.offline} offline\n`);
61
+ }),
62
+ };
@@ -1,24 +1,24 @@
1
- import { readConfig, getActiveEnv, ENVS } from '../config.js';
2
-
3
- export const statusCommand = {
4
- command: 'status',
5
- describe: 'Check Velaro API health',
6
- handler: async () => {
7
- const cfg = readConfig();
8
- const env = getActiveEnv();
9
- const apiBase = cfg.envs?.[env]?.adminApiBase ?? ENVS[env].adminApiBase;
10
- process.stdout.write(`Checking ${apiBase}/Status ... `);
11
- try {
12
- const res = await fetch(`${apiBase}/Status`);
13
- if (res.ok) {
14
- console.log('OK');
15
- } else {
16
- console.log(`DEGRADED (HTTP ${res.status})`);
17
- process.exit(1);
18
- }
19
- } catch (err) {
20
- console.log(`UNREACHABLE: ${err.message}`);
21
- process.exit(1);
22
- }
23
- },
24
- };
1
+ import { readConfig, getActiveEnv, ENVS } from '../config.js';
2
+
3
+ export const statusCommand = {
4
+ command: 'status',
5
+ describe: 'Check Velaro API health',
6
+ handler: async () => {
7
+ const cfg = readConfig();
8
+ const env = getActiveEnv();
9
+ const apiBase = cfg.envs?.[env]?.adminApiBase ?? ENVS[env].adminApiBase;
10
+ process.stdout.write(`Checking ${apiBase}/Status ... `);
11
+ try {
12
+ const res = await fetch(`${apiBase}/Status`);
13
+ if (res.ok) {
14
+ console.log('OK');
15
+ } else {
16
+ console.log(`DEGRADED (HTTP ${res.status})`);
17
+ process.exit(1);
18
+ }
19
+ } catch (err) {
20
+ console.log(`UNREACHABLE: ${err.message}`);
21
+ process.exit(1);
22
+ }
23
+ },
24
+ };
@@ -1,144 +1,144 @@
1
- import { get, post } from '../api.js';
2
- import { runCommand } from '../run.js';
3
-
4
- export const teamCommand = {
5
- command: 'team <subcommand>',
6
- describe: 'View teams and manage widget placements (deployments)',
7
- builder: (yargs) =>
8
- yargs
9
- .command(teamListCommand)
10
- .command(widgetListCommand)
11
- .command(widgetRenameCommand)
12
- .command(widgetAssignCommand)
13
- .demandCommand(1, 'Specify a subcommand: list, widget'),
14
- handler: () => {},
15
- };
16
-
17
- // ── team list ──────────────────────────────────────────────────────────────────
18
- // Shows teams and their widget placements inline — the mental model is:
19
- // team = who handles conversations
20
- // widget = which pages route to that team
21
-
22
- const teamListCommand = {
23
- command: 'list',
24
- describe: 'List teams and their widget placements',
25
- handler: runCommand(async () => {
26
- const [teams, deployments] = await Promise.all([
27
- get('/Teams/List'),
28
- get('/Deployment'),
29
- ]);
30
-
31
- if (!teams?.length) {
32
- console.log('No teams found.');
33
- return;
34
- }
35
-
36
- // Group deployments by team
37
- const byTeam = {};
38
- for (const d of (deployments ?? [])) {
39
- if (!byTeam[d.teamId]) byTeam[d.teamId] = [];
40
- byTeam[d.teamId].push(d);
41
- }
42
-
43
- for (const t of teams) {
44
- const widgets = byTeam[t.id] ?? [];
45
- const routing = t.routingAction ? ` routing=${t.routingAction}` : '';
46
- console.log(`\n[${t.id}] ${t.name}${routing}`);
47
-
48
- if (widgets.length) {
49
- for (const w of widgets) {
50
- console.log(` widget [${w.id}] "${w.displayName ?? 'unnamed'}" key: ${w.deploymentId}`);
51
- }
52
- } else {
53
- console.log(` (no widgets assigned — conversations can reach this team via routing rules)`);
54
- }
55
- }
56
-
57
- console.log(`\n${teams.length} team(s) · ${(deployments ?? []).length} widget(s)`);
58
- console.log('\nPaste a widget key into your page\'s embed snippet to route that page to its team.');
59
- }),
60
- };
61
-
62
- // ── widget subcommands ─────────────────────────────────────────────────────────
63
- // "Widget" is the user-facing term. Internally these are deployments.
64
- // A widget is a snippet you paste on a page. The page routes to the widget's team.
65
-
66
- const widgetListCommand = {
67
- command: 'widget list',
68
- describe: 'List all widgets (embed snippets) and which team each routes to',
69
- handler: runCommand(async () => {
70
- const [deployments, teams] = await Promise.all([
71
- get('/Deployment'),
72
- get('/Teams/List'),
73
- ]);
74
-
75
- if (!deployments?.length) {
76
- console.log('No widgets found.');
77
- console.log('\nCreate a widget in the Velaro admin under Deployments,');
78
- console.log('then paste its embed snippet on the pages you want chat on.');
79
- return;
80
- }
81
-
82
- const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
83
- const rows = deployments.map(d => ({
84
- id: `[${d.id}]`,
85
- key: d.deploymentId ?? '—',
86
- team: teamMap[d.teamId] ?? `team ${d.teamId}`,
87
- name: d.displayName ?? '(unnamed)',
88
- }));
89
-
90
- const idW = Math.max(...rows.map(r => r.id.length));
91
- const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
92
- const teamW = Math.max('routes-to'.length, ...rows.map(r => r.team.length));
93
-
94
- console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'routes-to'.padEnd(teamW)} name`);
95
- console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
96
- for (const r of rows) {
97
- console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
98
- }
99
- console.log(`\n${rows.length} widget(s)`);
100
- }),
101
- };
102
-
103
- const widgetRenameCommand = {
104
- command: 'widget rename <id> <name>',
105
- describe: 'Rename a widget',
106
- builder: (y) =>
107
- y
108
- .positional('id', { type: 'number', describe: 'Widget ID' })
109
- .positional('name', { type: 'string', describe: 'New name' }),
110
- handler: runCommand(async (argv) => {
111
- const dep = await get(`/Deployment/${argv.id}`);
112
- if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
113
- await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
114
- console.log(`Widget ${argv.id} renamed to "${argv.name}".`);
115
- }),
116
- };
117
-
118
- const widgetAssignCommand = {
119
- command: 'widget assign <id> <team-id>',
120
- describe: 'Point a widget at a different team (embed code stays the same)',
121
- builder: (y) =>
122
- y
123
- .positional('id', { type: 'number', describe: 'Widget ID' })
124
- .positional('team-id', { type: 'number', describe: 'Team ID to route to' }),
125
- handler: runCommand(async (argv) => {
126
- const [dep, teams] = await Promise.all([
127
- get(`/Deployment/${argv.id}`),
128
- get('/Teams/List'),
129
- ]);
130
-
131
- if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
132
-
133
- const team = (teams ?? []).find(t => t.id === argv['team-id']);
134
- if (!team) {
135
- console.error(`Team ${argv['team-id']} not found.`);
136
- console.error('Run: velaro team list');
137
- process.exit(1);
138
- }
139
-
140
- await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
141
- console.log(`Widget "${dep.displayName ?? dep.id}" now routes to team "${team.name}".`);
142
- console.log('Your embed snippet is unchanged — no website edits needed.');
143
- }),
144
- };
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ export const teamCommand = {
5
+ command: 'team <subcommand>',
6
+ describe: 'View teams and manage widget placements (deployments)',
7
+ builder: (yargs) =>
8
+ yargs
9
+ .command(teamListCommand)
10
+ .command(widgetListCommand)
11
+ .command(widgetRenameCommand)
12
+ .command(widgetAssignCommand)
13
+ .demandCommand(1, 'Specify a subcommand: list, widget'),
14
+ handler: () => {},
15
+ };
16
+
17
+ // ── team list ──────────────────────────────────────────────────────────────────
18
+ // Shows teams and their widget placements inline — the mental model is:
19
+ // team = who handles conversations
20
+ // widget = which pages route to that team
21
+
22
+ const teamListCommand = {
23
+ command: 'list',
24
+ describe: 'List teams and their widget placements',
25
+ handler: runCommand(async () => {
26
+ const [teams, deployments] = await Promise.all([
27
+ get('/Teams/List'),
28
+ get('/Deployment'),
29
+ ]);
30
+
31
+ if (!teams?.length) {
32
+ console.log('No teams found.');
33
+ return;
34
+ }
35
+
36
+ // Group deployments by team
37
+ const byTeam = {};
38
+ for (const d of (deployments ?? [])) {
39
+ if (!byTeam[d.teamId]) byTeam[d.teamId] = [];
40
+ byTeam[d.teamId].push(d);
41
+ }
42
+
43
+ for (const t of teams) {
44
+ const widgets = byTeam[t.id] ?? [];
45
+ const routing = t.routingAction ? ` routing=${t.routingAction}` : '';
46
+ console.log(`\n[${t.id}] ${t.name}${routing}`);
47
+
48
+ if (widgets.length) {
49
+ for (const w of widgets) {
50
+ console.log(` widget [${w.id}] "${w.displayName ?? 'unnamed'}" key: ${w.deploymentId}`);
51
+ }
52
+ } else {
53
+ console.log(` (no widgets assigned — conversations can reach this team via routing rules)`);
54
+ }
55
+ }
56
+
57
+ console.log(`\n${teams.length} team(s) · ${(deployments ?? []).length} widget(s)`);
58
+ console.log('\nPaste a widget key into your page\'s embed snippet to route that page to its team.');
59
+ }),
60
+ };
61
+
62
+ // ── widget subcommands ─────────────────────────────────────────────────────────
63
+ // "Widget" is the user-facing term. Internally these are deployments.
64
+ // A widget is a snippet you paste on a page. The page routes to the widget's team.
65
+
66
+ const widgetListCommand = {
67
+ command: 'widget list',
68
+ describe: 'List all widgets (embed snippets) and which team each routes to',
69
+ handler: runCommand(async () => {
70
+ const [deployments, teams] = await Promise.all([
71
+ get('/Deployment'),
72
+ get('/Teams/List'),
73
+ ]);
74
+
75
+ if (!deployments?.length) {
76
+ console.log('No widgets found.');
77
+ console.log('\nCreate a widget in the Velaro admin under Deployments,');
78
+ console.log('then paste its embed snippet on the pages you want chat on.');
79
+ return;
80
+ }
81
+
82
+ const teamMap = Object.fromEntries((teams ?? []).map(t => [t.id, t.name]));
83
+ const rows = deployments.map(d => ({
84
+ id: `[${d.id}]`,
85
+ key: d.deploymentId ?? '—',
86
+ team: teamMap[d.teamId] ?? `team ${d.teamId}`,
87
+ name: d.displayName ?? '(unnamed)',
88
+ }));
89
+
90
+ const idW = Math.max(...rows.map(r => r.id.length));
91
+ const keyW = Math.max('embed-key'.length, ...rows.map(r => r.key.length));
92
+ const teamW = Math.max('routes-to'.length, ...rows.map(r => r.team.length));
93
+
94
+ console.log(`\n${'id'.padStart(idW)} ${'embed-key'.padEnd(keyW)} ${'routes-to'.padEnd(teamW)} name`);
95
+ console.log(`${'-'.repeat(idW)} ${'-'.repeat(keyW)} ${'-'.repeat(teamW)} ----`);
96
+ for (const r of rows) {
97
+ console.log(`${r.id.padStart(idW)} ${r.key.padEnd(keyW)} ${r.team.padEnd(teamW)} ${r.name}`);
98
+ }
99
+ console.log(`\n${rows.length} widget(s)`);
100
+ }),
101
+ };
102
+
103
+ const widgetRenameCommand = {
104
+ command: 'widget rename <id> <name>',
105
+ describe: 'Rename a widget',
106
+ builder: (y) =>
107
+ y
108
+ .positional('id', { type: 'number', describe: 'Widget ID' })
109
+ .positional('name', { type: 'string', describe: 'New name' }),
110
+ handler: runCommand(async (argv) => {
111
+ const dep = await get(`/Deployment/${argv.id}`);
112
+ if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
113
+ await post('/Deployment', { ...dep, id: argv.id, displayName: argv.name });
114
+ console.log(`Widget ${argv.id} renamed to "${argv.name}".`);
115
+ }),
116
+ };
117
+
118
+ const widgetAssignCommand = {
119
+ command: 'widget assign <id> <team-id>',
120
+ describe: 'Point a widget at a different team (embed code stays the same)',
121
+ builder: (y) =>
122
+ y
123
+ .positional('id', { type: 'number', describe: 'Widget ID' })
124
+ .positional('team-id', { type: 'number', describe: 'Team ID to route to' }),
125
+ handler: runCommand(async (argv) => {
126
+ const [dep, teams] = await Promise.all([
127
+ get(`/Deployment/${argv.id}`),
128
+ get('/Teams/List'),
129
+ ]);
130
+
131
+ if (!dep) { console.error(`Widget ${argv.id} not found.`); process.exit(1); }
132
+
133
+ const team = (teams ?? []).find(t => t.id === argv['team-id']);
134
+ if (!team) {
135
+ console.error(`Team ${argv['team-id']} not found.`);
136
+ console.error('Run: velaro team list');
137
+ process.exit(1);
138
+ }
139
+
140
+ await post('/Deployment', { ...dep, id: argv.id, teamId: argv['team-id'] });
141
+ console.log(`Widget "${dep.displayName ?? dep.id}" now routes to team "${team.name}".`);
142
+ console.log('Your embed snippet is unchanged — no website edits needed.');
143
+ }),
144
+ };
@@ -1,47 +1,47 @@
1
- import { execSync } from 'child_process';
2
- import { createRequire } from 'module';
3
-
4
- const require = createRequire(import.meta.url);
5
- const { version: currentVersion } = require('../../package.json');
6
-
7
- const NPM_REGISTRY = 'https://registry.npmjs.org/@velaro/cli/latest';
8
-
9
- export const updateCommand = {
10
- command: 'update',
11
- describe: 'Update the Velaro CLI to the latest version',
12
- handler: async () => {
13
- process.stdout.write('Checking for updates... ');
14
-
15
- let latest;
16
- try {
17
- const res = await fetch(NPM_REGISTRY, { signal: AbortSignal.timeout(8000) });
18
- const data = await res.json();
19
- latest = data?.version;
20
- } catch {
21
- console.error('Could not reach npm registry. Check your connection and try again.');
22
- process.exit(1);
23
- }
24
-
25
- if (!latest) {
26
- console.error('Could not determine the latest version.');
27
- process.exit(1);
28
- }
29
-
30
- if (latest === currentVersion) {
31
- console.log(`already up to date (${currentVersion}).`);
32
- return;
33
- }
34
-
35
- console.log(`${currentVersion} → ${latest}`);
36
- console.log('Running: npm install -g @velaro/cli@latest\n');
37
-
38
- try {
39
- execSync('npm install -g @velaro/cli@latest', { stdio: 'inherit' });
40
- console.log('\nUpdated successfully. Run velaro --version to confirm.');
41
- } catch {
42
- console.error('\nUpdate failed. Try running manually:');
43
- console.error(' npm install -g @velaro/cli@latest');
44
- process.exit(1);
45
- }
46
- },
47
- };
1
+ import { execSync } from 'child_process';
2
+ import { createRequire } from 'module';
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const { version: currentVersion } = require('../../package.json');
6
+
7
+ const NPM_REGISTRY = 'https://registry.npmjs.org/@velaro/cli/latest';
8
+
9
+ export const updateCommand = {
10
+ command: 'update',
11
+ describe: 'Update the Velaro CLI to the latest version',
12
+ handler: async () => {
13
+ process.stdout.write('Checking for updates... ');
14
+
15
+ let latest;
16
+ try {
17
+ const res = await fetch(NPM_REGISTRY, { signal: AbortSignal.timeout(8000) });
18
+ const data = await res.json();
19
+ latest = data?.version;
20
+ } catch {
21
+ console.error('Could not reach npm registry. Check your connection and try again.');
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!latest) {
26
+ console.error('Could not determine the latest version.');
27
+ process.exit(1);
28
+ }
29
+
30
+ if (latest === currentVersion) {
31
+ console.log(`already up to date (${currentVersion}).`);
32
+ return;
33
+ }
34
+
35
+ console.log(`${currentVersion} → ${latest}`);
36
+ console.log('Running: npm install -g @velaro/cli@latest\n');
37
+
38
+ try {
39
+ execSync('npm install -g @velaro/cli@latest', { stdio: 'inherit' });
40
+ console.log('\nUpdated successfully. Run velaro --version to confirm.');
41
+ } catch {
42
+ console.error('\nUpdate failed. Try running manually:');
43
+ console.error(' npm install -g @velaro/cli@latest');
44
+ process.exit(1);
45
+ }
46
+ },
47
+ };
@@ -1,22 +1,22 @@
1
- import { readConfig } from '../config.js';
2
-
3
- export const whoamiCommand = {
4
- command: 'whoami',
5
- describe: 'Show current login and site info',
6
- handler: () => {
7
- const cfg = readConfig();
8
-
9
- if (!cfg?.velaroToken) {
10
- console.error('Not logged in. Run: velaro login');
11
- process.exit(1);
12
- }
13
-
14
- const expires = new Date(cfg.velaroExpires);
15
- const minLeft = Math.round((expires - Date.now()) / 60000);
16
-
17
- console.log(`User: ${cfg.userName ?? '(unknown)'}`);
18
- console.log(`Site ID: ${cfg.siteId ?? '(unknown)'}`);
19
- console.log(`API: ${cfg.apiBase}`);
20
- console.log(`Token: ${expires > new Date() ? `valid (expires in ${minLeft}m)` : 'EXPIRED — run velaro login'}`);
21
- },
22
- };
1
+ import { readConfig } from '../config.js';
2
+
3
+ export const whoamiCommand = {
4
+ command: 'whoami',
5
+ describe: 'Show current login and site info',
6
+ handler: () => {
7
+ const cfg = readConfig();
8
+
9
+ if (!cfg?.velaroToken) {
10
+ console.error('Not logged in. Run: velaro login');
11
+ process.exit(1);
12
+ }
13
+
14
+ const expires = new Date(cfg.velaroExpires);
15
+ const minLeft = Math.round((expires - Date.now()) / 60000);
16
+
17
+ console.log(`User: ${cfg.userName ?? '(unknown)'}`);
18
+ console.log(`Site ID: ${cfg.siteId ?? '(unknown)'}`);
19
+ console.log(`API: ${cfg.apiBase}`);
20
+ console.log(`Token: ${expires > new Date() ? `valid (expires in ${minLeft}m)` : 'EXPIRED — run velaro login'}`);
21
+ },
22
+ };