@velaro/cli 0.4.0 → 0.7.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 +7 -1
- package/lib/api.js +52 -48
- package/lib/commands/agent.js +50 -50
- package/lib/commands/article.js +120 -12
- package/lib/commands/check.js +163 -163
- package/lib/commands/deployment.js +107 -107
- package/lib/commands/env.js +45 -0
- package/lib/commands/index.js +212 -0
- package/lib/commands/ingest.js +31 -31
- package/lib/commands/kb.js +69 -1
- package/lib/commands/login.js +46 -20
- package/lib/commands/mcp-key.js +17 -10
- package/lib/commands/ops.js +173 -0
- package/lib/commands/site.js +62 -62
- package/lib/commands/status.js +24 -22
- package/lib/commands/team.js +144 -144
- package/lib/commands/update.js +47 -47
- package/lib/commands/whoami.js +22 -22
- package/lib/config.js +63 -8
- package/lib/oauth.js +135 -135
- package/lib/run.js +16 -16
- package/lib/subscription.js +39 -39
- package/lib/track.js +35 -35
- package/lib/update-check.js +64 -64
- package/package.json +2 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { get, post, getCredentials } from '../api.js';
|
|
2
|
+
|
|
3
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
function isVelaroAdmin(creds) {
|
|
6
|
+
return creds.siteId === 1032;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function mb(n) {
|
|
10
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)} GB`;
|
|
11
|
+
return `${n} MB`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function bar(used, total, width = 20) {
|
|
15
|
+
if (!total) return '[ ] n/a';
|
|
16
|
+
const pct = Math.min(used / total, 1);
|
|
17
|
+
const filled = Math.round(pct * width);
|
|
18
|
+
const color = pct >= 0.9 ? '\x1b[31m' : pct >= 0.7 ? '\x1b[33m' : '\x1b[32m';
|
|
19
|
+
return `${color}[${'█'.repeat(filled)}${' '.repeat(width - filled)}]\x1b[0m ${(pct * 100).toFixed(0)}%`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── list ─────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const listCommand = {
|
|
25
|
+
command: 'list',
|
|
26
|
+
describe: 'List indexes. Velaro admins see all physical indexes; customers see their virtual indexes + quota.',
|
|
27
|
+
builder: (y) =>
|
|
28
|
+
y.option('site', { type: 'number', describe: 'Site ID (Velaro staff only)' }),
|
|
29
|
+
handler: async (argv) => {
|
|
30
|
+
const creds = await getCredentials();
|
|
31
|
+
|
|
32
|
+
if (isVelaroAdmin(creds)) {
|
|
33
|
+
// ── Velaro admin: show all physical indexes with storage health ─────────
|
|
34
|
+
const stats = await get('/DatabaseTool/SearchIndexStats');
|
|
35
|
+
|
|
36
|
+
console.log('\n\x1b[1mPhysical Indexes\x1b[0m');
|
|
37
|
+
console.log(` Shared: ${stats.sharedIndexes.length} Dedicated: ${stats.dedicatedIndexes.length} Total: ${stats.totalSlots}`);
|
|
38
|
+
|
|
39
|
+
const LIMIT_MB = 160 * 1024;
|
|
40
|
+
|
|
41
|
+
console.log('\n\x1b[1mShared Indexes\x1b[0m');
|
|
42
|
+
for (const idx of stats.sharedIndexes) {
|
|
43
|
+
console.log(`\n \x1b[36m${idx.indexName}\x1b[0m ${idx.sites?.length ?? 0} sites ${idx.totalDocuments.toLocaleString()} docs`);
|
|
44
|
+
console.log(` Storage: ${bar(idx.storageMb, LIMIT_MB)} ${mb(idx.storageMb)} / ${mb(LIMIT_MB)}`);
|
|
45
|
+
if (idx.sites?.length) {
|
|
46
|
+
for (const s of idx.sites.slice(0, 10)) {
|
|
47
|
+
const badge = s.assignmentType === 'legacy-fallback' ? '\x1b[33m[legacy]\x1b[0m' :
|
|
48
|
+
s.assignmentType === 'dedicated' ? '\x1b[35m[dedicated]\x1b[0m' :
|
|
49
|
+
'\x1b[34m[shared]\x1b[0m';
|
|
50
|
+
console.log(` Site ${s.siteId.toString().padEnd(8)} ${badge} ${s.documentCount.toLocaleString()} docs`);
|
|
51
|
+
}
|
|
52
|
+
if (idx.sites.length > 10) console.log(` ... and ${idx.sites.length - 10} more`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (stats.dedicatedIndexes.length) {
|
|
57
|
+
console.log('\n\x1b[1mDedicated Indexes\x1b[0m');
|
|
58
|
+
for (const idx of stats.dedicatedIndexes) {
|
|
59
|
+
console.log(` \x1b[35m${idx.indexName}\x1b[0m Site ${idx.siteId} ${idx.documentCount.toLocaleString()} docs`);
|
|
60
|
+
console.log(` Storage: ${bar(idx.storageMb, LIMIT_MB)} ${mb(idx.storageMb)} / ${mb(LIMIT_MB)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const legacy = stats.sharedIndexes.flatMap(s => s.sites ?? []).filter(s => s.assignmentType === 'legacy-fallback').length;
|
|
65
|
+
if (legacy > 0) {
|
|
66
|
+
console.log(`\n\x1b[33m⚠ ${legacy} legacy site(s) have no index assignment (fallback to velaro-shared-index)\x1b[0m`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// ── Customer: show their virtual indexes + quota ────────────────────────
|
|
70
|
+
const siteId = argv.site ?? creds.siteId;
|
|
71
|
+
const [indexes, breakdown] = await Promise.all([
|
|
72
|
+
get('/AzureIndexes/List'),
|
|
73
|
+
get(`/DatabaseTool/SearchIndexSite?siteId=${siteId}`).catch(() => null),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// Quota from subscription
|
|
77
|
+
const sub = await get('/Subscription/Get').catch(() => null);
|
|
78
|
+
const chunkCap = sub?.maxIndexedChunkTotal ?? 0;
|
|
79
|
+
const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
|
|
80
|
+
|
|
81
|
+
console.log('\n\x1b[1mYour Search Indexes\x1b[0m');
|
|
82
|
+
|
|
83
|
+
if (chunkCap > 0) {
|
|
84
|
+
console.log(`\nQuota: ${bar(usedChunks, chunkCap)} ${usedChunks.toLocaleString()} / ${chunkCap.toLocaleString()} chunks used`);
|
|
85
|
+
const remaining = chunkCap - usedChunks;
|
|
86
|
+
if (remaining <= 0) {
|
|
87
|
+
console.log('\x1b[31m✗ Quota full — new content will not be indexed until you upgrade or remove content.\x1b[0m');
|
|
88
|
+
} else if (remaining < chunkCap * 0.1) {
|
|
89
|
+
console.log(`\x1b[33m⚠ Only ${remaining.toLocaleString()} chunks remaining. Consider upgrading your plan.\x1b[0m`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (breakdown) {
|
|
94
|
+
const typeLabel = breakdown.assignmentType === 'dedicated' ? '\x1b[35m[dedicated index]\x1b[0m' :
|
|
95
|
+
breakdown.assignmentType === 'shared' ? '\x1b[34m[shared index]\x1b[0m' :
|
|
96
|
+
'\x1b[33m[legacy]\x1b[0m';
|
|
97
|
+
console.log(`\nPhysical index: \x1b[36m${breakdown.physicalIndex}\x1b[0m ${typeLabel}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('');
|
|
101
|
+
for (const idx of indexes) {
|
|
102
|
+
const docs = idx.documentCount != null ? `${idx.documentCount.toLocaleString()} docs` : '';
|
|
103
|
+
console.log(` \x1b[36m${idx.name}\x1b[0m (id: ${idx.id}) ${docs}`);
|
|
104
|
+
if (idx.description) console.log(` ${idx.description}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!indexes.length) console.log(' No indexes found. Create one in your bot settings.');
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ── site (Velaro staff only) ──────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
const siteCommand = {
|
|
116
|
+
command: 'site <siteId>',
|
|
117
|
+
describe: 'Show index breakdown for a specific site (Velaro staff only)',
|
|
118
|
+
handler: async (argv) => {
|
|
119
|
+
const creds = await getCredentials();
|
|
120
|
+
if (!isVelaroAdmin(creds)) {
|
|
121
|
+
console.error('This command is only available to Velaro staff.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const data = await get(`/DatabaseTool/SearchIndexSite?siteId=${argv.siteId}`);
|
|
125
|
+
console.log(`\nSite ${argv.siteId} → \x1b[36m${data.physicalIndex}\x1b[0m [${data.assignmentType}]`);
|
|
126
|
+
console.log(`Total docs: ${data.totalDocuments.toLocaleString()}\n`);
|
|
127
|
+
for (const t of data.indexTypes) {
|
|
128
|
+
console.log(` ${t.indexName.padEnd(40)} ${t.count.toLocaleString()} docs`);
|
|
129
|
+
}
|
|
130
|
+
console.log('');
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ── provision (Velaro staff only) ─────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
const provisionCommand = {
|
|
137
|
+
command: 'provision <siteId>',
|
|
138
|
+
describe: 'Provision a dedicated physical index for a site (Velaro staff only)',
|
|
139
|
+
handler: async (argv) => {
|
|
140
|
+
const creds = await getCredentials();
|
|
141
|
+
if (!isVelaroAdmin(creds)) {
|
|
142
|
+
console.error('This command is only available to Velaro staff.');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
console.log(`Provisioning dedicated index for site ${argv.siteId}...`);
|
|
146
|
+
const result = await post('/DatabaseTool/ProvisionDedicatedIndex', { SiteId: argv.siteId });
|
|
147
|
+
console.log(`\x1b[32m✓ ${result.message}\x1b[0m`);
|
|
148
|
+
console.log(` Index: \x1b[36m${result.indexName}\x1b[0m`);
|
|
149
|
+
console.log('\nNote: Re-ingest content to populate the new index.');
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ── reingest (customer-facing, credit-gated) ──────────────────────────────────
|
|
154
|
+
|
|
155
|
+
const reingestCommand = {
|
|
156
|
+
command: 'reingest',
|
|
157
|
+
describe: 'Re-ingest all content sources for your site (requires available index quota)',
|
|
158
|
+
builder: (y) =>
|
|
159
|
+
y
|
|
160
|
+
.option('source', {
|
|
161
|
+
type: 'string',
|
|
162
|
+
describe: 'Specific source to reingest: kb | shopify | bigcommerce | scraper',
|
|
163
|
+
})
|
|
164
|
+
.option('site', {
|
|
165
|
+
type: 'number',
|
|
166
|
+
describe: 'Site ID (Velaro staff only)',
|
|
167
|
+
}),
|
|
168
|
+
handler: async (argv) => {
|
|
169
|
+
const creds = await getCredentials();
|
|
170
|
+
|
|
171
|
+
// Check quota before allowing reingest
|
|
172
|
+
const sub = await get('/Subscription/Get').catch(() => null);
|
|
173
|
+
if (!isVelaroAdmin(creds) && sub?.maxIndexedChunkTotal > 0) {
|
|
174
|
+
const indexes = await get('/AzureIndexes/List').catch(() => []);
|
|
175
|
+
const usedChunks = indexes.reduce((s, i) => s + (i.documentCount ?? 0), 0);
|
|
176
|
+
if (usedChunks >= sub.maxIndexedChunkTotal) {
|
|
177
|
+
console.error(`\x1b[31m✗ Index quota full (${usedChunks.toLocaleString()} / ${sub.maxIndexedChunkTotal.toLocaleString()} chunks).\x1b[0m`);
|
|
178
|
+
console.error(' Upgrade your plan or remove content before reingesting.');
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sources = argv.source ? [argv.source] : ['kb', 'shopify', 'bigcommerce', 'scraper'];
|
|
184
|
+
const siteParam = isVelaroAdmin(creds) && argv.site ? `?siteId=${argv.site}` : '';
|
|
185
|
+
|
|
186
|
+
for (const source of sources) {
|
|
187
|
+
try {
|
|
188
|
+
console.log(` Syncing ${source}...`);
|
|
189
|
+
await post(`/AzureIndexes/SyncIntegration${siteParam}`, { source, forceAll: true });
|
|
190
|
+
console.log(` \x1b[32m✓ ${source} queued\x1b[0m`);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.log(` \x1b[33m⚠ ${source} skipped: ${e.message}\x1b[0m`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.log('\nReingest queued. Run `velaro index list` to monitor progress.');
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ── export ────────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
export const indexCommand = {
|
|
202
|
+
command: 'index <subcommand>',
|
|
203
|
+
describe: 'Manage search indexes — view quota, physical index health, and reingest content',
|
|
204
|
+
builder: (yargs) =>
|
|
205
|
+
yargs
|
|
206
|
+
.command(listCommand)
|
|
207
|
+
.command(siteCommand)
|
|
208
|
+
.command(provisionCommand)
|
|
209
|
+
.command(reingestCommand)
|
|
210
|
+
.demandCommand(1, 'Specify a subcommand: list, site, provision, reingest'),
|
|
211
|
+
handler: () => {},
|
|
212
|
+
};
|
package/lib/commands/ingest.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { getCredentials, post } from '../api.js';
|
|
2
|
-
import { runCommand } from '../run.js';
|
|
3
|
-
|
|
4
|
-
export const ingestCommand = {
|
|
5
|
-
command: 'ingest',
|
|
6
|
-
describe: 'Trigger knowledge base ingestion for a completed scraper job',
|
|
7
|
-
builder: (y) =>
|
|
8
|
-
y.option('job-id', {
|
|
9
|
-
describe: 'Scraper job ID to ingest',
|
|
10
|
-
type: 'string',
|
|
11
|
-
demandOption: true,
|
|
12
|
-
}),
|
|
13
|
-
|
|
14
|
-
handler: runCommand(async (argv) => {
|
|
15
|
-
// Single credential resolution — avoids double readConfig() from calling readConfig()
|
|
16
|
-
// here and again inside post() → getCredentials().
|
|
17
|
-
const creds = await getCredentials();
|
|
18
|
-
|
|
19
|
-
console.log(`Triggering ingestion for job ${argv['job-id']} on site ${creds.siteId}...`);
|
|
20
|
-
|
|
21
|
-
const result = await post('/AzureIndexes/IngestJobDirect', {
|
|
22
|
-
siteId: creds.siteId,
|
|
23
|
-
jobId: argv['job-id'],
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
console.log('Ingestion started.');
|
|
27
|
-
console.log(` Job ID: ${result.jobId}`);
|
|
28
|
-
console.log(` Index: ${result.physicalIndex}`);
|
|
29
|
-
console.log('\nEmbedding runs in the background. Check logs for completion.');
|
|
30
|
-
}),
|
|
31
|
-
};
|
|
1
|
+
import { getCredentials, post } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
export const ingestCommand = {
|
|
5
|
+
command: 'ingest',
|
|
6
|
+
describe: 'Trigger knowledge base ingestion for a completed scraper job',
|
|
7
|
+
builder: (y) =>
|
|
8
|
+
y.option('job-id', {
|
|
9
|
+
describe: 'Scraper job ID to ingest',
|
|
10
|
+
type: 'string',
|
|
11
|
+
demandOption: true,
|
|
12
|
+
}),
|
|
13
|
+
|
|
14
|
+
handler: runCommand(async (argv) => {
|
|
15
|
+
// Single credential resolution — avoids double readConfig() from calling readConfig()
|
|
16
|
+
// here and again inside post() → getCredentials().
|
|
17
|
+
const creds = await getCredentials();
|
|
18
|
+
|
|
19
|
+
console.log(`Triggering ingestion for job ${argv['job-id']} on site ${creds.siteId}...`);
|
|
20
|
+
|
|
21
|
+
const result = await post('/AzureIndexes/IngestJobDirect', {
|
|
22
|
+
siteId: creds.siteId,
|
|
23
|
+
jobId: argv['job-id'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log('Ingestion started.');
|
|
27
|
+
console.log(` Job ID: ${result.jobId}`);
|
|
28
|
+
console.log(` Index: ${result.physicalIndex}`);
|
|
29
|
+
console.log('\nEmbedding runs in the background. Check logs for completion.');
|
|
30
|
+
}),
|
|
31
|
+
};
|
package/lib/commands/kb.js
CHANGED
|
@@ -14,10 +14,78 @@ export const kbCommand = {
|
|
|
14
14
|
.command(qnaCommand)
|
|
15
15
|
.command(overrideCommand)
|
|
16
16
|
.command(contentCommand)
|
|
17
|
-
.
|
|
17
|
+
.command(reindexCommand)
|
|
18
|
+
.demandCommand(1, 'Specify a subcommand: article, qna, override, content, reindex'),
|
|
18
19
|
handler: () => {},
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Reindex — re-embed all KB articles with the current embedding model
|
|
24
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const reindexCommand = {
|
|
27
|
+
command: 'reindex',
|
|
28
|
+
describe: 'Re-embed KB articles or scraper/ingestion content (use after embedding model upgrade)',
|
|
29
|
+
builder: (y) =>
|
|
30
|
+
y
|
|
31
|
+
.option('all', {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
describe: 'Reindex ALL sites (Velaro staff only)',
|
|
34
|
+
default: false,
|
|
35
|
+
})
|
|
36
|
+
.option('site', {
|
|
37
|
+
type: 'number',
|
|
38
|
+
describe: 'Reindex a specific site by ID (Velaro staff only)',
|
|
39
|
+
})
|
|
40
|
+
.option('ingestion', {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
describe: 'Reindex scraper/ingestion Azure Search indexers instead of KB articles',
|
|
43
|
+
default: false,
|
|
44
|
+
}),
|
|
45
|
+
handler: runCommand(async (argv) => {
|
|
46
|
+
if (argv.ingestion) {
|
|
47
|
+
if (argv.all) {
|
|
48
|
+
console.log('Starting bulk ingestion reindex for all sites — runs in background.');
|
|
49
|
+
console.log('Watch server logs ([ReindexAllIngestion]) for progress.\n');
|
|
50
|
+
const result = await post('/DatabaseTool/ReindexAllIngestionIndexes', {});
|
|
51
|
+
console.log(result.message ?? 'Queued.');
|
|
52
|
+
} else if (argv.site) {
|
|
53
|
+
console.log(`Reindexing scraper/ingestion index for site ${argv.site}…`);
|
|
54
|
+
const result = await post(`/DatabaseTool/ReindexIngestionIndex/${argv.site}`, {});
|
|
55
|
+
if (result.success) {
|
|
56
|
+
console.log(`✅ Triggered ${result.indexersRun} indexer(s) — re-processing in background.`);
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`❌ Failed: ${result.message}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.error('--ingestion requires --site <id> or --all (Velaro staff only)');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
} else if (argv.all) {
|
|
65
|
+
console.log('Starting bulk KB reindex for all sites — this runs in the background.');
|
|
66
|
+
console.log('Watch server logs ([ReindexAllKb]) for progress.\n');
|
|
67
|
+
const result = await post('/DatabaseTool/ReindexAllKb', {});
|
|
68
|
+
console.log(result.message ?? 'Queued.');
|
|
69
|
+
} else if (argv.site) {
|
|
70
|
+
console.log(`Reindexing KB articles for site ${argv.site}…`);
|
|
71
|
+
const result = await post(`/DatabaseTool/ReindexKb/${argv.site}`, {});
|
|
72
|
+
if (result.success) {
|
|
73
|
+
console.log(`✅ Done — ${result.articles} articles reindexed.`);
|
|
74
|
+
} else {
|
|
75
|
+
console.error(`❌ Failed: ${result.message}`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log("Reindexing your site's KB articles\u2026");
|
|
79
|
+
const result = await post('/KBSearchIndex/reindex', {});
|
|
80
|
+
if (result.success === false) {
|
|
81
|
+
console.error(`❌ Failed: ${result.message}`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`✅ Done — ${result.articles ?? result.count ?? 'all'} articles reindexed.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
|
|
21
89
|
// ────────────────────────────────────────────────────────────────────────────
|
|
22
90
|
// Q&A
|
|
23
91
|
// ────────────────────────────────────────────────────────────────────────────
|
package/lib/commands/login.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
|
|
2
|
-
import { readConfig, writeConfig,
|
|
3
|
-
import { runCommand }
|
|
2
|
+
import { readConfig, writeConfig, setEnvCredentials, getActiveEnv, ENVS } from '../config.js';
|
|
3
|
+
import { runCommand } from '../run.js';
|
|
4
4
|
|
|
5
5
|
export const loginCommand = {
|
|
6
6
|
command: 'login',
|
|
7
|
-
describe: 'Authenticate with Velaro
|
|
7
|
+
describe: 'Authenticate with Velaro (saves credentials for the chosen environment)',
|
|
8
8
|
builder: (y) =>
|
|
9
9
|
y
|
|
10
10
|
.option('staging', {
|
|
11
|
-
describe: '
|
|
11
|
+
describe: 'Log in to the staging environment',
|
|
12
12
|
type: 'boolean',
|
|
13
13
|
default: false,
|
|
14
14
|
})
|
|
15
15
|
.option('api', {
|
|
16
|
-
describe: 'Override the
|
|
17
|
-
'Production cutover: set this to your production API URL before going live.',
|
|
16
|
+
describe: 'Override the admin API base URL (advanced)',
|
|
18
17
|
type: 'string',
|
|
19
18
|
}),
|
|
20
19
|
|
|
21
20
|
handler: runCommand(async (argv) => {
|
|
22
|
-
const
|
|
21
|
+
const env = argv.staging || argv.api?.includes('staging') ? 'staging' : 'prod';
|
|
22
|
+
const adminApiBase = argv.api || ENVS[env].adminApiBase;
|
|
23
23
|
|
|
24
|
-
console.log(
|
|
24
|
+
console.log(`Starting Velaro login (${env})...\n`);
|
|
25
25
|
|
|
26
26
|
const deviceData = await requestDeviceCode();
|
|
27
27
|
|
|
@@ -30,11 +30,10 @@ export const loginCommand = {
|
|
|
30
30
|
console.log('Waiting for you to complete login in your browser...');
|
|
31
31
|
|
|
32
32
|
const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
|
|
33
|
-
const velaroResult = await exchangeForVelaroToken(entraTokens.access_token,
|
|
33
|
+
const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, adminApiBase);
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
apiBase,
|
|
35
|
+
setEnvCredentials(env, {
|
|
36
|
+
adminApiBase,
|
|
38
37
|
velaroToken: velaroResult.token.token,
|
|
39
38
|
velaroExpires: velaroResult.token.expires,
|
|
40
39
|
entraRefreshToken: entraTokens.refresh_token,
|
|
@@ -42,19 +41,46 @@ export const loginCommand = {
|
|
|
42
41
|
userName: velaroResult.profile?.Name,
|
|
43
42
|
});
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
// Set as active if it's the only env logged in, or if explicitly chosen
|
|
45
|
+
const cfg = readConfig();
|
|
46
|
+
if (!cfg.activeEnv || Object.keys(cfg.envs || {}).length === 1) {
|
|
47
|
+
cfg.activeEnv = env;
|
|
48
|
+
writeConfig(cfg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(`\n✅ Logged in to ${env} as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}`);
|
|
52
|
+
console.log(` Site ID: ${velaroResult.profile?.SiteId}`);
|
|
53
|
+
console.log(` API: ${adminApiBase}`);
|
|
54
|
+
console.log('\nCredentials saved. Run "velaro env" to see all environments.');
|
|
49
55
|
}),
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
export const logoutCommand = {
|
|
53
59
|
command: 'logout',
|
|
54
60
|
describe: 'Clear stored credentials',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
builder: (y) =>
|
|
62
|
+
y.option('staging', { describe: 'Log out of staging only', type: 'boolean', default: false })
|
|
63
|
+
.option('all', { describe: 'Log out of all environments', type: 'boolean', default: false }),
|
|
64
|
+
|
|
65
|
+
handler: runCommand(async (argv) => {
|
|
66
|
+
const { readConfig, writeConfig, clearConfig } = await import('../config.js');
|
|
67
|
+
if (argv.all) {
|
|
68
|
+
clearConfig();
|
|
69
|
+
console.log('Logged out of all environments.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const env = argv.staging ? 'staging' : getActiveEnv();
|
|
73
|
+
const cfg = readConfig();
|
|
74
|
+
if (cfg.envs?.[env]) {
|
|
75
|
+
delete cfg.envs[env];
|
|
76
|
+
if (cfg.activeEnv === env) {
|
|
77
|
+
const remaining = Object.keys(cfg.envs || {});
|
|
78
|
+
cfg.activeEnv = remaining[0] || 'prod';
|
|
79
|
+
}
|
|
80
|
+
writeConfig(cfg);
|
|
81
|
+
console.log(`Logged out of ${env}.`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`Not logged in to ${env}.`);
|
|
84
|
+
}
|
|
59
85
|
}),
|
|
60
86
|
};
|
package/lib/commands/mcp-key.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { get, post, del } from '../api.js';
|
|
5
|
+
import { getActiveEnv, ENVS } from '../config.js';
|
|
5
6
|
import { runCommand } from '../run.js';
|
|
6
7
|
|
|
7
8
|
export const mcpKeyCommand = {
|
|
@@ -76,28 +77,34 @@ const mcpKeyInstallCommand = {
|
|
|
76
77
|
.option('label', {
|
|
77
78
|
describe: 'Label for the key',
|
|
78
79
|
type: 'string',
|
|
79
|
-
default: 'Claude Code',
|
|
80
80
|
})
|
|
81
|
-
.option('
|
|
82
|
-
describe: '
|
|
83
|
-
type: '
|
|
81
|
+
.option('staging', {
|
|
82
|
+
describe: 'Install key for the staging environment',
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
default: false,
|
|
84
85
|
}),
|
|
85
86
|
|
|
86
87
|
handler: runCommand(async (argv) => {
|
|
87
|
-
const
|
|
88
|
+
const env = argv.staging ? 'staging' : getActiveEnv();
|
|
89
|
+
const apiBase = ENVS[env].adminApiBase;
|
|
90
|
+
const label = argv.label || (env === 'staging' ? 'Claude Code (Staging)' : 'Claude Code');
|
|
91
|
+
|
|
92
|
+
const payload = { label };
|
|
88
93
|
const result = await post('/McpApiKeys', payload);
|
|
89
94
|
const rawKey = result.rawKey;
|
|
90
95
|
|
|
91
96
|
// Write to ~/.claude/settings.json
|
|
97
|
+
// prod → entry named "velaro"
|
|
98
|
+
// staging → entry named "velaro-staging"
|
|
99
|
+
const serverName = env === 'prod' ? 'velaro' : `velaro-${env}`;
|
|
92
100
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
93
101
|
let settings = {};
|
|
94
102
|
if (fs.existsSync(settingsPath)) {
|
|
95
103
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
const apiBase = argv.api || 'https://api-admin-us-east.velaro.com';
|
|
99
106
|
settings.mcpServers = settings.mcpServers || {};
|
|
100
|
-
settings.mcpServers
|
|
107
|
+
settings.mcpServers[serverName] = {
|
|
101
108
|
command: 'npx',
|
|
102
109
|
args: ['-y', '@velaro/mcp-server'],
|
|
103
110
|
env: {
|
|
@@ -109,10 +116,10 @@ const mcpKeyInstallCommand = {
|
|
|
109
116
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
110
117
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
111
118
|
|
|
112
|
-
console.log(`✅ MCP key created: ${
|
|
113
|
-
console.log(`✅ Written to
|
|
119
|
+
console.log(`✅ MCP key created: ${label} (${result.keyPrefix}...)`);
|
|
120
|
+
console.log(`✅ Written to settings.json as "${serverName}" → ${apiBase}`);
|
|
114
121
|
console.log(`\nRestart Claude Code to pick up the new MCP server.`);
|
|
115
|
-
console.log(`\nTo
|
|
122
|
+
console.log(`\nTo use in Claude Code: say "use ${serverName}" to target ${env}.`);
|
|
116
123
|
}),
|
|
117
124
|
};
|
|
118
125
|
|