@vellumai/cli 0.5.6 → 0.5.8
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/knip.json +3 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +152 -13
- package/src/commands/hatch.ts +120 -65
- package/src/commands/restore.ts +359 -16
- package/src/commands/retire.ts +5 -5
- package/src/commands/rollback.ts +436 -142
- package/src/commands/upgrade.ts +575 -205
- package/src/index.ts +4 -4
- package/src/lib/assistant-config.ts +33 -6
- package/src/lib/aws.ts +15 -8
- package/src/lib/backup-ops.ts +213 -0
- package/src/lib/cli-error.ts +93 -0
- package/src/lib/config-utils.ts +59 -0
- package/src/lib/docker.ts +99 -50
- package/src/lib/doctor-client.ts +11 -1
- package/src/lib/gcp.ts +19 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +30 -9
- package/src/lib/platform-client.ts +205 -3
- package/src/lib/platform-releases.ts +112 -0
- package/src/lib/upgrade-lifecycle.ts +844 -0
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { join, dirname } from "path";
|
|
11
11
|
|
|
12
|
-
const DEFAULT_PLATFORM_URL = "https://platform.vellum.ai";
|
|
13
|
-
|
|
14
12
|
function getXdgConfigHome(): string {
|
|
15
13
|
return process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
|
|
16
14
|
}
|
|
@@ -20,7 +18,25 @@ function getPlatformTokenPath(): string {
|
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function getPlatformUrl(): string {
|
|
23
|
-
|
|
21
|
+
let configUrl: string | undefined;
|
|
22
|
+
try {
|
|
23
|
+
const base = process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
24
|
+
const configPath = join(base, ".vellum", "workspace", "config.json");
|
|
25
|
+
if (existsSync(configPath)) {
|
|
26
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
27
|
+
string,
|
|
28
|
+
unknown
|
|
29
|
+
>;
|
|
30
|
+
const val = (raw.platform as Record<string, unknown> | undefined)
|
|
31
|
+
?.baseUrl;
|
|
32
|
+
if (typeof val === "string" && val.trim()) configUrl = val.trim();
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// Config not available — fall through
|
|
36
|
+
}
|
|
37
|
+
return (
|
|
38
|
+
configUrl || process.env.VELLUM_PLATFORM_URL || "https://platform.vellum.ai"
|
|
39
|
+
);
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
export function readPlatformToken(): string | null {
|
|
@@ -113,3 +129,189 @@ export async function fetchCurrentUser(token: string): Promise<PlatformUser> {
|
|
|
113
129
|
const body = (await response.json()) as AllauthSessionResponse;
|
|
114
130
|
return body.data.user;
|
|
115
131
|
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Rollback
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export async function rollbackPlatformAssistant(
|
|
138
|
+
token: string,
|
|
139
|
+
orgId: string,
|
|
140
|
+
version?: string,
|
|
141
|
+
): Promise<{ detail: string; version: string | null }> {
|
|
142
|
+
const platformUrl = getPlatformUrl();
|
|
143
|
+
const response = await fetch(`${platformUrl}/v1/assistants/rollback/`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"X-Session-Token": token,
|
|
148
|
+
"Vellum-Organization-Id": orgId,
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(version ? { version } : {}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
154
|
+
detail?: string;
|
|
155
|
+
version?: string | null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (response.status === 200) {
|
|
159
|
+
return { detail: body.detail ?? "", version: body.version ?? null };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (response.status === 400) {
|
|
163
|
+
throw new Error(body.detail ?? "Rollback failed: bad request");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (response.status === 404) {
|
|
167
|
+
throw new Error(body.detail ?? "Rollback target not found");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (response.status === 502) {
|
|
171
|
+
throw new Error(body.detail ?? "Rollback failed: transport error");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new Error(`Rollback failed: ${response.status} ${response.statusText}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Migration export
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
export async function platformInitiateExport(
|
|
182
|
+
token: string,
|
|
183
|
+
orgId: string,
|
|
184
|
+
description?: string,
|
|
185
|
+
): Promise<{ jobId: string; status: string }> {
|
|
186
|
+
const platformUrl = getPlatformUrl();
|
|
187
|
+
const response = await fetch(`${platformUrl}/v1/migrations/export/`, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
"Content-Type": "application/json",
|
|
191
|
+
"X-Session-Token": token,
|
|
192
|
+
"Vellum-Organization-Id": orgId,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({ description: description ?? "CLI backup" }),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (response.status !== 201) {
|
|
198
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
199
|
+
detail?: string;
|
|
200
|
+
};
|
|
201
|
+
throw new Error(
|
|
202
|
+
body.detail ??
|
|
203
|
+
`Export initiation failed: ${response.status} ${response.statusText}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const body = (await response.json()) as {
|
|
208
|
+
job_id: string;
|
|
209
|
+
status: string;
|
|
210
|
+
};
|
|
211
|
+
return { jobId: body.job_id, status: body.status };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function platformPollExportStatus(
|
|
215
|
+
jobId: string,
|
|
216
|
+
token: string,
|
|
217
|
+
orgId: string,
|
|
218
|
+
): Promise<{ status: string; downloadUrl?: string; error?: string }> {
|
|
219
|
+
const platformUrl = getPlatformUrl();
|
|
220
|
+
const response = await fetch(
|
|
221
|
+
`${platformUrl}/v1/migrations/export/${jobId}/status/`,
|
|
222
|
+
{
|
|
223
|
+
headers: {
|
|
224
|
+
"X-Session-Token": token,
|
|
225
|
+
"Vellum-Organization-Id": orgId,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (response.status === 404) {
|
|
231
|
+
throw new Error("Export job not found");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Export status check failed: ${response.status} ${response.statusText}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const body = (await response.json()) as {
|
|
241
|
+
status: string;
|
|
242
|
+
download_url?: string;
|
|
243
|
+
error?: string;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
status: body.status,
|
|
247
|
+
downloadUrl: body.download_url,
|
|
248
|
+
error: body.error,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function platformDownloadExport(
|
|
253
|
+
downloadUrl: string,
|
|
254
|
+
): Promise<Response> {
|
|
255
|
+
const response = await fetch(downloadUrl);
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Download failed: ${response.status} ${response.statusText}`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Migration import
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
export async function platformImportPreflight(
|
|
269
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
270
|
+
token: string,
|
|
271
|
+
orgId: string,
|
|
272
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
273
|
+
const platformUrl = getPlatformUrl();
|
|
274
|
+
const response = await fetch(
|
|
275
|
+
`${platformUrl}/v1/migrations/import-preflight/`,
|
|
276
|
+
{
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/octet-stream",
|
|
280
|
+
"X-Session-Token": token,
|
|
281
|
+
"Vellum-Organization-Id": orgId,
|
|
282
|
+
},
|
|
283
|
+
body: new Blob([bundleData]),
|
|
284
|
+
signal: AbortSignal.timeout(120_000),
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
289
|
+
string,
|
|
290
|
+
unknown
|
|
291
|
+
>;
|
|
292
|
+
return { statusCode: response.status, body };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function platformImportBundle(
|
|
296
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
297
|
+
token: string,
|
|
298
|
+
orgId: string,
|
|
299
|
+
): Promise<{ statusCode: number; body: Record<string, unknown> }> {
|
|
300
|
+
const platformUrl = getPlatformUrl();
|
|
301
|
+
const response = await fetch(`${platformUrl}/v1/migrations/import/`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers: {
|
|
304
|
+
"Content-Type": "application/octet-stream",
|
|
305
|
+
"X-Session-Token": token,
|
|
306
|
+
"Vellum-Organization-Id": orgId,
|
|
307
|
+
},
|
|
308
|
+
body: new Blob([bundleData]),
|
|
309
|
+
signal: AbortSignal.timeout(120_000),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const body = (await response.json().catch(() => ({}))) as Record<
|
|
313
|
+
string,
|
|
314
|
+
unknown
|
|
315
|
+
>;
|
|
316
|
+
return { statusCode: response.status, body };
|
|
317
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getPlatformUrl } from "./platform-client.js";
|
|
2
|
+
import { DOCKERHUB_IMAGES } from "./docker.js";
|
|
3
|
+
import type { ServiceName } from "./docker.js";
|
|
4
|
+
|
|
5
|
+
export interface ResolvedImageRefs {
|
|
6
|
+
imageTags: Record<ServiceName, string>;
|
|
7
|
+
source: "platform" | "dockerhub";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve image references for a given version.
|
|
12
|
+
*
|
|
13
|
+
* Tries the platform API first (returns GCR digest-based refs when available),
|
|
14
|
+
* then falls back to DockerHub tag-based refs when the platform is unreachable
|
|
15
|
+
* or the version is not found.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveImageRefs(
|
|
18
|
+
version: string,
|
|
19
|
+
log?: (msg: string) => void,
|
|
20
|
+
): Promise<ResolvedImageRefs> {
|
|
21
|
+
log?.("Resolving image references...");
|
|
22
|
+
|
|
23
|
+
const platformRefs = await fetchPlatformImageRefs(version, log);
|
|
24
|
+
if (platformRefs) {
|
|
25
|
+
log?.("Resolved image refs from platform API");
|
|
26
|
+
return { imageTags: platformRefs, source: "platform" };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
log?.("Falling back to DockerHub tags");
|
|
30
|
+
const imageTags: Record<ServiceName, string> = {
|
|
31
|
+
assistant: `${DOCKERHUB_IMAGES.assistant}:${version}`,
|
|
32
|
+
"credential-executor": `${DOCKERHUB_IMAGES["credential-executor"]}:${version}`,
|
|
33
|
+
gateway: `${DOCKERHUB_IMAGES.gateway}:${version}`,
|
|
34
|
+
};
|
|
35
|
+
return { imageTags, source: "dockerhub" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fetch image references from the platform releases API.
|
|
40
|
+
*
|
|
41
|
+
* Returns a record of service name to image ref (GCR digest-based) for the
|
|
42
|
+
* given version, or null if the platform is unreachable, the version is not
|
|
43
|
+
* found, or any error occurs.
|
|
44
|
+
*/
|
|
45
|
+
async function fetchPlatformImageRefs(
|
|
46
|
+
version: string,
|
|
47
|
+
log?: (msg: string) => void,
|
|
48
|
+
): Promise<Record<ServiceName, string> | null> {
|
|
49
|
+
try {
|
|
50
|
+
const platformUrl = getPlatformUrl();
|
|
51
|
+
const url = `${platformUrl}/v1/releases/?stable=true`;
|
|
52
|
+
|
|
53
|
+
log?.(`Fetching releases from ${url}`);
|
|
54
|
+
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
signal: AbortSignal.timeout(10_000),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
log?.(`Platform API returned ${response.status}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const releases = (await response.json()) as Array<{
|
|
65
|
+
version?: string;
|
|
66
|
+
assistant_image_ref?: string | null;
|
|
67
|
+
gateway_image_ref?: string | null;
|
|
68
|
+
credential_executor_image_ref?: string | null;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
// Strip leading "v" from the requested version for matching
|
|
72
|
+
const normalizedVersion = version.replace(/^v/, "");
|
|
73
|
+
|
|
74
|
+
const release = releases.find((r) => {
|
|
75
|
+
const releaseVersion = (r.version ?? "").replace(/^v/, "");
|
|
76
|
+
return releaseVersion === normalizedVersion;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!release) {
|
|
80
|
+
log?.(`Version ${version} not found in platform releases`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const assistantImage = release.assistant_image_ref;
|
|
85
|
+
const gatewayImage = release.gateway_image_ref;
|
|
86
|
+
let credentialExecutorImage = release.credential_executor_image_ref;
|
|
87
|
+
|
|
88
|
+
// Assistant and gateway images are required; credential-executor falls back to DockerHub
|
|
89
|
+
if (!assistantImage || !gatewayImage) {
|
|
90
|
+
log?.("Platform release missing required image refs");
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fall back to DockerHub for credential-executor if its image ref is null
|
|
95
|
+
if (!credentialExecutorImage) {
|
|
96
|
+
credentialExecutorImage = `${DOCKERHUB_IMAGES["credential-executor"]}:${version}`;
|
|
97
|
+
log?.(
|
|
98
|
+
"credential-executor image not in platform release, using DockerHub fallback",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
assistant: assistantImage,
|
|
104
|
+
"credential-executor": credentialExecutorImage,
|
|
105
|
+
gateway: gatewayImage,
|
|
106
|
+
};
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
log?.(`Platform image ref resolution failed: ${message}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|