@vellumai/cli 0.5.7 → 0.5.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,6 +4,13 @@ import { dirname, join } from "path";
4
4
  import { findAssistantByName } from "../lib/assistant-config";
5
5
  import { getBackupsDir, formatSize } from "../lib/backup-ops.js";
6
6
  import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
7
+ import {
8
+ readPlatformToken,
9
+ fetchOrganizationId,
10
+ platformInitiateExport,
11
+ platformPollExportStatus,
12
+ platformDownloadExport,
13
+ } from "../lib/platform-client.js";
7
14
 
8
15
  export async function backup(): Promise<void> {
9
16
  const args = process.argv.slice(3);
@@ -55,6 +62,14 @@ export async function backup(): Promise<void> {
55
62
  process.exit(1);
56
63
  }
57
64
 
65
+ // Detect topology and route platform assistants through Django export
66
+ const cloud =
67
+ entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
68
+ if (cloud === "vellum") {
69
+ await backupPlatform(name, outputArg);
70
+ return;
71
+ }
72
+
58
73
  // Obtain an auth token
59
74
  let accessToken: string;
60
75
  const tokenData = loadGuardianToken(entry.assistantId);
@@ -164,3 +179,112 @@ export async function backup(): Promise<void> {
164
179
  console.log(`Manifest SHA-256: ${manifestSha}`);
165
180
  }
166
181
  }
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Platform (Vellum-hosted) backup via Django async migration export
185
+ // ---------------------------------------------------------------------------
186
+
187
+ async function backupPlatform(name: string, outputArg?: string): Promise<void> {
188
+ // Step 1 — Authenticate
189
+ const token = readPlatformToken();
190
+ if (!token) {
191
+ console.error("Not logged in. Run 'vellum login' first.");
192
+ process.exit(1);
193
+ }
194
+
195
+ let orgId: string;
196
+ try {
197
+ orgId = await fetchOrganizationId(token);
198
+ } catch (err) {
199
+ const msg = err instanceof Error ? err.message : String(err);
200
+ if (msg.includes("401") || msg.includes("403")) {
201
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
202
+ process.exit(1);
203
+ }
204
+ throw err;
205
+ }
206
+
207
+ // Step 2 — Initiate export job
208
+ let jobId: string;
209
+ try {
210
+ const result = await platformInitiateExport(token, orgId, "CLI backup");
211
+ jobId = result.jobId;
212
+ } catch (err) {
213
+ const msg = err instanceof Error ? err.message : String(err);
214
+ if (msg.includes("401") || msg.includes("403")) {
215
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
216
+ process.exit(1);
217
+ }
218
+ if (msg.includes("429")) {
219
+ console.error(
220
+ "Too many export requests. Please wait before trying again.",
221
+ );
222
+ process.exit(1);
223
+ }
224
+ throw err;
225
+ }
226
+
227
+ console.log(`Export started (job ${jobId})...`);
228
+
229
+ // Step 3 — Poll for completion
230
+ const POLL_INTERVAL_MS = 2_000;
231
+ const TIMEOUT_MS = 5 * 60 * 1_000; // 5 minutes
232
+ const deadline = Date.now() + TIMEOUT_MS;
233
+ let downloadUrl: string | undefined;
234
+
235
+ while (Date.now() < deadline) {
236
+ let status: { status: string; downloadUrl?: string; error?: string };
237
+ try {
238
+ status = await platformPollExportStatus(jobId, token, orgId);
239
+ } catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ // Let non-transient errors (e.g. 404 "job not found") propagate immediately
242
+ if (msg.includes("not found")) {
243
+ throw err;
244
+ }
245
+ console.warn(`Polling failed, retrying... (${msg})`);
246
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
247
+ continue;
248
+ }
249
+
250
+ if (status.status === "complete") {
251
+ downloadUrl = status.downloadUrl;
252
+ break;
253
+ }
254
+
255
+ if (status.status === "failed") {
256
+ console.error(`Export failed: ${status.error ?? "unknown error"}`);
257
+ process.exit(1);
258
+ }
259
+
260
+ // Still in progress — wait and retry
261
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
262
+ }
263
+
264
+ if (!downloadUrl) {
265
+ console.error("Export timed out after 5 minutes.");
266
+ process.exit(1);
267
+ }
268
+
269
+ // Step 4 — Download bundle
270
+ const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
271
+ const outputPath =
272
+ outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
273
+
274
+ mkdirSync(dirname(outputPath), { recursive: true });
275
+
276
+ const response = await platformDownloadExport(downloadUrl);
277
+ const arrayBuffer = await response.arrayBuffer();
278
+ const data = new Uint8Array(arrayBuffer);
279
+
280
+ writeFileSync(outputPath, data);
281
+
282
+ // Step 5 — Print success
283
+ console.log(`Backup saved to ${outputPath}`);
284
+ console.log(`Size: ${formatSize(data.byteLength)}`);
285
+
286
+ const manifestSha = response.headers.get("X-Vbundle-Manifest-Sha256");
287
+ if (manifestSha) {
288
+ console.log(`Manifest SHA-256: ${manifestSha}`);
289
+ }
290
+ }
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "crypto";
1
2
  import {
2
3
  appendFileSync,
3
4
  existsSync,
@@ -102,7 +103,7 @@ export async function buildStartupScript(
102
103
  instanceName: string,
103
104
  cloud: RemoteHost,
104
105
  configValues: Record<string, string> = {},
105
- ): Promise<string> {
106
+ ): Promise<{ script: string; laptopBootstrapSecret: string }> {
106
107
  const platformUrl = getPlatformUrl();
107
108
  const logPath =
108
109
  cloud === "custom"
@@ -115,18 +116,32 @@ export async function buildStartupScript(
115
116
  const ownershipFixup = buildOwnershipFixup();
116
117
 
117
118
  if (species === "openclaw") {
118
- return await buildOpenclawStartupScript(
119
+ const script = await buildOpenclawStartupScript(
119
120
  sshUser,
120
121
  providerApiKeys,
121
122
  timestampRedirect,
122
123
  userSetup,
123
124
  ownershipFixup,
124
125
  );
126
+ return { script, laptopBootstrapSecret: "" };
125
127
  }
126
128
 
129
+ // Generate a bootstrap secret for the laptop that initiated this remote
130
+ // hatch. The startup script exports it as GUARDIAN_BOOTSTRAP_SECRET so
131
+ // that when `vellum hatch --remote docker` runs on the VM, the docker
132
+ // hatch detects the pre-set env var and appends its own secret.
133
+ const laptopBootstrapSecret = randomBytes(32).toString("hex");
134
+
127
135
  // Build bash lines that set each provider API key as a shell variable
128
136
  // and corresponding dotenv lines for the env file.
129
- const envSetLines = Object.entries(providerApiKeys)
137
+ // Include the laptop bootstrap secret so that when the remote runs
138
+ // `vellum hatch --remote docker`, the docker hatch detects the pre-set
139
+ // env var and appends its own secret for multi-secret guardian init.
140
+ const allEnvEntries: Record<string, string> = {
141
+ ...providerApiKeys,
142
+ GUARDIAN_BOOTSTRAP_SECRET: laptopBootstrapSecret,
143
+ };
144
+ const envSetLines = Object.entries(allEnvEntries)
130
145
  .map(([envVar, value]) => `${envVar}=${value}`)
131
146
  .join("\n");
132
147
  const dotenvLines = Object.keys(providerApiKeys)
@@ -149,7 +164,9 @@ echo "Default workspace config written to \$VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH
149
164
  `;
150
165
  }
151
166
 
152
- return `#!/bin/bash
167
+ return {
168
+ laptopBootstrapSecret,
169
+ script: `#!/bin/bash
153
170
  set -e
154
171
 
155
172
  ${timestampRedirect}
@@ -166,6 +183,7 @@ DOTENV_EOF
166
183
 
167
184
  ${ownershipFixup}
168
185
  ${configWriteBlock}
186
+ export GUARDIAN_BOOTSTRAP_SECRET
169
187
  export VELLUM_SSH_USER="\$SSH_USER"
170
188
  export VELLUM_ASSISTANT_NAME="\$VELLUM_ASSISTANT_NAME"
171
189
  export VELLUM_CLOUD="${cloud}"
@@ -175,7 +193,8 @@ echo "Install script downloaded (\$(wc -c < ${INSTALL_SCRIPT_REMOTE_PATH}) bytes
175
193
  chmod +x ${INSTALL_SCRIPT_REMOTE_PATH}
176
194
  echo "Running install script..."
177
195
  source ${INSTALL_SCRIPT_REMOTE_PATH}
178
- `;
196
+ `,
197
+ };
179
198
  }
180
199
 
181
200
  const DEFAULT_REMOTE: RemoteHost = "local";
@@ -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 {