@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.
@@ -1,5 +1,4 @@
1
1
  import { randomBytes } from "crypto";
2
- import { join } from "path";
3
2
 
4
3
  import {
5
4
  findAssistantByName,
@@ -18,12 +17,12 @@ import {
18
17
  stopContainers,
19
18
  } from "../lib/docker";
20
19
  import type { ServiceName } from "../lib/docker";
21
- import {
22
- loadBootstrapSecret,
23
- saveBootstrapSecret,
24
- } from "../lib/guardian-token";
25
- import { restoreBackup } from "../lib/backup-ops.js";
26
20
  import { emitCliError, categorizeUpgradeError } from "../lib/cli-error.js";
21
+ import {
22
+ fetchOrganizationId,
23
+ readPlatformToken,
24
+ rollbackPlatformAssistant,
25
+ } from "../lib/platform-client.js";
27
26
  import {
28
27
  broadcastUpgradeEvent,
29
28
  buildCompleteEvent,
@@ -31,37 +30,56 @@ import {
31
30
  buildStartingEvent,
32
31
  buildUpgradeCommitMessage,
33
32
  captureContainerEnv,
33
+ commitWorkspaceViaGateway,
34
34
  CONTAINER_ENV_EXCLUDE_KEYS,
35
+ performDockerRollback,
35
36
  rollbackMigrations,
36
37
  UPGRADE_PROGRESS,
37
38
  waitForReady,
38
39
  } from "../lib/upgrade-lifecycle.js";
39
- import { commitWorkspaceState } from "../lib/workspace-git.js";
40
+ import { parseVersion } from "../lib/version-compat.js";
40
41
 
41
- function parseArgs(): { name: string | null } {
42
+ function parseArgs(): { name: string | null; version: string | null } {
42
43
  const args = process.argv.slice(3);
43
44
  let name: string | null = null;
45
+ let version: string | null = null;
44
46
 
45
47
  for (let i = 0; i < args.length; i++) {
46
48
  const arg = args[i];
47
49
  if (arg === "--help" || arg === "-h") {
48
- console.log("Usage: vellum rollback [<name>]");
50
+ console.log("Usage: vellum rollback [<name>] [--version <version>]");
49
51
  console.log("");
50
- console.log("Roll back a Docker assistant to the previous version.");
52
+ console.log(
53
+ "Roll back a Docker or managed assistant to a previous version.",
54
+ );
51
55
  console.log("");
52
56
  console.log("Arguments:");
53
57
  console.log(
54
- " <name> Name of the assistant to roll back (default: active or only assistant)",
58
+ " <name> Name of the assistant (default: active or only assistant)",
59
+ );
60
+ console.log("");
61
+ console.log("Options:");
62
+ console.log(
63
+ " --version <version> Target version (optional for managed — omit to roll back to previous)",
55
64
  );
56
65
  console.log("");
57
66
  console.log("Examples:");
58
67
  console.log(
59
- " vellum rollback # Roll back the active assistant",
68
+ " vellum rollback my-assistant # Roll back to previous version (Docker or managed)",
60
69
  );
61
70
  console.log(
62
- " vellum rollback my-assistant # Roll back a specific assistant by name",
71
+ " vellum rollback my-assistant --version v1.2.3 # Roll back to a specific version",
63
72
  );
64
73
  process.exit(0);
74
+ } else if (arg === "--version") {
75
+ const next = args[i + 1];
76
+ if (!next || next.startsWith("-")) {
77
+ console.error("Error: --version requires a value");
78
+ emitCliError("UNKNOWN", "--version requires a value");
79
+ process.exit(1);
80
+ }
81
+ version = next;
82
+ i++;
65
83
  } else if (!arg.startsWith("-")) {
66
84
  name = arg;
67
85
  } else {
@@ -71,7 +89,7 @@ function parseArgs(): { name: string | null } {
71
89
  }
72
90
  }
73
91
 
74
- return { name };
92
+ return { name, version };
75
93
  }
76
94
 
77
95
  function resolveCloud(entry: AssistantEntry): string {
@@ -129,20 +147,153 @@ function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
129
147
  process.exit(1);
130
148
  }
131
149
 
150
+ async function rollbackPlatformViaEndpoint(
151
+ entry: AssistantEntry,
152
+ version?: string,
153
+ ): Promise<void> {
154
+ const currentVersion = entry.serviceGroupVersion;
155
+
156
+ // Step 1 — Version validation (only if version provided)
157
+ if (version && currentVersion) {
158
+ const current = parseVersion(currentVersion);
159
+ const target = parseVersion(version);
160
+ if (current && target) {
161
+ const isOlder =
162
+ target.major < current.major ||
163
+ (target.major === current.major && target.minor < current.minor) ||
164
+ (target.major === current.major &&
165
+ target.minor === current.minor &&
166
+ target.patch < current.patch);
167
+ if (!isOlder) {
168
+ const msg = `Target version ${version} is not older than the current version ${currentVersion}. Use \`vellum upgrade --version ${version}\` to upgrade.`;
169
+ console.error(msg);
170
+ emitCliError("VERSION_DIRECTION", msg);
171
+ process.exit(1);
172
+ }
173
+ }
174
+ }
175
+
176
+ // Step 2 — Authenticate
177
+ const token = readPlatformToken();
178
+ if (!token) {
179
+ const msg =
180
+ "Error: Not logged in. Run `vellum login --token <token>` first.";
181
+ console.error(msg);
182
+ emitCliError("AUTH_FAILED", msg);
183
+ process.exit(1);
184
+ }
185
+
186
+ let orgId: string;
187
+ try {
188
+ orgId = await fetchOrganizationId(token);
189
+ } catch (err) {
190
+ const msg = err instanceof Error ? err.message : String(err);
191
+ if (msg.includes("401") || msg.includes("403")) {
192
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
193
+ } else {
194
+ console.error(`Error: ${msg}`);
195
+ }
196
+ emitCliError("AUTH_FAILED", "Failed to authenticate with platform", msg);
197
+ process.exit(1);
198
+ }
199
+
200
+ // Step 3 — Call rollback endpoint
201
+ if (version) {
202
+ console.log(`Rolling back to ${version}...`);
203
+ } else {
204
+ console.log("Rolling back to previous version...");
205
+ }
206
+
207
+ let result: { detail: string; version: string | null };
208
+ try {
209
+ result = await rollbackPlatformAssistant(token, orgId, version);
210
+ } catch (err) {
211
+ const detail = err instanceof Error ? err.message : String(err);
212
+
213
+ // Map specific server error messages to actionable CLI output
214
+ if (detail.includes("No previous version")) {
215
+ console.error(
216
+ "No previous version available. A successful upgrade must have been performed first.",
217
+ );
218
+ } else if (detail.includes("not older")) {
219
+ console.error(
220
+ `Target version is not older than the current version. Use 'vellum upgrade --version' instead.`,
221
+ );
222
+ } else if (detail.includes("not found")) {
223
+ console.error(
224
+ version
225
+ ? `Version ${version} not found.`
226
+ : `Rollback target not found.`,
227
+ );
228
+ } else if (
229
+ err instanceof TypeError ||
230
+ detail.includes("fetch failed") ||
231
+ detail.includes("ECONNREFUSED")
232
+ ) {
233
+ console.error(
234
+ `Connection error: ${detail}\nIs the platform reachable? Try 'vellum wake' if the assistant is asleep.`,
235
+ );
236
+ } else {
237
+ console.error(`Error: ${detail}`);
238
+ }
239
+
240
+ emitCliError("PLATFORM_API_ERROR", "Platform rollback failed", detail);
241
+ await broadcastUpgradeEvent(
242
+ entry.runtimeUrl,
243
+ entry.assistantId,
244
+ buildCompleteEvent(currentVersion ?? "unknown", false),
245
+ );
246
+ process.exit(1);
247
+ }
248
+
249
+ const rolledBackVersion = result.version ?? version ?? "unknown";
250
+
251
+ // Step 4 — Print success
252
+ console.log(`Rolled back to version ${rolledBackVersion}.`);
253
+ if (!version) {
254
+ console.log("Tip: Run 'vellum rollback' again to undo.");
255
+ }
256
+ }
257
+
132
258
  export async function rollback(): Promise<void> {
133
- const { name } = parseArgs();
259
+ const { name, version } = parseArgs();
134
260
  const entry = resolveTargetAssistant(name);
135
261
  const cloud = resolveCloud(entry);
136
262
 
137
- // Only Docker assistants support rollback
263
+ // ---------- Managed (Vellum platform) rollback ----------
264
+ if (cloud === "vellum") {
265
+ await rollbackPlatformViaEndpoint(entry, version ?? undefined);
266
+ return;
267
+ }
268
+
269
+ // ---------- Unsupported topologies ----------
138
270
  if (cloud !== "docker") {
139
- const msg =
140
- "Rollback is only supported for Docker assistants. For managed assistants, use the version picker to upgrade to the previous version.";
271
+ const msg = "Rollback is only supported for Docker and managed assistants.";
141
272
  console.error(msg);
142
273
  emitCliError("UNSUPPORTED_TOPOLOGY", msg);
143
274
  process.exit(1);
144
275
  }
145
276
 
277
+ // ---------- Docker: Targeted version rollback (--version specified) ----------
278
+ if (version) {
279
+ try {
280
+ await performDockerRollback(entry, { targetVersion: version });
281
+ } catch (err) {
282
+ const detail = err instanceof Error ? err.message : String(err);
283
+ console.error(`\n❌ Rollback failed: ${detail}`);
284
+ await broadcastUpgradeEvent(
285
+ entry.runtimeUrl,
286
+ entry.assistantId,
287
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
288
+ );
289
+ emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
290
+ process.exit(1);
291
+ }
292
+ return;
293
+ }
294
+
295
+ // ---------- Docker: Saved-state rollback (no --version) ----------
296
+
146
297
  // Verify rollback state exists
147
298
  if (!entry.previousServiceGroupVersion || !entry.previousContainerInfo) {
148
299
  const msg =
@@ -173,30 +324,19 @@ export async function rollback(): Promise<void> {
173
324
  const res = dockerResourceNames(instanceName);
174
325
 
175
326
  try {
176
- const workspaceDir = entry.resources
177
- ? join(entry.resources.instanceDir, ".vellum", "workspace")
178
- : undefined;
179
-
180
327
  // Record rollback start in workspace git history
181
- if (workspaceDir) {
182
- try {
183
- await commitWorkspaceState(
184
- workspaceDir,
185
- buildUpgradeCommitMessage({
186
- action: "rollback",
187
- phase: "starting",
188
- from: entry.serviceGroupVersion ?? "unknown",
189
- to: entry.previousServiceGroupVersion ?? "unknown",
190
- topology: "docker",
191
- assistantId: entry.assistantId,
192
- }),
193
- );
194
- } catch (err) {
195
- console.warn(
196
- `⚠️ Failed to create pre-rollback workspace commit: ${err instanceof Error ? err.message : String(err)}`,
197
- );
198
- }
199
- }
328
+ await commitWorkspaceViaGateway(
329
+ entry.runtimeUrl,
330
+ entry.assistantId,
331
+ buildUpgradeCommitMessage({
332
+ action: "rollback",
333
+ phase: "starting",
334
+ from: entry.serviceGroupVersion ?? "unknown",
335
+ to: entry.previousServiceGroupVersion ?? "unknown",
336
+ topology: "docker",
337
+ assistantId: entry.assistantId,
338
+ }),
339
+ );
200
340
 
201
341
  console.log(
202
342
  `🔄 Rolling back Docker assistant '${instanceName}' to ${entry.previousServiceGroupVersion}...\n`,
@@ -213,13 +353,6 @@ export async function rollback(): Promise<void> {
213
353
  const cesServiceToken =
214
354
  capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
215
355
 
216
- // Retrieve or generate a bootstrap secret for the gateway.
217
- const loadedSecret = loadBootstrapSecret(instanceName);
218
- const bootstrapSecret = loadedSecret || randomBytes(32).toString("hex");
219
- if (!loadedSecret) {
220
- saveBootstrapSecret(instanceName, bootstrapSecret);
221
- }
222
-
223
356
  // Extract or generate the shared JWT signing key.
224
357
  const signingKey =
225
358
  capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
@@ -301,7 +434,6 @@ export async function rollback(): Promise<void> {
301
434
  await startContainers(
302
435
  {
303
436
  signingKey,
304
- bootstrapSecret,
305
437
  cesServiceToken,
306
438
  extraAssistantEnv,
307
439
  gatewayPort,
@@ -317,40 +449,6 @@ export async function rollback(): Promise<void> {
317
449
  const ready = await waitForReady(entry.runtimeUrl);
318
450
 
319
451
  if (ready) {
320
- // Restore data from the backup created for the specific upgrade being
321
- // rolled back. We use the persisted preUpgradeBackupPath rather than
322
- // scanning for the latest backup on disk — if the most recent upgrade's
323
- // backup failed, a global scan would find a stale backup from a prior
324
- // cycle and overwrite newer user data.
325
- const backupPath = entry.preUpgradeBackupPath as string | undefined;
326
- if (backupPath) {
327
- // Progress: restoring data (gateway is back up at this point)
328
- await broadcastUpgradeEvent(
329
- entry.runtimeUrl,
330
- entry.assistantId,
331
- buildProgressEvent(UPGRADE_PROGRESS.RESTORING),
332
- );
333
-
334
- console.log(`📦 Restoring data from pre-upgrade backup...`);
335
- console.log(` Source: ${backupPath}`);
336
- const restored = await restoreBackup(
337
- entry.runtimeUrl,
338
- entry.assistantId,
339
- backupPath,
340
- );
341
- if (restored) {
342
- console.log(" ✅ Data restored successfully\n");
343
- } else {
344
- console.warn(
345
- " ⚠️ Data restore failed (rollback continues without data restoration)\n",
346
- );
347
- }
348
- } else {
349
- console.log(
350
- "ℹ️ No pre-upgrade backup was created for this upgrade, skipping data restoration\n",
351
- );
352
- }
353
-
354
452
  // Capture new digests from the rolled-back containers
355
453
  const newDigests = await captureImageRefs(res);
356
454
 
@@ -384,30 +482,26 @@ export async function rollback(): Promise<void> {
384
482
  );
385
483
 
386
484
  // Record successful rollback in workspace git history
387
- if (workspaceDir) {
388
- try {
389
- await commitWorkspaceState(
390
- workspaceDir,
391
- buildUpgradeCommitMessage({
392
- action: "rollback",
393
- phase: "complete",
394
- from: entry.serviceGroupVersion ?? "unknown",
395
- to: entry.previousServiceGroupVersion ?? "unknown",
396
- topology: "docker",
397
- assistantId: entry.assistantId,
398
- result: "success",
399
- }),
400
- );
401
- } catch (err) {
402
- console.warn(
403
- `⚠️ Failed to create post-rollback workspace commit: ${err instanceof Error ? err.message : String(err)}`,
404
- );
405
- }
406
- }
485
+ await commitWorkspaceViaGateway(
486
+ entry.runtimeUrl,
487
+ entry.assistantId,
488
+ buildUpgradeCommitMessage({
489
+ action: "rollback",
490
+ phase: "complete",
491
+ from: entry.serviceGroupVersion ?? "unknown",
492
+ to: entry.previousServiceGroupVersion ?? "unknown",
493
+ topology: "docker",
494
+ assistantId: entry.assistantId,
495
+ result: "success",
496
+ }),
497
+ );
407
498
 
408
499
  console.log(
409
500
  `\n✅ Docker assistant '${instanceName}' rolled back to ${entry.previousServiceGroupVersion}.`,
410
501
  );
502
+ console.log(
503
+ "\nTip: To also restore data from before the upgrade, use `vellum restore --from <backup-path>`.",
504
+ );
411
505
  } else {
412
506
  console.error(
413
507
  `\n❌ Containers failed to become ready within the timeout.`,