@vellumai/cli 0.5.12 → 0.5.14
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/AGENTS.md +6 -0
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +829 -0
- package/src/commands/backup.ts +14 -5
- package/src/commands/hatch.ts +56 -5
- package/src/commands/ps.ts +1 -1
- package/src/commands/recover.ts +13 -4
- package/src/commands/restore.ts +9 -2
- package/src/commands/retire.ts +8 -4
- package/src/commands/rollback.ts +7 -11
- package/src/commands/teleport.ts +746 -0
- package/src/commands/upgrade.ts +2 -13
- package/src/commands/wake.ts +17 -4
- package/src/index.ts +3 -0
- package/src/lib/assistant-config.ts +6 -7
- package/src/lib/constants.ts +11 -0
- package/src/lib/docker.ts +2 -146
- package/src/lib/health-check.ts +1 -1
- package/src/lib/local.ts +32 -2
- package/src/lib/platform-client.ts +89 -16
- package/src/lib/upgrade-lifecycle.ts +0 -11
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { findAssistantByName } from "../lib/assistant-config.js";
|
|
2
|
+
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
3
|
+
import {
|
|
4
|
+
loadGuardianToken,
|
|
5
|
+
leaseGuardianToken,
|
|
6
|
+
} from "../lib/guardian-token.js";
|
|
7
|
+
import {
|
|
8
|
+
readPlatformToken,
|
|
9
|
+
fetchOrganizationId,
|
|
10
|
+
platformInitiateExport,
|
|
11
|
+
platformPollExportStatus,
|
|
12
|
+
platformDownloadExport,
|
|
13
|
+
platformImportPreflight,
|
|
14
|
+
platformImportBundle,
|
|
15
|
+
} from "../lib/platform-client.js";
|
|
16
|
+
|
|
17
|
+
function printHelp(): void {
|
|
18
|
+
console.log(
|
|
19
|
+
"Usage: vellum teleport --from <assistant> --to <assistant> [options]",
|
|
20
|
+
);
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(
|
|
23
|
+
"Transfer assistant data between local and platform environments.",
|
|
24
|
+
);
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log("Options:");
|
|
27
|
+
console.log(" --from <name> Source assistant to export data from");
|
|
28
|
+
console.log(" --to <name> Target assistant to import data into");
|
|
29
|
+
console.log(
|
|
30
|
+
" --dry-run Preview the transfer without applying changes",
|
|
31
|
+
);
|
|
32
|
+
console.log(" --help, -h Show this help");
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("Examples:");
|
|
35
|
+
console.log(" vellum teleport --from my-local --to my-cloud");
|
|
36
|
+
console.log(" vellum teleport --from my-cloud --to my-local --dry-run");
|
|
37
|
+
console.log(" vellum teleport --from staging --to production --dry-run");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv: string[]): {
|
|
41
|
+
from: string | undefined;
|
|
42
|
+
to: string | undefined;
|
|
43
|
+
dryRun: boolean;
|
|
44
|
+
help: boolean;
|
|
45
|
+
} {
|
|
46
|
+
let from: string | undefined;
|
|
47
|
+
let to: string | undefined;
|
|
48
|
+
let dryRun = false;
|
|
49
|
+
let help = false;
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg === "--from" && i + 1 < argv.length) {
|
|
54
|
+
if (argv[i + 1].startsWith("--")) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
from = argv[++i];
|
|
58
|
+
} else if (arg === "--to" && i + 1 < argv.length) {
|
|
59
|
+
if (argv[i + 1].startsWith("--")) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
to = argv[++i];
|
|
63
|
+
} else if (arg === "--dry-run") {
|
|
64
|
+
dryRun = true;
|
|
65
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
66
|
+
help = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { from, to, dryRun, help };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveCloud(entry: AssistantEntry): string {
|
|
74
|
+
return (
|
|
75
|
+
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local")
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Auth helper — same pattern as restore.ts
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
async function getAccessToken(
|
|
84
|
+
runtimeUrl: string,
|
|
85
|
+
assistantId: string,
|
|
86
|
+
displayName: string,
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const tokenData = loadGuardianToken(assistantId);
|
|
89
|
+
|
|
90
|
+
if (tokenData && new Date(tokenData.accessTokenExpiresAt) > new Date()) {
|
|
91
|
+
return tokenData.accessToken;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const freshToken = await leaseGuardianToken(runtimeUrl, assistantId);
|
|
96
|
+
return freshToken.accessToken;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
100
|
+
console.error(
|
|
101
|
+
`Error: Could not connect to assistant '${displayName}'. Is it running?`,
|
|
102
|
+
);
|
|
103
|
+
console.error(`Try: vellum wake ${displayName}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Export from source assistant
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
async function exportFromAssistant(
|
|
115
|
+
entry: AssistantEntry,
|
|
116
|
+
cloud: string,
|
|
117
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
118
|
+
if (cloud === "vellum") {
|
|
119
|
+
// Platform source — use Django async export
|
|
120
|
+
const token = readPlatformToken();
|
|
121
|
+
if (!token) {
|
|
122
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let orgId: string;
|
|
127
|
+
try {
|
|
128
|
+
orgId = await fetchOrganizationId(token, entry.runtimeUrl);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
132
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initiate export job
|
|
139
|
+
let jobId: string;
|
|
140
|
+
try {
|
|
141
|
+
const result = await platformInitiateExport(
|
|
142
|
+
token,
|
|
143
|
+
orgId,
|
|
144
|
+
"teleport export",
|
|
145
|
+
entry.runtimeUrl,
|
|
146
|
+
);
|
|
147
|
+
jobId = result.jobId;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
150
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
151
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`Export started (job ${jobId})...`);
|
|
158
|
+
|
|
159
|
+
// Poll for completion
|
|
160
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
161
|
+
const TIMEOUT_MS = 5 * 60 * 1_000;
|
|
162
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
163
|
+
let downloadUrl: string | undefined;
|
|
164
|
+
|
|
165
|
+
while (Date.now() < deadline) {
|
|
166
|
+
let status: { status: string; downloadUrl?: string; error?: string };
|
|
167
|
+
try {
|
|
168
|
+
status = await platformPollExportStatus(
|
|
169
|
+
jobId,
|
|
170
|
+
token,
|
|
171
|
+
orgId,
|
|
172
|
+
entry.runtimeUrl,
|
|
173
|
+
);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
if (msg.includes("not found")) {
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
console.warn(`Polling failed, retrying... (${msg})`);
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (status.status === "complete") {
|
|
185
|
+
downloadUrl = status.downloadUrl;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (status.status === "failed") {
|
|
190
|
+
console.error(`Export failed: ${status.error ?? "unknown error"}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!downloadUrl) {
|
|
198
|
+
console.error("Export timed out after 5 minutes.");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Download the bundle
|
|
203
|
+
const response = await platformDownloadExport(downloadUrl);
|
|
204
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
205
|
+
return new Uint8Array(arrayBuffer);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (cloud === "local") {
|
|
209
|
+
// Local source — direct export endpoint
|
|
210
|
+
let accessToken = await getAccessToken(
|
|
211
|
+
entry.runtimeUrl,
|
|
212
|
+
entry.assistantId,
|
|
213
|
+
entry.assistantId,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
let response: Response;
|
|
217
|
+
try {
|
|
218
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: {
|
|
221
|
+
Authorization: `Bearer ${accessToken}`,
|
|
222
|
+
"Content-Type": "application/json",
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
225
|
+
signal: AbortSignal.timeout(120_000),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Retry once with a fresh token on 401
|
|
229
|
+
if (response.status === 401) {
|
|
230
|
+
let refreshedToken: string | null = null;
|
|
231
|
+
try {
|
|
232
|
+
const freshToken = await leaseGuardianToken(
|
|
233
|
+
entry.runtimeUrl,
|
|
234
|
+
entry.assistantId,
|
|
235
|
+
);
|
|
236
|
+
refreshedToken = freshToken.accessToken;
|
|
237
|
+
} catch {
|
|
238
|
+
// If token refresh fails, fall through to the error handler below
|
|
239
|
+
}
|
|
240
|
+
if (refreshedToken) {
|
|
241
|
+
accessToken = refreshedToken;
|
|
242
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: {
|
|
245
|
+
Authorization: `Bearer ${accessToken}`,
|
|
246
|
+
"Content-Type": "application/json",
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
249
|
+
signal: AbortSignal.timeout(120_000),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
255
|
+
console.error("Error: Export request timed out after 2 minutes.");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
259
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
260
|
+
console.error(
|
|
261
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
262
|
+
);
|
|
263
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (response.status === 401 || response.status === 403) {
|
|
270
|
+
console.error("Authentication failed.");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (response.status === 404) {
|
|
275
|
+
console.error("Assistant not found or not running.");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
response.status === 502 ||
|
|
281
|
+
response.status === 503 ||
|
|
282
|
+
response.status === 504
|
|
283
|
+
) {
|
|
284
|
+
console.error(
|
|
285
|
+
`Assistant is unreachable. Try 'vellum wake ${entry.assistantId}'.`,
|
|
286
|
+
);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
const body = await response.text();
|
|
292
|
+
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
297
|
+
return new Uint8Array(arrayBuffer);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.error(
|
|
301
|
+
"Teleport only supports local and platform assistants as source.",
|
|
302
|
+
);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Import into target assistant
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
interface PreflightFileEntry {
|
|
311
|
+
path: string;
|
|
312
|
+
action: string;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface StructuredError {
|
|
316
|
+
code: string;
|
|
317
|
+
message: string;
|
|
318
|
+
path?: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface PreflightResponse {
|
|
322
|
+
can_import: boolean;
|
|
323
|
+
validation?: {
|
|
324
|
+
is_valid: false;
|
|
325
|
+
errors: StructuredError[];
|
|
326
|
+
};
|
|
327
|
+
files?: PreflightFileEntry[];
|
|
328
|
+
summary?: {
|
|
329
|
+
files_to_create: number;
|
|
330
|
+
files_to_overwrite: number;
|
|
331
|
+
files_unchanged: number;
|
|
332
|
+
total_files: number;
|
|
333
|
+
};
|
|
334
|
+
conflicts?: StructuredError[];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface ImportResponse {
|
|
338
|
+
success: boolean;
|
|
339
|
+
reason?: string;
|
|
340
|
+
errors?: StructuredError[];
|
|
341
|
+
message?: string;
|
|
342
|
+
warnings?: string[];
|
|
343
|
+
summary?: {
|
|
344
|
+
total_files: number;
|
|
345
|
+
files_created: number;
|
|
346
|
+
files_overwritten: number;
|
|
347
|
+
files_skipped: number;
|
|
348
|
+
backups_created: number;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function importToAssistant(
|
|
353
|
+
entry: AssistantEntry,
|
|
354
|
+
cloud: string,
|
|
355
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
356
|
+
dryRun: boolean,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
if (cloud === "vellum") {
|
|
359
|
+
// Platform target
|
|
360
|
+
const token = readPlatformToken();
|
|
361
|
+
if (!token) {
|
|
362
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let orgId: string;
|
|
367
|
+
try {
|
|
368
|
+
orgId = await fetchOrganizationId(token, entry.runtimeUrl);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
371
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
372
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
throw err;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (dryRun) {
|
|
379
|
+
console.log("Running preflight analysis...\n");
|
|
380
|
+
|
|
381
|
+
let preflightResult: {
|
|
382
|
+
statusCode: number;
|
|
383
|
+
body: Record<string, unknown>;
|
|
384
|
+
};
|
|
385
|
+
try {
|
|
386
|
+
preflightResult = await platformImportPreflight(
|
|
387
|
+
bundleData,
|
|
388
|
+
token,
|
|
389
|
+
orgId,
|
|
390
|
+
entry.runtimeUrl,
|
|
391
|
+
);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
394
|
+
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (
|
|
401
|
+
preflightResult.statusCode === 401 ||
|
|
402
|
+
preflightResult.statusCode === 403
|
|
403
|
+
) {
|
|
404
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (preflightResult.statusCode === 404) {
|
|
409
|
+
console.error("Assistant not found or not running.");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
preflightResult.statusCode === 502 ||
|
|
415
|
+
preflightResult.statusCode === 503 ||
|
|
416
|
+
preflightResult.statusCode === 504
|
|
417
|
+
) {
|
|
418
|
+
console.error(
|
|
419
|
+
`Assistant is unreachable. Try 'vellum wake ${entry.assistantId}'.`,
|
|
420
|
+
);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (preflightResult.statusCode !== 200) {
|
|
425
|
+
console.error(
|
|
426
|
+
`Error: Preflight check failed (${preflightResult.statusCode}): ${JSON.stringify(preflightResult.body)}`,
|
|
427
|
+
);
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const result = preflightResult.body as unknown as PreflightResponse;
|
|
432
|
+
printPreflightSummary(result);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Actual import
|
|
437
|
+
console.log("Importing data...");
|
|
438
|
+
|
|
439
|
+
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
440
|
+
try {
|
|
441
|
+
importResult = await platformImportBundle(
|
|
442
|
+
bundleData,
|
|
443
|
+
token,
|
|
444
|
+
orgId,
|
|
445
|
+
entry.runtimeUrl,
|
|
446
|
+
);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
449
|
+
console.error("Error: Import request timed out after 2 minutes.");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
throw err;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
handleImportStatusErrors(importResult.statusCode, entry.assistantId);
|
|
456
|
+
|
|
457
|
+
const result = importResult.body as unknown as ImportResponse;
|
|
458
|
+
printImportSummary(result);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (cloud === "local") {
|
|
463
|
+
// Local target
|
|
464
|
+
const accessToken = await getAccessToken(
|
|
465
|
+
entry.runtimeUrl,
|
|
466
|
+
entry.assistantId,
|
|
467
|
+
entry.assistantId,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (dryRun) {
|
|
471
|
+
console.log("Running preflight analysis...\n");
|
|
472
|
+
|
|
473
|
+
let response: Response;
|
|
474
|
+
try {
|
|
475
|
+
response = await fetch(
|
|
476
|
+
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
477
|
+
{
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: {
|
|
480
|
+
Authorization: `Bearer ${accessToken}`,
|
|
481
|
+
"Content-Type": "application/octet-stream",
|
|
482
|
+
},
|
|
483
|
+
body: new Blob([bundleData]),
|
|
484
|
+
signal: AbortSignal.timeout(120_000),
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
489
|
+
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
493
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
494
|
+
console.error(
|
|
495
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
496
|
+
);
|
|
497
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
throw err;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
504
|
+
|
|
505
|
+
const result = (await response.json()) as PreflightResponse;
|
|
506
|
+
printPreflightSummary(result);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Actual import
|
|
511
|
+
console.log("Importing data...");
|
|
512
|
+
|
|
513
|
+
let response: Response;
|
|
514
|
+
try {
|
|
515
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: {
|
|
518
|
+
Authorization: `Bearer ${accessToken}`,
|
|
519
|
+
"Content-Type": "application/octet-stream",
|
|
520
|
+
},
|
|
521
|
+
body: new Blob([bundleData]),
|
|
522
|
+
signal: AbortSignal.timeout(120_000),
|
|
523
|
+
});
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
526
|
+
console.error("Error: Import request timed out after 2 minutes.");
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
531
|
+
console.error(
|
|
532
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
533
|
+
);
|
|
534
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
throw err;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
541
|
+
|
|
542
|
+
const result = (await response.json()) as ImportResponse;
|
|
543
|
+
printImportSummary(result);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
console.error(
|
|
548
|
+
"Teleport only supports local and platform assistants as target.",
|
|
549
|
+
);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// Error handling helpers
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
|
|
557
|
+
function handleLocalResponseErrors(
|
|
558
|
+
response: Response,
|
|
559
|
+
assistantName: string,
|
|
560
|
+
): void {
|
|
561
|
+
if (response.status === 401 || response.status === 403) {
|
|
562
|
+
console.error("Authentication failed.");
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (response.status === 404) {
|
|
567
|
+
console.error("Assistant not found or not running.");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (
|
|
572
|
+
response.status === 502 ||
|
|
573
|
+
response.status === 503 ||
|
|
574
|
+
response.status === 504
|
|
575
|
+
) {
|
|
576
|
+
console.error(
|
|
577
|
+
`Assistant is unreachable. Try 'vellum wake ${assistantName}'.`,
|
|
578
|
+
);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!response.ok) {
|
|
583
|
+
console.error(`Error: Request failed (${response.status})`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function handleImportStatusErrors(
|
|
589
|
+
statusCode: number,
|
|
590
|
+
assistantName: string,
|
|
591
|
+
): void {
|
|
592
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
593
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (statusCode === 404) {
|
|
598
|
+
console.error("Assistant not found or not running.");
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (statusCode === 502 || statusCode === 503 || statusCode === 504) {
|
|
603
|
+
console.error(
|
|
604
|
+
`Assistant is unreachable. Try 'vellum wake ${assistantName}'.`,
|
|
605
|
+
);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
610
|
+
console.error(`Error: Import failed (${statusCode})`);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ---------------------------------------------------------------------------
|
|
616
|
+
// Summary printing — matches restore.ts format
|
|
617
|
+
// ---------------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
function printPreflightSummary(result: PreflightResponse): void {
|
|
620
|
+
if (!result.can_import) {
|
|
621
|
+
if (result.validation?.errors?.length) {
|
|
622
|
+
console.error("Import blocked by validation errors:");
|
|
623
|
+
for (const err of result.validation.errors) {
|
|
624
|
+
console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (result.conflicts?.length) {
|
|
628
|
+
console.error("Import blocked by conflicts:");
|
|
629
|
+
for (const conflict of result.conflicts) {
|
|
630
|
+
console.error(
|
|
631
|
+
` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`,
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const summary = result.summary ?? {
|
|
639
|
+
files_to_create: 0,
|
|
640
|
+
files_to_overwrite: 0,
|
|
641
|
+
files_unchanged: 0,
|
|
642
|
+
total_files: 0,
|
|
643
|
+
};
|
|
644
|
+
console.log("Preflight analysis:");
|
|
645
|
+
console.log(` Files to create: ${summary.files_to_create}`);
|
|
646
|
+
console.log(` Files to overwrite: ${summary.files_to_overwrite}`);
|
|
647
|
+
console.log(` Files unchanged: ${summary.files_unchanged}`);
|
|
648
|
+
console.log(` Total: ${summary.total_files}`);
|
|
649
|
+
console.log("");
|
|
650
|
+
|
|
651
|
+
const conflicts = result.conflicts ?? [];
|
|
652
|
+
console.log(
|
|
653
|
+
`Conflicts: ${conflicts.length > 0 ? conflicts.map((c) => c.message).join(", ") : "none"}`,
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (result.files && result.files.length > 0) {
|
|
657
|
+
console.log("");
|
|
658
|
+
console.log("Files:");
|
|
659
|
+
for (const file of result.files) {
|
|
660
|
+
console.log(` [${file.action}] ${file.path}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function printImportSummary(result: ImportResponse): void {
|
|
666
|
+
if (!result.success) {
|
|
667
|
+
console.error(
|
|
668
|
+
`Error: Import failed — ${result.message ?? result.reason ?? "unknown reason"}`,
|
|
669
|
+
);
|
|
670
|
+
for (const err of result.errors ?? []) {
|
|
671
|
+
console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
|
|
672
|
+
}
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const summary = result.summary ?? {
|
|
677
|
+
total_files: 0,
|
|
678
|
+
files_created: 0,
|
|
679
|
+
files_overwritten: 0,
|
|
680
|
+
files_skipped: 0,
|
|
681
|
+
backups_created: 0,
|
|
682
|
+
};
|
|
683
|
+
console.log(` Files created: ${summary.files_created}`);
|
|
684
|
+
console.log(` Files overwritten: ${summary.files_overwritten}`);
|
|
685
|
+
console.log(` Files skipped: ${summary.files_skipped}`);
|
|
686
|
+
console.log(` Backups created: ${summary.backups_created}`);
|
|
687
|
+
|
|
688
|
+
const warnings = result.warnings ?? [];
|
|
689
|
+
if (warnings.length > 0) {
|
|
690
|
+
console.log("");
|
|
691
|
+
console.log("Warnings:");
|
|
692
|
+
for (const warning of warnings) {
|
|
693
|
+
console.log(` ${warning}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---------------------------------------------------------------------------
|
|
699
|
+
// Main entry point
|
|
700
|
+
// ---------------------------------------------------------------------------
|
|
701
|
+
|
|
702
|
+
export async function teleport(): Promise<void> {
|
|
703
|
+
const args = process.argv.slice(3);
|
|
704
|
+
const { from, to, dryRun, help } = parseArgs(args);
|
|
705
|
+
|
|
706
|
+
if (help) {
|
|
707
|
+
printHelp();
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (!from || !to) {
|
|
712
|
+
printHelp();
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Look up both assistants
|
|
717
|
+
const fromEntry = findAssistantByName(from);
|
|
718
|
+
if (!fromEntry) {
|
|
719
|
+
console.error(
|
|
720
|
+
`Assistant '${from}' not found in lockfile. Run \`vellum ps\` to see available assistants.`,
|
|
721
|
+
);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const toEntry = findAssistantByName(to);
|
|
726
|
+
if (!toEntry) {
|
|
727
|
+
console.error(
|
|
728
|
+
`Assistant '${to}' not found in lockfile. Run \`vellum ps\` to see available assistants.`,
|
|
729
|
+
);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const fromCloud = resolveCloud(fromEntry);
|
|
734
|
+
const toCloud = resolveCloud(toEntry);
|
|
735
|
+
|
|
736
|
+
// Export from source
|
|
737
|
+
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
738
|
+
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
739
|
+
|
|
740
|
+
// Import to target
|
|
741
|
+
console.log(`Importing to ${to} (${toCloud})...`);
|
|
742
|
+
await importToAssistant(toEntry, toCloud, bundleData, dryRun);
|
|
743
|
+
|
|
744
|
+
// Success summary
|
|
745
|
+
console.log(`Teleport complete: ${from} → ${to}`);
|
|
746
|
+
}
|