@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 +1 -1
- package/src/commands/backup.ts +124 -0
- package/src/commands/hatch.ts +24 -5
- package/src/commands/restore.ts +359 -16
- package/src/commands/rollback.ts +197 -103
- package/src/commands/upgrade.ts +204 -220
- package/src/index.ts +4 -4
- package/src/lib/aws.ts +14 -9
- package/src/lib/cli-error.ts +2 -0
- package/src/lib/docker.ts +54 -13
- package/src/lib/gcp.ts +15 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +1 -0
- package/src/lib/platform-client.ts +206 -18
- package/src/lib/upgrade-lifecycle.ts +612 -5
- package/src/lib/workspace-git.ts +0 -39
package/package.json
CHANGED
package/src/commands/backup.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/hatch.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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";
|
package/src/commands/restore.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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>
|
|
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
|
-
" --
|
|
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
|
|
41
|
+
console.log(" vellum restore my-assistant --from backup.vbundle");
|
|
25
42
|
console.log(
|
|
26
|
-
" vellum restore my-assistant --from
|
|
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 {
|
|
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
|
-
//
|
|
164
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
259
|
-
|
|
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 {
|