@vellumai/cli 0.5.7 → 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,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,190 @@ 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 — Workspace commit (starting)
177
+ await commitWorkspaceViaGateway(
178
+ entry.runtimeUrl,
179
+ entry.assistantId,
180
+ buildUpgradeCommitMessage({
181
+ action: "rollback",
182
+ phase: "starting",
183
+ from: currentVersion ?? "unknown",
184
+ to: version ?? "previous",
185
+ topology: "managed",
186
+ assistantId: entry.assistantId,
187
+ }),
188
+ );
189
+
190
+ // Step 3 — Authenticate
191
+ const token = readPlatformToken();
192
+ if (!token) {
193
+ const msg =
194
+ "Error: Not logged in. Run `vellum login --token <token>` first.";
195
+ console.error(msg);
196
+ emitCliError("AUTH_FAILED", msg);
197
+ process.exit(1);
198
+ }
199
+
200
+ let orgId: string;
201
+ try {
202
+ orgId = await fetchOrganizationId(token);
203
+ } catch (err) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ if (msg.includes("401") || msg.includes("403")) {
206
+ console.error("Authentication failed. Run 'vellum login' to refresh.");
207
+ } else {
208
+ console.error(`Error: ${msg}`);
209
+ }
210
+ emitCliError("AUTH_FAILED", "Failed to authenticate with platform", msg);
211
+ process.exit(1);
212
+ }
213
+
214
+ // Step 4 — Broadcast starting event
215
+ console.log("📢 Notifying connected clients...");
216
+ await broadcastUpgradeEvent(
217
+ entry.runtimeUrl,
218
+ entry.assistantId,
219
+ buildStartingEvent(version ?? "previous", 90),
220
+ );
221
+
222
+ // Step 5 — Call rollback endpoint
223
+ if (version) {
224
+ console.log(`Rolling back to ${version}...`);
225
+ } else {
226
+ console.log("Rolling back to previous version...");
227
+ }
228
+
229
+ let result: { detail: string; version: string | null };
230
+ try {
231
+ result = await rollbackPlatformAssistant(token, orgId, version);
232
+ } catch (err) {
233
+ const detail = err instanceof Error ? err.message : String(err);
234
+
235
+ // Map specific server error messages to actionable CLI output
236
+ if (detail.includes("No previous version")) {
237
+ console.error(
238
+ "No previous version available. A successful upgrade must have been performed first.",
239
+ );
240
+ } else if (detail.includes("not older")) {
241
+ console.error(
242
+ `Target version is not older than the current version. Use 'vellum upgrade --version' instead.`,
243
+ );
244
+ } else if (detail.includes("not found")) {
245
+ console.error(
246
+ version
247
+ ? `Version ${version} not found.`
248
+ : `Rollback target not found.`,
249
+ );
250
+ } else if (
251
+ err instanceof TypeError ||
252
+ detail.includes("fetch failed") ||
253
+ detail.includes("ECONNREFUSED")
254
+ ) {
255
+ console.error(
256
+ `Connection error: ${detail}\nIs the platform reachable? Try 'vellum wake' if the assistant is asleep.`,
257
+ );
258
+ } else {
259
+ console.error(`Error: ${detail}`);
260
+ }
261
+
262
+ emitCliError("PLATFORM_API_ERROR", "Platform rollback failed", detail);
263
+ await broadcastUpgradeEvent(
264
+ entry.runtimeUrl,
265
+ entry.assistantId,
266
+ buildCompleteEvent(currentVersion ?? "unknown", false),
267
+ );
268
+ process.exit(1);
269
+ }
270
+
271
+ const rolledBackVersion = result.version ?? version ?? "unknown";
272
+
273
+ // Step 6 — Workspace commit (complete)
274
+ await commitWorkspaceViaGateway(
275
+ entry.runtimeUrl,
276
+ entry.assistantId,
277
+ buildUpgradeCommitMessage({
278
+ action: "rollback",
279
+ phase: "complete",
280
+ from: currentVersion ?? "unknown",
281
+ to: rolledBackVersion,
282
+ topology: "managed",
283
+ assistantId: entry.assistantId,
284
+ result: "success",
285
+ }),
286
+ );
287
+
288
+ // Step 7 — Print success
289
+ console.log(`Rolled back to version ${rolledBackVersion}.`);
290
+ if (!version) {
291
+ console.log("Tip: Run 'vellum rollback' again to undo.");
292
+ }
293
+ }
294
+
132
295
  export async function rollback(): Promise<void> {
133
- const { name } = parseArgs();
296
+ const { name, version } = parseArgs();
134
297
  const entry = resolveTargetAssistant(name);
135
298
  const cloud = resolveCloud(entry);
136
299
 
137
- // Only Docker assistants support rollback
300
+ // ---------- Managed (Vellum platform) rollback ----------
301
+ if (cloud === "vellum") {
302
+ await rollbackPlatformViaEndpoint(entry, version ?? undefined);
303
+ return;
304
+ }
305
+
306
+ // ---------- Unsupported topologies ----------
138
307
  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.";
308
+ const msg = "Rollback is only supported for Docker and managed assistants.";
141
309
  console.error(msg);
142
310
  emitCliError("UNSUPPORTED_TOPOLOGY", msg);
143
311
  process.exit(1);
144
312
  }
145
313
 
314
+ // ---------- Docker: Targeted version rollback (--version specified) ----------
315
+ if (version) {
316
+ try {
317
+ await performDockerRollback(entry, { targetVersion: version });
318
+ } catch (err) {
319
+ const detail = err instanceof Error ? err.message : String(err);
320
+ console.error(`\n❌ Rollback failed: ${detail}`);
321
+ await broadcastUpgradeEvent(
322
+ entry.runtimeUrl,
323
+ entry.assistantId,
324
+ buildCompleteEvent(entry.serviceGroupVersion ?? "unknown", false),
325
+ );
326
+ emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
327
+ process.exit(1);
328
+ }
329
+ return;
330
+ }
331
+
332
+ // ---------- Docker: Saved-state rollback (no --version) ----------
333
+
146
334
  // Verify rollback state exists
147
335
  if (!entry.previousServiceGroupVersion || !entry.previousContainerInfo) {
148
336
  const msg =
@@ -173,30 +361,19 @@ export async function rollback(): Promise<void> {
173
361
  const res = dockerResourceNames(instanceName);
174
362
 
175
363
  try {
176
- const workspaceDir = entry.resources
177
- ? join(entry.resources.instanceDir, ".vellum", "workspace")
178
- : undefined;
179
-
180
364
  // 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
- }
365
+ await commitWorkspaceViaGateway(
366
+ entry.runtimeUrl,
367
+ entry.assistantId,
368
+ buildUpgradeCommitMessage({
369
+ action: "rollback",
370
+ phase: "starting",
371
+ from: entry.serviceGroupVersion ?? "unknown",
372
+ to: entry.previousServiceGroupVersion ?? "unknown",
373
+ topology: "docker",
374
+ assistantId: entry.assistantId,
375
+ }),
376
+ );
200
377
 
201
378
  console.log(
202
379
  `🔄 Rolling back Docker assistant '${instanceName}' to ${entry.previousServiceGroupVersion}...\n`,
@@ -213,13 +390,6 @@ export async function rollback(): Promise<void> {
213
390
  const cesServiceToken =
214
391
  capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
215
392
 
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
393
  // Extract or generate the shared JWT signing key.
224
394
  const signingKey =
225
395
  capturedEnv["ACTOR_TOKEN_SIGNING_KEY"] || randomBytes(32).toString("hex");
@@ -301,7 +471,6 @@ export async function rollback(): Promise<void> {
301
471
  await startContainers(
302
472
  {
303
473
  signingKey,
304
- bootstrapSecret,
305
474
  cesServiceToken,
306
475
  extraAssistantEnv,
307
476
  gatewayPort,
@@ -317,40 +486,6 @@ export async function rollback(): Promise<void> {
317
486
  const ready = await waitForReady(entry.runtimeUrl);
318
487
 
319
488
  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
489
  // Capture new digests from the rolled-back containers
355
490
  const newDigests = await captureImageRefs(res);
356
491
 
@@ -384,30 +519,26 @@ export async function rollback(): Promise<void> {
384
519
  );
385
520
 
386
521
  // 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
- }
522
+ await commitWorkspaceViaGateway(
523
+ entry.runtimeUrl,
524
+ entry.assistantId,
525
+ buildUpgradeCommitMessage({
526
+ action: "rollback",
527
+ phase: "complete",
528
+ from: entry.serviceGroupVersion ?? "unknown",
529
+ to: entry.previousServiceGroupVersion ?? "unknown",
530
+ topology: "docker",
531
+ assistantId: entry.assistantId,
532
+ result: "success",
533
+ }),
534
+ );
407
535
 
408
536
  console.log(
409
537
  `\n✅ Docker assistant '${instanceName}' rolled back to ${entry.previousServiceGroupVersion}.`,
410
538
  );
539
+ console.log(
540
+ "\nTip: To also restore data from before the upgrade, use `vellum restore --from <backup-path>`.",
541
+ );
411
542
  } else {
412
543
  console.error(
413
544
  `\n❌ Containers failed to become ready within the timeout.`,