@vellumai/cli 0.6.0 → 0.6.1
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/package.json +1 -1
- package/src/__tests__/teleport.test.ts +4 -21
- package/src/__tests__/version-compat.test.ts +206 -0
- package/src/commands/backup.ts +1 -15
- package/src/commands/events.ts +146 -0
- package/src/commands/hatch.ts +1 -14
- package/src/commands/message.ts +105 -0
- package/src/commands/restore.ts +1 -21
- package/src/commands/retire.ts +2 -7
- package/src/commands/rollback.ts +8 -37
- package/src/commands/teleport.ts +18 -109
- package/src/commands/upgrade.ts +43 -43
- package/src/index.ts +6 -0
- package/src/lib/arg-utils.ts +13 -0
- package/src/lib/assistant-client.ts +228 -0
- package/src/lib/docker.ts +141 -49
- package/src/lib/hatch-local.ts +5 -2
- package/src/lib/health-check.ts +3 -8
- package/src/lib/ngrok.ts +11 -1
- package/src/lib/platform-client.ts +77 -56
- package/src/lib/upgrade-lifecycle.ts +5 -15
- package/src/lib/version-compat.ts +67 -5
|
@@ -68,19 +68,78 @@ export function clearPlatformToken(): void {
|
|
|
68
68
|
const VAK_PREFIX = "vak_";
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
71
|
+
* Sync helper – returns only the token-based auth header.
|
|
72
72
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Used internally by {@link fetchOrganizationId} (which cannot call the
|
|
74
|
+
* async {@link authHeaders} without creating a cycle) and by functions
|
|
75
|
+
* that already have an org ID in hand.
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
function tokenAuthHeader(token: string): Record<string, string> {
|
|
78
78
|
if (token.startsWith(VAK_PREFIX)) {
|
|
79
79
|
return { Authorization: `Bearer ${token}` };
|
|
80
80
|
}
|
|
81
81
|
return { "X-Session-Token": token };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** Module-level cache for org IDs to avoid redundant fetches in polling loops. */
|
|
85
|
+
const orgIdCache = new Map<string, { orgId: string; expiresAt: number }>();
|
|
86
|
+
const ORG_ID_CACHE_TTL_MS = 60_000; // 60 seconds
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns the full set of headers needed for an authenticated platform
|
|
90
|
+
* API request:
|
|
91
|
+
*
|
|
92
|
+
* - `Content-Type: application/json`
|
|
93
|
+
* - The appropriate auth header (`Authorization: Bearer` for `vak_`
|
|
94
|
+
* API keys, `X-Session-Token` for session tokens).
|
|
95
|
+
* - `Vellum-Organization-Id` – fetched from the platform. Only
|
|
96
|
+
* included for session-token callers; API keys are already org-scoped.
|
|
97
|
+
*
|
|
98
|
+
* The org ID is cached per (token, platformUrl) for 60 seconds to avoid
|
|
99
|
+
* redundant HTTP requests in tight polling loops.
|
|
100
|
+
*
|
|
101
|
+
* Auth errors (401 / 403) from the org-ID fetch are logged with a
|
|
102
|
+
* user-friendly message before re-throwing, so callers don't need to
|
|
103
|
+
* repeat that logic.
|
|
104
|
+
*/
|
|
105
|
+
export async function authHeaders(
|
|
106
|
+
token: string,
|
|
107
|
+
platformUrl?: string,
|
|
108
|
+
): Promise<Record<string, string>> {
|
|
109
|
+
const base: Record<string, string> = {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
...tokenAuthHeader(token),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (token.startsWith(VAK_PREFIX)) {
|
|
115
|
+
// API keys are org-scoped – no need to fetch the org ID.
|
|
116
|
+
return base;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cacheKey = `${token}::${platformUrl ?? ""}`;
|
|
120
|
+
const cached = orgIdCache.get(cacheKey);
|
|
121
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
122
|
+
return { ...base, "Vellum-Organization-Id": cached.orgId };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const orgId = await fetchOrganizationId(token, platformUrl);
|
|
127
|
+
orgIdCache.set(cacheKey, {
|
|
128
|
+
orgId,
|
|
129
|
+
expiresAt: Date.now() + ORG_ID_CACHE_TTL_MS,
|
|
130
|
+
});
|
|
131
|
+
return { ...base, "Vellum-Organization-Id": orgId };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
134
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
135
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
136
|
+
} else {
|
|
137
|
+
console.error(`Failed to fetch organization: ${msg}`);
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
84
143
|
export interface HatchedAssistant {
|
|
85
144
|
id: string;
|
|
86
145
|
name: string;
|
|
@@ -89,7 +148,6 @@ export interface HatchedAssistant {
|
|
|
89
148
|
|
|
90
149
|
export async function hatchAssistant(
|
|
91
150
|
token: string,
|
|
92
|
-
orgId: string,
|
|
93
151
|
platformUrl?: string,
|
|
94
152
|
): Promise<HatchedAssistant> {
|
|
95
153
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
@@ -97,11 +155,7 @@ export async function hatchAssistant(
|
|
|
97
155
|
|
|
98
156
|
const response = await fetch(url, {
|
|
99
157
|
method: "POST",
|
|
100
|
-
headers:
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
...authHeaders(token),
|
|
103
|
-
"Vellum-Organization-Id": orgId,
|
|
104
|
-
},
|
|
158
|
+
headers: await authHeaders(token, platformUrl),
|
|
105
159
|
body: JSON.stringify({}),
|
|
106
160
|
});
|
|
107
161
|
|
|
@@ -149,7 +203,7 @@ export async function fetchOrganizationId(
|
|
|
149
203
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
150
204
|
const url = `${resolvedUrl}/v1/organizations/`;
|
|
151
205
|
const response = await fetch(url, {
|
|
152
|
-
headers: { ...
|
|
206
|
+
headers: { ...tokenAuthHeader(token) },
|
|
153
207
|
});
|
|
154
208
|
|
|
155
209
|
if (!response.ok) {
|
|
@@ -210,18 +264,13 @@ export async function fetchCurrentUser(
|
|
|
210
264
|
|
|
211
265
|
export async function rollbackPlatformAssistant(
|
|
212
266
|
token: string,
|
|
213
|
-
orgId: string,
|
|
214
267
|
version?: string,
|
|
215
268
|
platformUrl?: string,
|
|
216
269
|
): Promise<{ detail: string; version: string | null }> {
|
|
217
270
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
218
271
|
const response = await fetch(`${resolvedUrl}/v1/assistants/rollback/`, {
|
|
219
272
|
method: "POST",
|
|
220
|
-
headers:
|
|
221
|
-
"Content-Type": "application/json",
|
|
222
|
-
...authHeaders(token),
|
|
223
|
-
"Vellum-Organization-Id": orgId,
|
|
224
|
-
},
|
|
273
|
+
headers: await authHeaders(token, platformUrl),
|
|
225
274
|
body: JSON.stringify(version ? { version } : {}),
|
|
226
275
|
});
|
|
227
276
|
|
|
@@ -255,18 +304,13 @@ export async function rollbackPlatformAssistant(
|
|
|
255
304
|
|
|
256
305
|
export async function platformInitiateExport(
|
|
257
306
|
token: string,
|
|
258
|
-
orgId: string,
|
|
259
307
|
description?: string,
|
|
260
308
|
platformUrl?: string,
|
|
261
309
|
): Promise<{ jobId: string; status: string }> {
|
|
262
310
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
263
311
|
const response = await fetch(`${resolvedUrl}/v1/migrations/export/`, {
|
|
264
312
|
method: "POST",
|
|
265
|
-
headers:
|
|
266
|
-
"Content-Type": "application/json",
|
|
267
|
-
...authHeaders(token),
|
|
268
|
-
"Vellum-Organization-Id": orgId,
|
|
269
|
-
},
|
|
313
|
+
headers: await authHeaders(token, platformUrl),
|
|
270
314
|
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
271
315
|
});
|
|
272
316
|
|
|
@@ -290,17 +334,13 @@ export async function platformInitiateExport(
|
|
|
290
334
|
export async function platformPollExportStatus(
|
|
291
335
|
jobId: string,
|
|
292
336
|
token: string,
|
|
293
|
-
orgId: string,
|
|
294
337
|
platformUrl?: string,
|
|
295
338
|
): Promise<{ status: string; downloadUrl?: string; error?: string }> {
|
|
296
339
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
297
340
|
const response = await fetch(
|
|
298
341
|
`${resolvedUrl}/v1/migrations/export/${jobId}/status/`,
|
|
299
342
|
{
|
|
300
|
-
headers:
|
|
301
|
-
...authHeaders(token),
|
|
302
|
-
"Vellum-Organization-Id": orgId,
|
|
303
|
-
},
|
|
343
|
+
headers: await authHeaders(token, platformUrl),
|
|
304
344
|
},
|
|
305
345
|
);
|
|
306
346
|
|
|
@@ -345,7 +385,6 @@ export async function platformDownloadExport(
|
|
|
345
385
|
export async function platformImportPreflight(
|
|
346
386
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
347
387
|
token: string,
|
|
348
|
-
orgId: string,
|
|
349
388
|
platformUrl?: string,
|
|
350
389
|
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
351
390
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
@@ -354,9 +393,8 @@ export async function platformImportPreflight(
|
|
|
354
393
|
{
|
|
355
394
|
method: "POST",
|
|
356
395
|
headers: {
|
|
396
|
+
...(await authHeaders(token, platformUrl)),
|
|
357
397
|
"Content-Type": "application/octet-stream",
|
|
358
|
-
...authHeaders(token),
|
|
359
|
-
"Vellum-Organization-Id": orgId,
|
|
360
398
|
},
|
|
361
399
|
body: new Blob([bundleData]),
|
|
362
400
|
signal: AbortSignal.timeout(120_000),
|
|
@@ -373,19 +411,17 @@ export async function platformImportPreflight(
|
|
|
373
411
|
export async function platformImportBundle(
|
|
374
412
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
375
413
|
token: string,
|
|
376
|
-
orgId: string,
|
|
377
414
|
platformUrl?: string,
|
|
378
415
|
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
379
416
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
380
417
|
const response = await fetch(`${resolvedUrl}/v1/migrations/import/`, {
|
|
381
418
|
method: "POST",
|
|
382
419
|
headers: {
|
|
420
|
+
...(await authHeaders(token, platformUrl)),
|
|
383
421
|
"Content-Type": "application/octet-stream",
|
|
384
|
-
...authHeaders(token),
|
|
385
|
-
"Vellum-Organization-Id": orgId,
|
|
386
422
|
},
|
|
387
423
|
body: new Blob([bundleData]),
|
|
388
|
-
signal: AbortSignal.timeout(
|
|
424
|
+
signal: AbortSignal.timeout(300_000),
|
|
389
425
|
});
|
|
390
426
|
|
|
391
427
|
const body = (await response.json().catch(() => ({}))) as Record<
|
|
@@ -401,17 +437,12 @@ export async function platformImportBundle(
|
|
|
401
437
|
|
|
402
438
|
export async function platformRequestUploadUrl(
|
|
403
439
|
token: string,
|
|
404
|
-
orgId: string,
|
|
405
440
|
platformUrl?: string,
|
|
406
441
|
): Promise<{ uploadUrl: string; bundleKey: string; expiresAt: string }> {
|
|
407
442
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
408
443
|
const response = await fetch(`${resolvedUrl}/v1/migrations/upload-url/`, {
|
|
409
444
|
method: "POST",
|
|
410
|
-
headers:
|
|
411
|
-
"Content-Type": "application/json",
|
|
412
|
-
...authHeaders(token),
|
|
413
|
-
"Vellum-Organization-Id": orgId,
|
|
414
|
-
},
|
|
445
|
+
headers: await authHeaders(token, platformUrl),
|
|
415
446
|
body: JSON.stringify({ content_type: "application/octet-stream" }),
|
|
416
447
|
});
|
|
417
448
|
|
|
@@ -466,7 +497,6 @@ export async function platformUploadToSignedUrl(
|
|
|
466
497
|
export async function platformImportPreflightFromGcs(
|
|
467
498
|
bundleKey: string,
|
|
468
499
|
token: string,
|
|
469
|
-
orgId: string,
|
|
470
500
|
platformUrl?: string,
|
|
471
501
|
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
472
502
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
@@ -474,13 +504,9 @@ export async function platformImportPreflightFromGcs(
|
|
|
474
504
|
`${resolvedUrl}/v1/migrations/import-preflight-from-gcs/`,
|
|
475
505
|
{
|
|
476
506
|
method: "POST",
|
|
477
|
-
headers:
|
|
478
|
-
"Content-Type": "application/json",
|
|
479
|
-
...authHeaders(token),
|
|
480
|
-
"Vellum-Organization-Id": orgId,
|
|
481
|
-
},
|
|
482
|
-
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
507
|
+
headers: await authHeaders(token, platformUrl),
|
|
483
508
|
signal: AbortSignal.timeout(120_000),
|
|
509
|
+
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
484
510
|
},
|
|
485
511
|
);
|
|
486
512
|
|
|
@@ -494,7 +520,6 @@ export async function platformImportPreflightFromGcs(
|
|
|
494
520
|
export async function platformImportBundleFromGcs(
|
|
495
521
|
bundleKey: string,
|
|
496
522
|
token: string,
|
|
497
|
-
orgId: string,
|
|
498
523
|
platformUrl?: string,
|
|
499
524
|
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
500
525
|
const resolvedUrl = platformUrl || getPlatformUrl();
|
|
@@ -502,13 +527,9 @@ export async function platformImportBundleFromGcs(
|
|
|
502
527
|
`${resolvedUrl}/v1/migrations/import-from-gcs/`,
|
|
503
528
|
{
|
|
504
529
|
method: "POST",
|
|
505
|
-
headers:
|
|
506
|
-
"Content-Type": "application/json",
|
|
507
|
-
...authHeaders(token),
|
|
508
|
-
"Vellum-Organization-Id": orgId,
|
|
509
|
-
},
|
|
530
|
+
headers: await authHeaders(token, platformUrl),
|
|
510
531
|
body: JSON.stringify({ bundle_key: bundleKey }),
|
|
511
|
-
signal: AbortSignal.timeout(
|
|
532
|
+
signal: AbortSignal.timeout(300_000),
|
|
512
533
|
},
|
|
513
534
|
);
|
|
514
535
|
|
|
@@ -16,7 +16,7 @@ import { loadGuardianToken } from "./guardian-token.js";
|
|
|
16
16
|
import { getPlatformUrl } from "./platform-client.js";
|
|
17
17
|
import { resolveImageRefs } from "./platform-releases.js";
|
|
18
18
|
import { exec, execOutput } from "./step-runner.js";
|
|
19
|
-
import {
|
|
19
|
+
import { compareVersions } from "./version-compat.js";
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Shared constants & builders for upgrade / rollback lifecycle events
|
|
@@ -318,26 +318,16 @@ export async function performDockerRollback(
|
|
|
318
318
|
|
|
319
319
|
// Validate target version < current version
|
|
320
320
|
if (currentVersion) {
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const isNewer = (() => {
|
|
325
|
-
if (target.major !== current.major) return target.major > current.major;
|
|
326
|
-
if (target.minor !== current.minor) return target.minor > current.minor;
|
|
327
|
-
return target.patch > current.patch;
|
|
328
|
-
})();
|
|
329
|
-
if (isNewer) {
|
|
321
|
+
const cmp = compareVersions(targetVersion, currentVersion);
|
|
322
|
+
if (cmp !== null) {
|
|
323
|
+
if (cmp > 0) {
|
|
330
324
|
const msg =
|
|
331
325
|
"Cannot roll back to a newer version. Use `vellum upgrade` instead.";
|
|
332
326
|
console.error(msg);
|
|
333
327
|
emitCliError("VERSION_DIRECTION", msg);
|
|
334
328
|
process.exit(1);
|
|
335
329
|
}
|
|
336
|
-
|
|
337
|
-
target.major === current.major &&
|
|
338
|
-
target.minor === current.minor &&
|
|
339
|
-
target.patch === current.patch;
|
|
340
|
-
if (isSame) {
|
|
330
|
+
if (cmp === 0) {
|
|
341
331
|
const msg = `Already on version ${targetVersion}. Nothing to roll back to.`;
|
|
342
332
|
console.error(msg);
|
|
343
333
|
emitCliError("VERSION_DIRECTION", msg);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parse a version string into { major, minor, patch } components.
|
|
3
|
-
* Handles optional `v` prefix (e.g., "v1.2.3" or "1.2.3").
|
|
2
|
+
* Parse a version string into { major, minor, patch, pre } components.
|
|
3
|
+
* Handles optional `v`/`V` prefix (e.g., "v1.2.3" or "1.2.3").
|
|
4
|
+
* Pre-release suffixes are captured (e.g., "0.6.0-staging.5" → pre: "staging.5").
|
|
4
5
|
* Returns null if the string cannot be parsed as semver.
|
|
5
6
|
*/
|
|
6
7
|
export function parseVersion(
|
|
7
8
|
version: string,
|
|
8
|
-
): { major: number; minor: number; patch: number } | null {
|
|
9
|
+
): { major: number; minor: number; patch: number; pre: string | null } | null {
|
|
9
10
|
const stripped = version.replace(/^[vV]/, "");
|
|
10
|
-
const
|
|
11
|
+
const [core, ...rest] = stripped.split("-");
|
|
12
|
+
const pre = rest.length > 0 ? rest.join("-") : null;
|
|
13
|
+
const segments = (core ?? "").split(".");
|
|
11
14
|
|
|
12
15
|
if (segments.length < 2) {
|
|
13
16
|
return null;
|
|
@@ -21,7 +24,66 @@ export function parseVersion(
|
|
|
21
24
|
return null;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
return { major, minor, patch };
|
|
27
|
+
return { major, minor, patch, pre };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compare two pre-release strings per semver §11:
|
|
32
|
+
* - Dot-separated identifiers compared left to right.
|
|
33
|
+
* - Both numeric → compare as integers.
|
|
34
|
+
* - Both non-numeric → compare lexically.
|
|
35
|
+
* - Numeric vs non-numeric → numeric sorts lower (§11.4.4).
|
|
36
|
+
* - Fewer identifiers sorts earlier when all preceding are equal.
|
|
37
|
+
*/
|
|
38
|
+
function comparePreRelease(a: string, b: string): number {
|
|
39
|
+
const pa = a.split(".");
|
|
40
|
+
const pb = b.split(".");
|
|
41
|
+
const len = Math.max(pa.length, pb.length);
|
|
42
|
+
for (let i = 0; i < len; i++) {
|
|
43
|
+
if (i >= pa.length) return -1; // a has fewer fields → a < b
|
|
44
|
+
if (i >= pb.length) return 1;
|
|
45
|
+
const aIsNum = /^\d+$/.test(pa[i]);
|
|
46
|
+
const bIsNum = /^\d+$/.test(pb[i]);
|
|
47
|
+
if (aIsNum && bIsNum) {
|
|
48
|
+
const diff = Number(pa[i]) - Number(pb[i]);
|
|
49
|
+
if (diff !== 0) return diff;
|
|
50
|
+
} else if (aIsNum !== bIsNum) {
|
|
51
|
+
return aIsNum ? -1 : 1; // numeric < non-numeric per §11.4.4
|
|
52
|
+
} else {
|
|
53
|
+
const cmp = (pa[i] ?? "").localeCompare(pb[i] ?? "");
|
|
54
|
+
if (cmp !== 0) return cmp;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compare two semver version strings.
|
|
62
|
+
* Returns negative if a < b, 0 if equal, positive if a > b.
|
|
63
|
+
*
|
|
64
|
+
* Handles pre-release suffixes per semver spec:
|
|
65
|
+
* - `0.6.0-staging.1 < 0.6.0` (pre-release < release)
|
|
66
|
+
* - `0.6.0-staging.1 < 0.6.0-staging.2` (numeric postfix comparison)
|
|
67
|
+
*
|
|
68
|
+
* Returns null if either version cannot be parsed.
|
|
69
|
+
*/
|
|
70
|
+
export function compareVersions(a: string, b: string): number | null {
|
|
71
|
+
const pa = parseVersion(a);
|
|
72
|
+
const pb = parseVersion(b);
|
|
73
|
+
if (pa === null || pb === null) return null;
|
|
74
|
+
|
|
75
|
+
const majorDiff = pa.major - pb.major;
|
|
76
|
+
if (majorDiff !== 0) return majorDiff;
|
|
77
|
+
const minorDiff = pa.minor - pb.minor;
|
|
78
|
+
if (minorDiff !== 0) return minorDiff;
|
|
79
|
+
const patchDiff = pa.patch - pb.patch;
|
|
80
|
+
if (patchDiff !== 0) return patchDiff;
|
|
81
|
+
|
|
82
|
+
// Same major.minor.patch — compare pre-release
|
|
83
|
+
if (pa.pre === null && pb.pre === null) return 0;
|
|
84
|
+
if (pa.pre !== null && pb.pre === null) return -1; // pre-release < release
|
|
85
|
+
if (pa.pre === null && pb.pre !== null) return 1;
|
|
86
|
+
return comparePreRelease(pa.pre!, pb.pre!);
|
|
25
87
|
}
|
|
26
88
|
|
|
27
89
|
/**
|