@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.
@@ -1,45 +1,71 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
 
3
3
  import { findAssistantByName } from "../lib/assistant-config.js";
4
+ import type { AssistantEntry } from "../lib/assistant-config.js";
4
5
  import {
5
6
  loadGuardianToken,
6
7
  leaseGuardianToken,
7
8
  } from "../lib/guardian-token.js";
9
+ import {
10
+ readPlatformToken,
11
+ fetchOrganizationId,
12
+ rollbackPlatformAssistant,
13
+ platformImportPreflight,
14
+ platformImportBundle,
15
+ } from "../lib/platform-client.js";
16
+ import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
8
17
 
9
18
  function printUsage(): void {
10
- console.log("Usage: vellum restore <name> --from <path> [--dry-run]");
19
+ console.log(
20
+ "Usage: vellum restore <name> --from <path> [--version <version>] [--dry-run]",
21
+ );
11
22
  console.log("");
12
- console.log("Restore a .vbundle backup into a running assistant.");
23
+ console.log("Restore data from a .vbundle backup into an assistant.");
24
+ console.log(
25
+ "With --version, also rolls back to the specified version first.",
26
+ );
13
27
  console.log("");
14
28
  console.log("Arguments:");
15
- console.log(" <name> Name of the assistant to restore into");
29
+ console.log(" <name> Name of the assistant to restore into");
16
30
  console.log("");
17
31
  console.log("Options:");
32
+ console.log(" --from <path> Path to the .vbundle file (required)");
33
+ console.log(
34
+ " --version <version> Roll back to this version before importing data",
35
+ );
18
36
  console.log(
19
- " --from <path> Path to the .vbundle file to restore (required)",
37
+ " --dry-run Show what would change without applying (data-only)",
20
38
  );
21
- console.log(" --dry-run Show what would change without applying");
22
39
  console.log("");
23
40
  console.log("Examples:");
24
- console.log(" vellum restore my-assistant --from ~/Desktop/backup.vbundle");
41
+ console.log(" vellum restore my-assistant --from backup.vbundle");
25
42
  console.log(
26
- " vellum restore my-assistant --from ~/Desktop/backup.vbundle --dry-run",
43
+ " vellum restore my-assistant --from backup.vbundle --version v1.2.3",
27
44
  );
45
+ console.log(" vellum restore my-assistant --from backup.vbundle --dry-run");
28
46
  }
29
47
 
30
48
  function parseArgs(argv: string[]): {
31
49
  name: string | undefined;
32
50
  fromPath: string | undefined;
51
+ version: string | undefined;
33
52
  dryRun: boolean;
34
53
  help: boolean;
35
54
  } {
36
55
  const args = argv.slice(3);
37
56
 
38
57
  if (args.includes("--help") || args.includes("-h")) {
39
- return { name: undefined, fromPath: undefined, dryRun: false, help: true };
58
+ return {
59
+ name: undefined,
60
+ fromPath: undefined,
61
+ version: undefined,
62
+ dryRun: false,
63
+ help: true,
64
+ };
40
65
  }
41
66
 
42
67
  let fromPath: string | undefined;
68
+ let version: string | undefined;
43
69
  const dryRun = args.includes("--dry-run");
44
70
  const positionals: string[] = [];
45
71
 
@@ -47,6 +73,14 @@ function parseArgs(argv: string[]): {
47
73
  if (args[i] === "--from" && args[i + 1]) {
48
74
  fromPath = args[i + 1];
49
75
  i++; // skip the value
76
+ } else if (args[i] === "--version") {
77
+ const next = args[i + 1];
78
+ if (!next || next.startsWith("-")) {
79
+ console.error("Error: --version requires a value");
80
+ process.exit(1);
81
+ }
82
+ version = next;
83
+ i++; // skip the value
50
84
  } else if (args[i] === "--dry-run") {
51
85
  // already handled above
52
86
  } else if (!args[i].startsWith("-")) {
@@ -54,7 +88,7 @@ function parseArgs(argv: string[]): {
54
88
  }
55
89
  }
56
90
 
57
- return { name: positionals[0], fromPath, dryRun, help: false };
91
+ return { name: positionals[0], fromPath, version, dryRun, help: false };
58
92
  }
59
93
 
60
94
  async function getAccessToken(
@@ -126,14 +160,289 @@ interface ImportResponse {
126
160
  };
127
161
  }
128
162
 
163
+ // ---------------------------------------------------------------------------
164
+ // Platform (Vellum-hosted) restore via Django migration import
165
+ // ---------------------------------------------------------------------------
166
+
167
+ async function restorePlatform(
168
+ entry: AssistantEntry,
169
+ name: string,
170
+ bundleData: Buffer,
171
+ opts: { version?: string; dryRun: boolean },
172
+ ): Promise<void> {
173
+ // Step 1 — Authenticate
174
+ const token = readPlatformToken();
175
+ if (!token) {
176
+ console.error("Not logged in. Run 'vellum login' first.");
177
+ process.exit(1);
178
+ }
179
+
180
+ let orgId: string;
181
+ try {
182
+ orgId = await fetchOrganizationId(token);
183
+ } catch (err) {
184
+ const msg = err instanceof Error ? err.message : String(err);
185
+ if (msg.includes("401") || msg.includes("403")) {
186
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
187
+ process.exit(1);
188
+ }
189
+ throw err;
190
+ }
191
+
192
+ // Step 2 — Dry-run path
193
+ if (opts.dryRun) {
194
+ if (opts.version) {
195
+ console.error(
196
+ "Dry-run is not supported with --version. Use `vellum restore --from <path> --dry-run` for data-only preflight.",
197
+ );
198
+ process.exit(1);
199
+ }
200
+
201
+ console.log("Running preflight analysis...\n");
202
+
203
+ let preflightResult: { statusCode: number; body: Record<string, unknown> };
204
+ try {
205
+ preflightResult = await platformImportPreflight(
206
+ new Uint8Array(bundleData),
207
+ token,
208
+ orgId,
209
+ );
210
+ } catch (err) {
211
+ if (err instanceof Error && err.name === "TimeoutError") {
212
+ console.error("Error: Preflight request timed out after 2 minutes.");
213
+ process.exit(1);
214
+ }
215
+ throw err;
216
+ }
217
+
218
+ if (
219
+ preflightResult.statusCode === 401 ||
220
+ preflightResult.statusCode === 403
221
+ ) {
222
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
223
+ process.exit(1);
224
+ }
225
+
226
+ if (preflightResult.statusCode === 404) {
227
+ console.error(
228
+ "No managed assistant found. Ensure your assistant is running.",
229
+ );
230
+ process.exit(1);
231
+ }
232
+
233
+ if (preflightResult.statusCode === 409) {
234
+ console.error(
235
+ "Multiple assistants found. This is a platform configuration issue.",
236
+ );
237
+ process.exit(1);
238
+ }
239
+
240
+ if (
241
+ preflightResult.statusCode === 502 ||
242
+ preflightResult.statusCode === 503 ||
243
+ preflightResult.statusCode === 504
244
+ ) {
245
+ console.error(
246
+ `Assistant is unreachable. Try 'vellum wake ${name}' first.`,
247
+ );
248
+ process.exit(1);
249
+ }
250
+
251
+ if (preflightResult.statusCode !== 200) {
252
+ console.error(
253
+ `Error: Preflight check failed (${preflightResult.statusCode}): ${JSON.stringify(preflightResult.body)}`,
254
+ );
255
+ process.exit(1);
256
+ }
257
+
258
+ const result = preflightResult.body as unknown as PreflightResponse;
259
+
260
+ if (!result.can_import) {
261
+ if (result.validation?.errors?.length) {
262
+ console.error("Import blocked by validation errors:");
263
+ for (const err of result.validation.errors) {
264
+ console.error(
265
+ ` - ${err.message}${err.path ? ` (${err.path})` : ""}`,
266
+ );
267
+ }
268
+ }
269
+ if (result.conflicts?.length) {
270
+ console.error("Import blocked by conflicts:");
271
+ for (const conflict of result.conflicts) {
272
+ console.error(
273
+ ` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`,
274
+ );
275
+ }
276
+ }
277
+ process.exit(1);
278
+ }
279
+
280
+ // Print summary table
281
+ const summary = result.summary ?? {
282
+ files_to_create: 0,
283
+ files_to_overwrite: 0,
284
+ files_unchanged: 0,
285
+ total_files: 0,
286
+ };
287
+ console.log("Preflight analysis:");
288
+ console.log(` Files to create: ${summary.files_to_create}`);
289
+ console.log(` Files to overwrite: ${summary.files_to_overwrite}`);
290
+ console.log(` Files unchanged: ${summary.files_unchanged}`);
291
+ console.log(` Total: ${summary.total_files}`);
292
+ console.log("");
293
+
294
+ const conflicts = result.conflicts ?? [];
295
+ console.log(
296
+ `Conflicts: ${conflicts.length > 0 ? conflicts.map((c) => c.message).join(", ") : "none"}`,
297
+ );
298
+
299
+ // List individual files with their action
300
+ if (result.files && result.files.length > 0) {
301
+ console.log("");
302
+ console.log("Files:");
303
+ for (const file of result.files) {
304
+ console.log(` [${file.action}] ${file.path}`);
305
+ }
306
+ }
307
+
308
+ return;
309
+ }
310
+
311
+ // Step 3 — Version rollback (if --version set)
312
+ if (opts.version) {
313
+ console.log(
314
+ `Rolling back to version ${opts.version} before restoring data...`,
315
+ );
316
+
317
+ try {
318
+ await rollbackPlatformAssistant(token, orgId, opts.version);
319
+ } catch (err) {
320
+ const msg = err instanceof Error ? err.message : String(err);
321
+ if (msg.includes("401") || msg.includes("403")) {
322
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
323
+ process.exit(1);
324
+ }
325
+ console.error(`Error: Rollback failed — ${msg}`);
326
+ process.exit(1);
327
+ }
328
+
329
+ console.log(
330
+ `Rolled back to ${opts.version}. Proceeding with data restore...`,
331
+ );
332
+ }
333
+
334
+ // Step 4 — Data import
335
+ console.log("Importing backup data...");
336
+
337
+ let importResult: { statusCode: number; body: Record<string, unknown> };
338
+ try {
339
+ importResult = await platformImportBundle(
340
+ new Uint8Array(bundleData),
341
+ token,
342
+ orgId,
343
+ );
344
+ } catch (err) {
345
+ if (err instanceof Error && err.name === "TimeoutError") {
346
+ console.error("Error: Import request timed out after 2 minutes.");
347
+ process.exit(1);
348
+ }
349
+ throw err;
350
+ }
351
+
352
+ if (importResult.statusCode === 401 || importResult.statusCode === 403) {
353
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
354
+ process.exit(1);
355
+ }
356
+
357
+ if (importResult.statusCode === 404) {
358
+ console.error(
359
+ "No managed assistant found. Ensure your assistant is running.",
360
+ );
361
+ process.exit(1);
362
+ }
363
+
364
+ if (importResult.statusCode === 409) {
365
+ console.error(
366
+ "Multiple assistants found. This is a platform configuration issue.",
367
+ );
368
+ process.exit(1);
369
+ }
370
+
371
+ if (
372
+ importResult.statusCode === 502 ||
373
+ importResult.statusCode === 503 ||
374
+ importResult.statusCode === 504
375
+ ) {
376
+ console.error(`Assistant is unreachable. Try 'vellum wake ${name}' first.`);
377
+ process.exit(1);
378
+ }
379
+
380
+ if (importResult.statusCode < 200 || importResult.statusCode >= 300) {
381
+ console.error(`Error: Import failed (${importResult.statusCode})`);
382
+ process.exit(1);
383
+ }
384
+
385
+ const result = importResult.body as unknown as ImportResponse;
386
+
387
+ if (!result.success) {
388
+ console.error(
389
+ `Error: Import failed — ${result.message ?? result.reason ?? "unknown reason"}`,
390
+ );
391
+ for (const err of result.errors ?? []) {
392
+ console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
393
+ }
394
+ process.exit(1);
395
+ }
396
+
397
+ // Print import report
398
+ const summary = result.summary ?? {
399
+ total_files: 0,
400
+ files_created: 0,
401
+ files_overwritten: 0,
402
+ files_skipped: 0,
403
+ backups_created: 0,
404
+ };
405
+ console.log("✅ Restore complete.");
406
+ console.log(` Files created: ${summary.files_created}`);
407
+ console.log(` Files overwritten: ${summary.files_overwritten}`);
408
+ console.log(` Files skipped: ${summary.files_skipped}`);
409
+ console.log(` Backups created: ${summary.backups_created}`);
410
+
411
+ // Print warnings if any
412
+ const warnings = result.warnings ?? [];
413
+ if (warnings.length > 0) {
414
+ console.log("");
415
+ console.log("Warnings:");
416
+ for (const warning of warnings) {
417
+ console.log(` ⚠️ ${warning}`);
418
+ }
419
+ }
420
+ }
421
+
129
422
  export async function restore(): Promise<void> {
130
- const { name, fromPath, dryRun, help } = parseArgs(process.argv);
423
+ const { name, fromPath, version, dryRun, help } = parseArgs(process.argv);
131
424
 
132
425
  if (help) {
133
426
  printUsage();
134
427
  process.exit(0);
135
428
  }
136
429
 
430
+ // --version requires --from
431
+ if (version && !fromPath) {
432
+ console.error(
433
+ "A backup file is required for restore. Use --from <path> to specify the .vbundle file.",
434
+ );
435
+ process.exit(1);
436
+ }
437
+
438
+ // --dry-run is not supported with --version
439
+ if (version && dryRun) {
440
+ console.error(
441
+ "Dry-run is not supported with --version. Use `vellum restore --from <path> --dry-run` for data-only preflight.",
442
+ );
443
+ process.exit(1);
444
+ }
445
+
137
446
  if (!name || !fromPath) {
138
447
  console.error("Error: Both <name> and --from <path> are required.");
139
448
  console.error("");
@@ -160,8 +469,24 @@ export async function restore(): Promise<void> {
160
469
  const sizeMB = (bundleData.byteLength / (1024 * 1024)).toFixed(2);
161
470
  console.log(`Reading ${fromPath} (${sizeMB} MB)...`);
162
471
 
163
- // Obtain auth token
164
- const accessToken = await getAccessToken(
472
+ // Detect topology and route platform assistants through Django import
473
+ const cloud =
474
+ entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
475
+ if (cloud === "vellum") {
476
+ await restorePlatform(entry, name, bundleData, { version, dryRun });
477
+ return;
478
+ }
479
+
480
+ if (version && cloud !== "docker") {
481
+ console.error(
482
+ "Restore with --version is only supported for Docker and managed assistants.",
483
+ );
484
+ process.exit(1);
485
+ }
486
+
487
+ // Obtain auth token (acquired before dry-run or before data import;
488
+ // re-acquired after version rollback since containers restart).
489
+ let accessToken = await getAccessToken(
165
490
  entry.runtimeUrl,
166
491
  entry.assistantId,
167
492
  name,
@@ -215,13 +540,17 @@ export async function restore(): Promise<void> {
215
540
  if (result.validation?.errors?.length) {
216
541
  console.error("Import blocked by validation errors:");
217
542
  for (const err of result.validation.errors) {
218
- console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
543
+ console.error(
544
+ ` - ${err.message}${err.path ? ` (${err.path})` : ""}`,
545
+ );
219
546
  }
220
547
  }
221
548
  if (result.conflicts?.length) {
222
549
  console.error("Import blocked by conflicts:");
223
550
  for (const conflict of result.conflicts) {
224
- console.error(` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`);
551
+ console.error(
552
+ ` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`,
553
+ );
225
554
  }
226
555
  }
227
556
  process.exit(1);
@@ -255,8 +584,22 @@ export async function restore(): Promise<void> {
255
584
  }
256
585
  }
257
586
  } else {
258
- // Full import
259
- console.log("Importing backup...\n");
587
+ // Version rollback (when --version is specified)
588
+ if (version) {
589
+ console.log(`Rolling back to version ${version}...`);
590
+ await performDockerRollback(entry, { targetVersion: version });
591
+ console.log("");
592
+
593
+ // Re-acquire auth token since containers were restarted during rollback
594
+ accessToken = await getAccessToken(
595
+ entry.runtimeUrl,
596
+ entry.assistantId,
597
+ name,
598
+ );
599
+ }
600
+
601
+ // Data import
602
+ console.log("Importing backup data...\n");
260
603
 
261
604
  let response: Response;
262
605
  try {
@@ -1,10 +1,10 @@
1
1
  import { spawn } from "child_process";
2
2
  import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
3
  import { basename, dirname, join } from "path";
5
4
 
6
5
  import {
7
6
  findAssistantByName,
7
+ getBaseDir,
8
8
  loadAllAssistants,
9
9
  removeAssistantEntry,
10
10
  } from "../lib/assistant-config";
@@ -109,10 +109,10 @@ async function retireLocal(name: string, entry: AssistantEntry): Promise<void> {
109
109
  await stopOrphanedDaemonProcesses();
110
110
  }
111
111
 
112
- // For named instances (instanceDir differs from homedir), archive and
113
- // remove the entire instance directory. For the default instance
114
- // (instanceDir is homedir), archive only the .vellum subdirectory.
115
- const isNamedInstance = resources.instanceDir !== homedir();
112
+ // For named instances (instanceDir differs from the base directory),
113
+ // archive and remove the entire instance directory. For the default
114
+ // instance, archive only the .vellum subdirectory.
115
+ const isNamedInstance = resources.instanceDir !== getBaseDir();
116
116
  const dirToArchive = isNamedInstance ? resources.instanceDir : vellumDir;
117
117
 
118
118
  // Move the data directory out of the way so the path is immediately available