@velaro/cli 0.1.2 → 0.3.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.
- package/bin/velaro.js +56 -36
- package/lib/commands/article.js +280 -0
- package/lib/commands/bot.js +137 -54
- package/lib/commands/check.js +163 -0
- package/lib/commands/deployment.js +107 -0
- package/lib/commands/kb.js +241 -0
- package/lib/commands/login.js +60 -53
- package/lib/commands/mcp-key.js +100 -63
- package/lib/commands/rule.js +85 -28
- package/lib/commands/team.js +144 -0
- package/lib/commands/update.js +47 -0
- package/lib/commands/workflow.js +98 -32
- package/lib/config.js +28 -25
- package/lib/oauth.js +2 -1
- package/lib/run.js +6 -1
- package/lib/subscription.js +39 -0
- package/lib/track.js +35 -0
- package/lib/update-check.js +64 -0
- package/package.json +1 -1
package/lib/commands/mcp-key.js
CHANGED
|
@@ -1,63 +1,100 @@
|
|
|
1
|
-
import { get, post, del } from '../api.js';
|
|
2
|
-
import { runCommand } from '../run.js';
|
|
3
|
-
|
|
4
|
-
export const mcpKeyCommand = {
|
|
5
|
-
command: 'mcp-key <subcommand>',
|
|
6
|
-
describe: 'Manage MCP API keys (vel_live_* keys for AI skill access)',
|
|
7
|
-
builder: (yargs) =>
|
|
8
|
-
yargs
|
|
9
|
-
.command(mcpKeyListCommand)
|
|
10
|
-
.command(mcpKeyCreateCommand)
|
|
11
|
-
.command(mcpKeyRevokeCommand)
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
console.log(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
console.log(
|
|
53
|
-
})
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
1
|
+
import { get, post, del } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
export const mcpKeyCommand = {
|
|
5
|
+
command: 'mcp-key <subcommand>',
|
|
6
|
+
describe: 'Manage MCP API keys (vel_live_* keys for AI skill access)',
|
|
7
|
+
builder: (yargs) =>
|
|
8
|
+
yargs
|
|
9
|
+
.command(mcpKeyListCommand)
|
|
10
|
+
.command(mcpKeyCreateCommand)
|
|
11
|
+
.command(mcpKeyRevokeCommand)
|
|
12
|
+
.command(mcpKeyRotateCommand)
|
|
13
|
+
.demandCommand(1, 'Specify a subcommand: list, create, revoke, rotate'),
|
|
14
|
+
handler: () => {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mcpKeyListCommand = {
|
|
18
|
+
command: 'list',
|
|
19
|
+
describe: 'List MCP API keys for your site',
|
|
20
|
+
handler: runCommand(async () => {
|
|
21
|
+
const keys = await get('/McpApiKeys');
|
|
22
|
+
if (!keys?.length) { console.log('No MCP keys found.'); return; }
|
|
23
|
+
console.log(`Found ${keys.length} key(s):\n`);
|
|
24
|
+
for (const k of keys) {
|
|
25
|
+
const used = k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'never';
|
|
26
|
+
const expires = k.expiresAt ? ` expires ${new Date(k.expiresAt).toLocaleDateString()}` : '';
|
|
27
|
+
console.log(` [${k.id}] ${k.label} prefix=${k.keyPrefix} ${k.isActive ? 'active' : 'revoked'} last used=${used}${expires}`);
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mcpKeyCreateCommand = {
|
|
33
|
+
command: 'create',
|
|
34
|
+
describe: 'Create a new MCP API key',
|
|
35
|
+
builder: (y) =>
|
|
36
|
+
y
|
|
37
|
+
.option('label', {
|
|
38
|
+
describe: 'Human-readable label (e.g. "Claude Code")',
|
|
39
|
+
type: 'string',
|
|
40
|
+
demandOption: true,
|
|
41
|
+
})
|
|
42
|
+
.option('expires', {
|
|
43
|
+
describe: 'Expiry date in ISO 8601 format (e.g. 2027-01-01)',
|
|
44
|
+
type: 'string',
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
handler: runCommand(async (argv) => {
|
|
48
|
+
const payload = { label: argv.label };
|
|
49
|
+
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
50
|
+
|
|
51
|
+
const result = await post('/McpApiKeys', payload);
|
|
52
|
+
console.log(`MCP key created: ${result.label}`);
|
|
53
|
+
console.log(`\n Key: ${result.rawKey}`);
|
|
54
|
+
console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const mcpKeyRevokeCommand = {
|
|
59
|
+
command: 'revoke <id>',
|
|
60
|
+
describe: 'Revoke an MCP API key by ID',
|
|
61
|
+
handler: runCommand(async (argv) => {
|
|
62
|
+
await del(`/McpApiKeys/${argv.id}`);
|
|
63
|
+
console.log(`Key ${argv.id} revoked.`);
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const mcpKeyRotateCommand = {
|
|
68
|
+
command: 'rotate <id>',
|
|
69
|
+
describe: 'Revoke an existing key and issue a replacement in one step',
|
|
70
|
+
builder: (y) =>
|
|
71
|
+
y
|
|
72
|
+
.positional('id', { type: 'number', describe: 'ID of the key to revoke (get it from mcp-key list)' })
|
|
73
|
+
.option('label', {
|
|
74
|
+
describe: 'Label for the replacement key (defaults to the old key\'s label)',
|
|
75
|
+
type: 'string',
|
|
76
|
+
})
|
|
77
|
+
.option('expires', {
|
|
78
|
+
describe: 'Expiry date for the new key in ISO 8601 format (e.g. 2027-01-01)',
|
|
79
|
+
type: 'string',
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
handler: runCommand(async (argv) => {
|
|
83
|
+
const keys = await get('/McpApiKeys');
|
|
84
|
+
const old = keys?.find(k => k.id === argv.id);
|
|
85
|
+
if (!old) throw new Error(`Key ID ${argv.id} not found. Run 'velaro mcp-key list' to see your keys.`);
|
|
86
|
+
|
|
87
|
+
const label = argv.label || old.label;
|
|
88
|
+
|
|
89
|
+
await del(`/McpApiKeys/${argv.id}`);
|
|
90
|
+
console.log(`Revoked: [${argv.id}] ${old.label} (${old.keyPrefix}...)`);
|
|
91
|
+
|
|
92
|
+
const payload = { label };
|
|
93
|
+
if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
|
|
94
|
+
const result = await post('/McpApiKeys', payload);
|
|
95
|
+
|
|
96
|
+
console.log(`Replaced: [${result.id}] ${result.label}`);
|
|
97
|
+
console.log(`\n New key: ${result.rawKey}`);
|
|
98
|
+
console.log(`\n ${result.warning ?? 'Copy this key now — it will not be shown again.'}`);
|
|
99
|
+
}),
|
|
100
|
+
};
|
package/lib/commands/rule.js
CHANGED
|
@@ -1,28 +1,85 @@
|
|
|
1
|
-
import { get }
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
import { get, post, del } from '../api.js';
|
|
2
|
+
import { requireFeature } from '../subscription.js';
|
|
3
|
+
import { runCommand } from '../run.js';
|
|
4
|
+
|
|
5
|
+
export const ruleCommand = {
|
|
6
|
+
command: 'rule <subcommand>',
|
|
7
|
+
describe: 'Manage routing rules',
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.command(ruleListCommand)
|
|
11
|
+
.command(ruleCreateCommand)
|
|
12
|
+
.command(ruleDeleteCommand)
|
|
13
|
+
.demandCommand(1, 'Specify a subcommand: list, create, delete'),
|
|
14
|
+
handler: () => {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ── list ───────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const ruleListCommand = {
|
|
20
|
+
command: 'list',
|
|
21
|
+
describe: 'List all routing rules',
|
|
22
|
+
handler: runCommand(async () => {
|
|
23
|
+
await requireFeature('enableWorkflowRules', 'Routing Rules');
|
|
24
|
+
|
|
25
|
+
const rules = await get('/Rules/List');
|
|
26
|
+
if (!rules?.length) { console.log('No routing rules found.'); return; }
|
|
27
|
+
|
|
28
|
+
const sorted = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
29
|
+
const idW = Math.max(...sorted.map(r => String(r.id).length + 2));
|
|
30
|
+
const priW = Math.max('pri'.length, ...sorted.map(r => String(r.priority ?? 0).length));
|
|
31
|
+
const nameW = Math.max('name'.length, ...sorted.map(r => (r.name ?? r.trigger ?? '—').length));
|
|
32
|
+
|
|
33
|
+
console.log(`\n${'id'.padStart(idW)} ${'pri'.padEnd(priW)} ${'name'.padEnd(nameW)} trigger`);
|
|
34
|
+
console.log(`${'-'.repeat(idW)} ${'-'.repeat(priW)} ${'-'.repeat(nameW)} -------`);
|
|
35
|
+
for (const r of sorted) {
|
|
36
|
+
const id = `[${r.id}]`.padStart(idW);
|
|
37
|
+
const pri = String(r.priority ?? 0).padEnd(priW);
|
|
38
|
+
const name = (r.name ?? r.trigger ?? '—').padEnd(nameW);
|
|
39
|
+
console.log(`${id} ${pri} ${name} ${r.trigger ?? '—'}`);
|
|
40
|
+
}
|
|
41
|
+
console.log(`\n${sorted.length} rule(s)`);
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── create ─────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const ruleCreateCommand = {
|
|
48
|
+
command: 'create',
|
|
49
|
+
describe: 'Create a routing rule',
|
|
50
|
+
builder: (y) =>
|
|
51
|
+
y
|
|
52
|
+
.option('name', { type: 'string', demandOption: true, describe: 'Rule name' })
|
|
53
|
+
.option('trigger', { type: 'string', demandOption: true, describe: 'Trigger type (e.g. pageUrl, country)' })
|
|
54
|
+
.option('value', { type: 'string', demandOption: true, describe: 'Trigger value to match' })
|
|
55
|
+
.option('team-id', { type: 'number', demandOption: true, describe: 'Team ID to route to' })
|
|
56
|
+
.option('priority', { type: 'number', default: 0, describe: 'Rule priority (lower = higher priority)' }),
|
|
57
|
+
|
|
58
|
+
handler: runCommand(async (argv) => {
|
|
59
|
+
await requireFeature('enableWorkflowRules', 'Routing Rules');
|
|
60
|
+
|
|
61
|
+
const result = await post('/Rules', {
|
|
62
|
+
name: argv.name,
|
|
63
|
+
trigger: argv.trigger,
|
|
64
|
+
value: argv.value,
|
|
65
|
+
teamId: argv['team-id'],
|
|
66
|
+
priority: argv.priority,
|
|
67
|
+
isEnabled: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`Rule created: [${result.id}] ${result.name}`);
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ── delete ─────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const ruleDeleteCommand = {
|
|
77
|
+
command: 'delete <id>',
|
|
78
|
+
describe: 'Delete a routing rule',
|
|
79
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Rule ID' }),
|
|
80
|
+
handler: runCommand(async (argv) => {
|
|
81
|
+
await requireFeature('enableWorkflowRules', 'Routing Rules');
|
|
82
|
+
await del(`/Rules/${argv.id}`);
|
|
83
|
+
console.log(`Rule ${argv.id} deleted.`);
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
@@ -0,0 +1,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
|
+
};
|
|
@@ -0,0 +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
|
+
};
|
package/lib/commands/workflow.js
CHANGED
|
@@ -1,32 +1,98 @@
|
|
|
1
|
-
import { get }
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
1
|
+
import { get, post } from '../api.js';
|
|
2
|
+
import { requireFeature } from '../subscription.js';
|
|
3
|
+
import { runCommand } from '../run.js';
|
|
4
|
+
|
|
5
|
+
export const workflowCommand = {
|
|
6
|
+
command: 'workflow <subcommand>',
|
|
7
|
+
describe: 'Manage automation workflows',
|
|
8
|
+
builder: (yargs) =>
|
|
9
|
+
yargs
|
|
10
|
+
.command(workflowListCommand)
|
|
11
|
+
.command(workflowGetCommand)
|
|
12
|
+
.command(workflowEnableCommand)
|
|
13
|
+
.command(workflowDisableCommand)
|
|
14
|
+
.demandCommand(1, 'Specify a subcommand: list, get, enable, disable'),
|
|
15
|
+
handler: () => {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ── list ───────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const workflowListCommand = {
|
|
21
|
+
command: 'list',
|
|
22
|
+
describe: 'List all workflows',
|
|
23
|
+
handler: runCommand(async () => {
|
|
24
|
+
await requireFeature('enableAutomation', 'Workflows');
|
|
25
|
+
|
|
26
|
+
const workflows = await get('/Workflows/List');
|
|
27
|
+
const rows = (workflows ?? []).filter(w => !w.isTemplate);
|
|
28
|
+
|
|
29
|
+
if (!rows.length) { console.log('No workflows found.'); return; }
|
|
30
|
+
|
|
31
|
+
const idW = Math.max(...rows.map(w => String(w.id).length + 2));
|
|
32
|
+
const triggerW = Math.max('trigger'.length, ...rows.map(w => (w.triggerType ?? '—').length));
|
|
33
|
+
|
|
34
|
+
console.log(`\n${'id'.padStart(idW)} status ${'trigger'.padEnd(triggerW)} name`);
|
|
35
|
+
console.log(`${'-'.repeat(idW)} -------- ${'-'.repeat(triggerW)} ----`);
|
|
36
|
+
for (const w of rows) {
|
|
37
|
+
const id = `[${w.id}]`.padStart(idW);
|
|
38
|
+
const status = w.enabled ? 'enabled ' : 'disabled';
|
|
39
|
+
const trigger = (w.triggerType ?? '—').padEnd(triggerW);
|
|
40
|
+
console.log(`${id} ${status} ${trigger} ${w.name}`);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\n${rows.length} workflow(s)`);
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ── get ────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const workflowGetCommand = {
|
|
49
|
+
command: 'get <id>',
|
|
50
|
+
describe: 'Show workflow details',
|
|
51
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
|
|
52
|
+
handler: runCommand(async (argv) => {
|
|
53
|
+
await requireFeature('enableAutomation', 'Workflows');
|
|
54
|
+
|
|
55
|
+
const w = await get(`/Workflows/${argv.id}`);
|
|
56
|
+
if (!w) { console.error(`Workflow ${argv.id} not found.`); process.exit(1); }
|
|
57
|
+
|
|
58
|
+
console.log(`\n[${w.id}] ${w.name}`);
|
|
59
|
+
console.log(` Status: ${w.enabled ? 'enabled' : 'disabled'}`);
|
|
60
|
+
console.log(` Trigger: ${w.triggerType ?? '—'}`);
|
|
61
|
+
if (w.nodes?.length) {
|
|
62
|
+
console.log(` Nodes: ${w.nodes.length}`);
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ── enable / disable ──────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const workflowEnableCommand = {
|
|
70
|
+
command: 'enable <id>',
|
|
71
|
+
describe: 'Enable a workflow',
|
|
72
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
|
|
73
|
+
handler: runCommand(async (argv) => {
|
|
74
|
+
await requireFeature('enableAutomation', 'Workflows');
|
|
75
|
+
await post(`/Workflows/toggle/${argv.id}`, null);
|
|
76
|
+
console.log(`Workflow ${argv.id} enabled.`);
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const workflowDisableCommand = {
|
|
81
|
+
command: 'disable <id>',
|
|
82
|
+
describe: 'Disable a workflow',
|
|
83
|
+
builder: (y) => y.positional('id', { type: 'number', describe: 'Workflow ID' }),
|
|
84
|
+
handler: runCommand(async (argv) => {
|
|
85
|
+
await requireFeature('enableAutomation', 'Workflows');
|
|
86
|
+
|
|
87
|
+
const w = await get(`/Workflows/${argv.id}`);
|
|
88
|
+
if (!w) { console.error(`Workflow ${argv.id} not found.`); process.exit(1); }
|
|
89
|
+
|
|
90
|
+
if (!w.enabled) {
|
|
91
|
+
console.log(`Workflow ${argv.id} is already disabled.`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await post(`/Workflows/toggle/${argv.id}`, null);
|
|
96
|
+
console.log(`Workflow ${argv.id} disabled.`);
|
|
97
|
+
}),
|
|
98
|
+
};
|
package/lib/config.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { homedir } from 'os';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
-
|
|
5
|
-
const CONFIG_DIR = join(homedir(), '.velaro');
|
|
6
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.velaro');
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
// Production API URL — update when production messaging API is deployed.
|
|
9
|
+
// messaging.velaro.com is the chat widget host (Azure SWA), not the API.
|
|
10
|
+
export const DEFAULT_API_BASE = process.env.VELARO_API_BASE || 'https://velaro-messaging-api-staging.azurewebsites.net';
|
|
11
|
+
export const STAGING_API_BASE = process.env.VELARO_STAGING_API || 'https://velaro-messaging-api-staging.azurewebsites.net';
|
|
12
|
+
|
|
13
|
+
export function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeConfig(data) {
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true }); // no-op if already exists
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearConfig() {
|
|
27
|
+
writeConfig({});
|
|
28
|
+
}
|