@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.
@@ -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
- return process.env.VELLUM_PLATFORM_URL ?? DEFAULT_PLATFORM_URL;
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
+ }