@vellumai/cli 0.5.15 → 0.6.0
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/bun.lock +46 -52
- package/package.json +1 -1
- package/src/__tests__/teleport.test.ts +1005 -391
- package/src/commands/hatch.ts +17 -388
- package/src/commands/retire.ts +2 -120
- package/src/commands/rollback.ts +6 -0
- package/src/commands/teleport.ts +757 -198
- package/src/commands/upgrade.ts +7 -0
- package/src/lib/aws.ts +2 -1
- package/src/lib/constants.ts +0 -11
- package/src/lib/docker.ts +27 -13
- package/src/lib/gcp.ts +2 -5
- package/src/lib/hatch-local.ts +403 -0
- package/src/lib/local.ts +9 -120
- package/src/lib/platform-client.ts +142 -8
- package/src/lib/retire-local.ts +124 -0
- package/src/lib/upgrade-lifecycle.ts +8 -0
- package/src/shared/provider-env-vars.ts +19 -0
package/src/commands/teleport.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findAssistantByName,
|
|
3
|
+
loadAllAssistants,
|
|
4
|
+
removeAssistantEntry,
|
|
5
|
+
saveAssistantEntry,
|
|
6
|
+
setActiveAssistant,
|
|
7
|
+
} from "../lib/assistant-config.js";
|
|
2
8
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
3
9
|
import {
|
|
4
10
|
loadGuardianToken,
|
|
@@ -7,47 +13,147 @@ import {
|
|
|
7
13
|
import {
|
|
8
14
|
readPlatformToken,
|
|
9
15
|
fetchOrganizationId,
|
|
16
|
+
getPlatformUrl,
|
|
17
|
+
hatchAssistant,
|
|
10
18
|
platformInitiateExport,
|
|
11
19
|
platformPollExportStatus,
|
|
12
20
|
platformDownloadExport,
|
|
13
21
|
platformImportPreflight,
|
|
14
22
|
platformImportBundle,
|
|
23
|
+
platformRequestUploadUrl,
|
|
24
|
+
platformUploadToSignedUrl,
|
|
25
|
+
platformImportPreflightFromGcs,
|
|
26
|
+
platformImportBundleFromGcs,
|
|
15
27
|
} from "../lib/platform-client.js";
|
|
28
|
+
import {
|
|
29
|
+
hatchDocker,
|
|
30
|
+
retireDocker,
|
|
31
|
+
sleepContainers,
|
|
32
|
+
dockerResourceNames,
|
|
33
|
+
} from "../lib/docker.js";
|
|
34
|
+
import { hatchLocal } from "../lib/hatch-local.js";
|
|
35
|
+
import { retireLocal } from "../lib/retire-local.js";
|
|
36
|
+
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
37
|
+
import { stopProcessByPidFile } from "../lib/process.js";
|
|
38
|
+
import { join } from "node:path";
|
|
16
39
|
|
|
17
40
|
function printHelp(): void {
|
|
18
41
|
console.log(
|
|
19
|
-
"Usage: vellum teleport --from <assistant> --
|
|
42
|
+
"Usage: vellum teleport --from <assistant> <--local | --docker | --platform> [name] [options]",
|
|
20
43
|
);
|
|
21
44
|
console.log("");
|
|
22
45
|
console.log(
|
|
23
|
-
"Transfer assistant data between local and platform environments.",
|
|
46
|
+
"Transfer assistant data between local, docker, and platform environments.",
|
|
47
|
+
);
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(
|
|
50
|
+
"The --from flag specifies the source assistant to export data from.",
|
|
51
|
+
);
|
|
52
|
+
console.log(
|
|
53
|
+
"Exactly one environment flag (--local, --docker, --platform) specifies",
|
|
24
54
|
);
|
|
55
|
+
console.log(
|
|
56
|
+
"the target environment. An optional name after the environment flag",
|
|
57
|
+
);
|
|
58
|
+
console.log(
|
|
59
|
+
"targets an existing assistant (overwriting its data) or names a newly",
|
|
60
|
+
);
|
|
61
|
+
console.log(
|
|
62
|
+
"hatched one. If no name is given, a new assistant is hatched with an",
|
|
63
|
+
);
|
|
64
|
+
console.log("auto-generated name.");
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(
|
|
67
|
+
"The source and target must be different environments. Same-environment",
|
|
68
|
+
);
|
|
69
|
+
console.log("transfers (e.g. local to local) are not supported.");
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(
|
|
72
|
+
"For local-to-docker and docker-to-local transfers, the source assistant",
|
|
73
|
+
);
|
|
74
|
+
console.log(
|
|
75
|
+
"is automatically retired after a successful import to free up ports and",
|
|
76
|
+
);
|
|
77
|
+
console.log("avoid resource conflicts. Use --keep-source to skip this.");
|
|
78
|
+
console.log("");
|
|
79
|
+
console.log("Environment flags:");
|
|
80
|
+
console.log(" --local [name] Target a local bare-metal assistant");
|
|
81
|
+
console.log(" --docker [name] Target a docker assistant");
|
|
82
|
+
console.log(" --platform [name] Target a platform-hosted assistant");
|
|
25
83
|
console.log("");
|
|
26
84
|
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
85
|
console.log(
|
|
30
|
-
" --
|
|
86
|
+
" --from <name> Source assistant to export data from (required)",
|
|
87
|
+
);
|
|
88
|
+
console.log(
|
|
89
|
+
" --keep-source Do not retire the source after local/docker transfers",
|
|
90
|
+
);
|
|
91
|
+
console.log(
|
|
92
|
+
" --dry-run Preview the transfer without applying changes.",
|
|
93
|
+
);
|
|
94
|
+
console.log(
|
|
95
|
+
" If the target exists, runs preflight analysis.",
|
|
96
|
+
);
|
|
97
|
+
console.log(
|
|
98
|
+
" If the target would be hatched, shows what would happen",
|
|
31
99
|
);
|
|
32
|
-
console.log("
|
|
100
|
+
console.log(" without creating anything.");
|
|
101
|
+
console.log(" --help, -h Show this help");
|
|
33
102
|
console.log("");
|
|
34
103
|
console.log("Examples:");
|
|
35
|
-
console.log(" vellum teleport --from my-local --
|
|
36
|
-
console.log(
|
|
37
|
-
|
|
104
|
+
console.log(" vellum teleport --from my-local --docker");
|
|
105
|
+
console.log(
|
|
106
|
+
" Hatch a new docker assistant, import data, and retire my-local",
|
|
107
|
+
);
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log(" vellum teleport --from my-local --docker my-docker");
|
|
110
|
+
console.log(
|
|
111
|
+
" Import data from my-local into existing docker assistant my-docker",
|
|
112
|
+
);
|
|
113
|
+
console.log(
|
|
114
|
+
" (or hatch a new docker assistant named my-docker if it doesn't exist)",
|
|
115
|
+
);
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(" vellum teleport --from my-local --platform");
|
|
118
|
+
console.log(
|
|
119
|
+
" Hatch a new platform assistant and import data from my-local",
|
|
120
|
+
);
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(" vellum teleport --from my-cloud --local my-new-local");
|
|
123
|
+
console.log(
|
|
124
|
+
" Import data from platform assistant my-cloud into local assistant",
|
|
125
|
+
);
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(" vellum teleport --from my-docker --local --keep-source");
|
|
128
|
+
console.log(
|
|
129
|
+
" Transfer to a new local assistant but keep the docker source running",
|
|
130
|
+
);
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(
|
|
133
|
+
" vellum teleport --from staging --docker staging-copy --dry-run",
|
|
134
|
+
);
|
|
135
|
+
console.log(" Preview what would be imported without applying changes");
|
|
38
136
|
}
|
|
39
137
|
|
|
40
|
-
function parseArgs(argv: string[]): {
|
|
138
|
+
export function parseArgs(argv: string[]): {
|
|
41
139
|
from: string | undefined;
|
|
42
140
|
to: string | undefined;
|
|
141
|
+
targetEnv: "local" | "docker" | "platform" | undefined;
|
|
142
|
+
targetName: string | undefined;
|
|
143
|
+
keepSource: boolean;
|
|
43
144
|
dryRun: boolean;
|
|
44
145
|
help: boolean;
|
|
45
146
|
} {
|
|
46
147
|
let from: string | undefined;
|
|
47
148
|
let to: string | undefined;
|
|
149
|
+
let targetEnv: "local" | "docker" | "platform" | undefined;
|
|
150
|
+
let targetName: string | undefined;
|
|
151
|
+
let keepSource = false;
|
|
48
152
|
let dryRun = false;
|
|
49
153
|
let help = false;
|
|
50
154
|
|
|
155
|
+
const envFlags: string[] = [];
|
|
156
|
+
|
|
51
157
|
for (let i = 0; i < argv.length; i++) {
|
|
52
158
|
const arg = argv[i];
|
|
53
159
|
if (arg === "--from" && i + 1 < argv.length) {
|
|
@@ -60,6 +166,20 @@ function parseArgs(argv: string[]): {
|
|
|
60
166
|
continue;
|
|
61
167
|
}
|
|
62
168
|
to = argv[++i];
|
|
169
|
+
} else if (
|
|
170
|
+
arg === "--local" ||
|
|
171
|
+
arg === "--docker" ||
|
|
172
|
+
arg === "--platform"
|
|
173
|
+
) {
|
|
174
|
+
const env = arg.slice(2) as "local" | "docker" | "platform";
|
|
175
|
+
envFlags.push(env);
|
|
176
|
+
targetEnv = env;
|
|
177
|
+
// Peek at next arg for optional target name
|
|
178
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
179
|
+
targetName = argv[++i];
|
|
180
|
+
}
|
|
181
|
+
} else if (arg === "--keep-source") {
|
|
182
|
+
keepSource = true;
|
|
63
183
|
} else if (arg === "--dry-run") {
|
|
64
184
|
dryRun = true;
|
|
65
185
|
} else if (arg === "--help" || arg === "-h") {
|
|
@@ -67,7 +187,14 @@ function parseArgs(argv: string[]): {
|
|
|
67
187
|
}
|
|
68
188
|
}
|
|
69
189
|
|
|
70
|
-
|
|
190
|
+
if (envFlags.length > 1) {
|
|
191
|
+
console.error(
|
|
192
|
+
"Error: Only one environment flag (--local, --docker, --platform) may be specified.",
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { from, to, targetEnv, targetName, keepSource, dryRun, help };
|
|
71
198
|
}
|
|
72
199
|
|
|
73
200
|
function resolveCloud(entry: AssistantEntry): string {
|
|
@@ -107,6 +234,245 @@ async function getAccessToken(
|
|
|
107
234
|
}
|
|
108
235
|
}
|
|
109
236
|
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// HTTP-based export/import helpers (shared by local and docker)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
async function exportViaHttp(
|
|
242
|
+
entry: AssistantEntry,
|
|
243
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
244
|
+
let accessToken = await getAccessToken(
|
|
245
|
+
entry.runtimeUrl,
|
|
246
|
+
entry.assistantId,
|
|
247
|
+
entry.assistantId,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
let response: Response;
|
|
251
|
+
try {
|
|
252
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
253
|
+
method: "POST",
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${accessToken}`,
|
|
256
|
+
"Content-Type": "application/json",
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
259
|
+
signal: AbortSignal.timeout(120_000),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Retry once with a fresh token on 401
|
|
263
|
+
if (response.status === 401) {
|
|
264
|
+
let refreshedToken: string | null = null;
|
|
265
|
+
try {
|
|
266
|
+
const freshToken = await leaseGuardianToken(
|
|
267
|
+
entry.runtimeUrl,
|
|
268
|
+
entry.assistantId,
|
|
269
|
+
);
|
|
270
|
+
refreshedToken = freshToken.accessToken;
|
|
271
|
+
} catch {
|
|
272
|
+
// If token refresh fails, fall through to the error handler below
|
|
273
|
+
}
|
|
274
|
+
if (refreshedToken) {
|
|
275
|
+
accessToken = refreshedToken;
|
|
276
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/export`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
Authorization: `Bearer ${accessToken}`,
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ description: "teleport export" }),
|
|
283
|
+
signal: AbortSignal.timeout(120_000),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
289
|
+
console.error("Error: Export request timed out after 2 minutes.");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
294
|
+
console.error(
|
|
295
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
296
|
+
);
|
|
297
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (response.status === 401 || response.status === 403) {
|
|
304
|
+
console.error("Authentication failed.");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (response.status === 404) {
|
|
309
|
+
console.error("Assistant not found or not running.");
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
response.status === 502 ||
|
|
315
|
+
response.status === 503 ||
|
|
316
|
+
response.status === 504
|
|
317
|
+
) {
|
|
318
|
+
console.error(
|
|
319
|
+
`Assistant is unreachable. Try 'vellum wake ${entry.assistantId}'.`,
|
|
320
|
+
);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
const body = await response.text();
|
|
326
|
+
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
331
|
+
return new Uint8Array(arrayBuffer);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function importViaHttp(
|
|
335
|
+
entry: AssistantEntry,
|
|
336
|
+
bundleData: Uint8Array<ArrayBuffer>,
|
|
337
|
+
dryRun: boolean,
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
let accessToken = await getAccessToken(
|
|
340
|
+
entry.runtimeUrl,
|
|
341
|
+
entry.assistantId,
|
|
342
|
+
entry.assistantId,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
if (dryRun) {
|
|
346
|
+
console.log("Running preflight analysis...\n");
|
|
347
|
+
|
|
348
|
+
let response: Response;
|
|
349
|
+
try {
|
|
350
|
+
response = await fetch(
|
|
351
|
+
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
352
|
+
{
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: {
|
|
355
|
+
Authorization: `Bearer ${accessToken}`,
|
|
356
|
+
"Content-Type": "application/octet-stream",
|
|
357
|
+
},
|
|
358
|
+
body: new Blob([bundleData]),
|
|
359
|
+
signal: AbortSignal.timeout(120_000),
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Retry once with a fresh token on 401
|
|
364
|
+
if (response.status === 401) {
|
|
365
|
+
let refreshedToken: string | null = null;
|
|
366
|
+
try {
|
|
367
|
+
const freshToken = await leaseGuardianToken(
|
|
368
|
+
entry.runtimeUrl,
|
|
369
|
+
entry.assistantId,
|
|
370
|
+
);
|
|
371
|
+
refreshedToken = freshToken.accessToken;
|
|
372
|
+
} catch {
|
|
373
|
+
// If token refresh fails, fall through to the error handler below
|
|
374
|
+
}
|
|
375
|
+
if (refreshedToken) {
|
|
376
|
+
accessToken = refreshedToken;
|
|
377
|
+
response = await fetch(
|
|
378
|
+
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
379
|
+
{
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
Authorization: `Bearer ${accessToken}`,
|
|
383
|
+
"Content-Type": "application/octet-stream",
|
|
384
|
+
},
|
|
385
|
+
body: new Blob([bundleData]),
|
|
386
|
+
signal: AbortSignal.timeout(120_000),
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
393
|
+
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
397
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
398
|
+
console.error(
|
|
399
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
400
|
+
);
|
|
401
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
408
|
+
|
|
409
|
+
const result = (await response.json()) as PreflightResponse;
|
|
410
|
+
printPreflightSummary(result);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Actual import
|
|
415
|
+
console.log("Importing data...");
|
|
416
|
+
|
|
417
|
+
let response: Response;
|
|
418
|
+
try {
|
|
419
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: {
|
|
422
|
+
Authorization: `Bearer ${accessToken}`,
|
|
423
|
+
"Content-Type": "application/octet-stream",
|
|
424
|
+
},
|
|
425
|
+
body: new Blob([bundleData]),
|
|
426
|
+
signal: AbortSignal.timeout(120_000),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Retry once with a fresh token on 401
|
|
430
|
+
if (response.status === 401) {
|
|
431
|
+
let refreshedToken: string | null = null;
|
|
432
|
+
try {
|
|
433
|
+
const freshToken = await leaseGuardianToken(
|
|
434
|
+
entry.runtimeUrl,
|
|
435
|
+
entry.assistantId,
|
|
436
|
+
);
|
|
437
|
+
refreshedToken = freshToken.accessToken;
|
|
438
|
+
} catch {
|
|
439
|
+
// If token refresh fails, fall through to the error handler below
|
|
440
|
+
}
|
|
441
|
+
if (refreshedToken) {
|
|
442
|
+
accessToken = refreshedToken;
|
|
443
|
+
response = await fetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: {
|
|
446
|
+
Authorization: `Bearer ${accessToken}`,
|
|
447
|
+
"Content-Type": "application/octet-stream",
|
|
448
|
+
},
|
|
449
|
+
body: new Blob([bundleData]),
|
|
450
|
+
signal: AbortSignal.timeout(120_000),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
456
|
+
console.error("Error: Import request timed out after 2 minutes.");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
460
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
461
|
+
console.error(
|
|
462
|
+
`Error: Could not connect to assistant '${entry.assistantId}'. Is it running?`,
|
|
463
|
+
);
|
|
464
|
+
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
handleLocalResponseErrors(response, entry.assistantId);
|
|
471
|
+
|
|
472
|
+
const result = (await response.json()) as ImportResponse;
|
|
473
|
+
printImportSummary(result);
|
|
474
|
+
}
|
|
475
|
+
|
|
110
476
|
// ---------------------------------------------------------------------------
|
|
111
477
|
// Export from source assistant
|
|
112
478
|
// ---------------------------------------------------------------------------
|
|
@@ -205,100 +571,12 @@ async function exportFromAssistant(
|
|
|
205
571
|
return new Uint8Array(arrayBuffer);
|
|
206
572
|
}
|
|
207
573
|
|
|
208
|
-
if (cloud === "local") {
|
|
209
|
-
|
|
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);
|
|
574
|
+
if (cloud === "local" || cloud === "docker") {
|
|
575
|
+
return exportViaHttp(entry);
|
|
298
576
|
}
|
|
299
577
|
|
|
300
578
|
console.error(
|
|
301
|
-
"Teleport only supports local and platform assistants as source.",
|
|
579
|
+
"Teleport only supports local, docker, and platform assistants as source.",
|
|
302
580
|
);
|
|
303
581
|
process.exit(1);
|
|
304
582
|
}
|
|
@@ -354,6 +632,7 @@ async function importToAssistant(
|
|
|
354
632
|
cloud: string,
|
|
355
633
|
bundleData: Uint8Array<ArrayBuffer>,
|
|
356
634
|
dryRun: boolean,
|
|
635
|
+
preUploadedBundleKey?: string | null,
|
|
357
636
|
): Promise<void> {
|
|
358
637
|
if (cloud === "vellum") {
|
|
359
638
|
// Platform target
|
|
@@ -375,6 +654,32 @@ async function importToAssistant(
|
|
|
375
654
|
throw err;
|
|
376
655
|
}
|
|
377
656
|
|
|
657
|
+
// Use pre-uploaded bundle key if provided (string), skip upload if null
|
|
658
|
+
// (signals signed URLs were already tried and unavailable), or try
|
|
659
|
+
// signed-URL upload if undefined (never attempted).
|
|
660
|
+
let bundleKey: string | undefined =
|
|
661
|
+
preUploadedBundleKey === null ? undefined : preUploadedBundleKey;
|
|
662
|
+
if (preUploadedBundleKey === undefined) {
|
|
663
|
+
try {
|
|
664
|
+
const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
|
|
665
|
+
token,
|
|
666
|
+
orgId,
|
|
667
|
+
entry.runtimeUrl,
|
|
668
|
+
);
|
|
669
|
+
bundleKey = key;
|
|
670
|
+
console.log("Uploading bundle...");
|
|
671
|
+
await platformUploadToSignedUrl(uploadUrl, bundleData);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
// If signed uploads unavailable (503), fall back to inline upload
|
|
674
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
675
|
+
if (msg.includes("not available")) {
|
|
676
|
+
bundleKey = undefined;
|
|
677
|
+
} else {
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
378
683
|
if (dryRun) {
|
|
379
684
|
console.log("Running preflight analysis...\n");
|
|
380
685
|
|
|
@@ -383,12 +688,19 @@ async function importToAssistant(
|
|
|
383
688
|
body: Record<string, unknown>;
|
|
384
689
|
};
|
|
385
690
|
try {
|
|
386
|
-
preflightResult =
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
691
|
+
preflightResult = bundleKey
|
|
692
|
+
? await platformImportPreflightFromGcs(
|
|
693
|
+
bundleKey,
|
|
694
|
+
token,
|
|
695
|
+
orgId,
|
|
696
|
+
entry.runtimeUrl,
|
|
697
|
+
)
|
|
698
|
+
: await platformImportPreflight(
|
|
699
|
+
bundleData,
|
|
700
|
+
token,
|
|
701
|
+
orgId,
|
|
702
|
+
entry.runtimeUrl,
|
|
703
|
+
);
|
|
392
704
|
} catch (err) {
|
|
393
705
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
394
706
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -438,12 +750,19 @@ async function importToAssistant(
|
|
|
438
750
|
|
|
439
751
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
440
752
|
try {
|
|
441
|
-
importResult =
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
753
|
+
importResult = bundleKey
|
|
754
|
+
? await platformImportBundleFromGcs(
|
|
755
|
+
bundleKey,
|
|
756
|
+
token,
|
|
757
|
+
orgId,
|
|
758
|
+
entry.runtimeUrl,
|
|
759
|
+
)
|
|
760
|
+
: await platformImportBundle(
|
|
761
|
+
bundleData,
|
|
762
|
+
token,
|
|
763
|
+
orgId,
|
|
764
|
+
entry.runtimeUrl,
|
|
765
|
+
);
|
|
447
766
|
} catch (err) {
|
|
448
767
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
449
768
|
console.error("Error: Import request timed out after 2 minutes.");
|
|
@@ -459,94 +778,137 @@ async function importToAssistant(
|
|
|
459
778
|
return;
|
|
460
779
|
}
|
|
461
780
|
|
|
462
|
-
if (cloud === "local") {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
entry.assistantId,
|
|
467
|
-
entry.assistantId,
|
|
468
|
-
);
|
|
781
|
+
if (cloud === "local" || cloud === "docker") {
|
|
782
|
+
await importViaHttp(entry, bundleData, dryRun);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
469
785
|
|
|
470
|
-
|
|
471
|
-
|
|
786
|
+
console.error(
|
|
787
|
+
"Teleport only supports local, docker, and platform assistants as target.",
|
|
788
|
+
);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
// Resolve or hatch target assistant
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
472
795
|
|
|
473
|
-
|
|
796
|
+
export async function resolveOrHatchTarget(
|
|
797
|
+
targetEnv: "local" | "docker" | "platform",
|
|
798
|
+
targetName?: string,
|
|
799
|
+
orgId?: string,
|
|
800
|
+
): Promise<AssistantEntry> {
|
|
801
|
+
// If a name is provided, try to find an existing assistant
|
|
802
|
+
if (targetName) {
|
|
803
|
+
const existing = findAssistantByName(targetName);
|
|
804
|
+
if (existing) {
|
|
805
|
+
// Validate the existing assistant's cloud matches the requested env
|
|
806
|
+
const existingCloud = resolveCloud(existing);
|
|
807
|
+
const normalizedExisting =
|
|
808
|
+
existingCloud === "vellum" ? "platform" : existingCloud;
|
|
809
|
+
if (normalizedExisting !== targetEnv) {
|
|
810
|
+
console.error(
|
|
811
|
+
`Error: Assistant '${targetName}' is a ${normalizedExisting} assistant, not ${targetEnv}. ` +
|
|
812
|
+
`Use --${normalizedExisting} to target it.`,
|
|
813
|
+
);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
console.log(`Target: ${targetName} (${targetEnv})`);
|
|
817
|
+
return existing;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Name not found — will hatch.
|
|
821
|
+
if (targetEnv === "platform") {
|
|
822
|
+
// Platform API doesn't accept custom names — warn and ignore
|
|
823
|
+
console.log(
|
|
824
|
+
`Note: Platform assistants receive a server-assigned ID. The name '${targetName}' will not be used.`,
|
|
825
|
+
);
|
|
826
|
+
} else {
|
|
827
|
+
// Validate the name before passing to hatch
|
|
474
828
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
},
|
|
829
|
+
validateAssistantName(targetName);
|
|
830
|
+
} catch {
|
|
831
|
+
console.error(
|
|
832
|
+
"Error: Target name contains invalid characters (path separators or traversal segments are not allowed).",
|
|
486
833
|
);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Hatch a new assistant in the target environment
|
|
840
|
+
if (targetEnv === "local") {
|
|
841
|
+
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
842
|
+
await hatchLocal("vellum", targetName ?? null, false, false, false, {});
|
|
843
|
+
const entry = targetName
|
|
844
|
+
? findAssistantByName(targetName)
|
|
845
|
+
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
846
|
+
null);
|
|
847
|
+
if (!entry) {
|
|
848
|
+
console.error("Error: Could not find the newly hatched local assistant.");
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
console.log(`Hatched new local assistant: ${entry.assistantId}`);
|
|
852
|
+
return entry;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (targetEnv === "docker") {
|
|
856
|
+
const beforeIds = new Set(loadAllAssistants().map((e) => e.assistantId));
|
|
857
|
+
await hatchDocker("vellum", false, targetName ?? null, false, {});
|
|
858
|
+
const entry = targetName
|
|
859
|
+
? findAssistantByName(targetName)
|
|
860
|
+
: (loadAllAssistants().find((e) => !beforeIds.has(e.assistantId)) ??
|
|
861
|
+
null);
|
|
862
|
+
if (!entry) {
|
|
863
|
+
console.error(
|
|
864
|
+
"Error: Could not find the newly hatched docker assistant.",
|
|
865
|
+
);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
console.log(`Hatched new docker assistant: ${entry.assistantId}`);
|
|
869
|
+
return entry;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (targetEnv === "platform") {
|
|
873
|
+
const token = readPlatformToken();
|
|
874
|
+
if (!token) {
|
|
875
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
let resolvedOrgId: string;
|
|
880
|
+
if (orgId) {
|
|
881
|
+
resolvedOrgId = orgId;
|
|
882
|
+
} else {
|
|
883
|
+
try {
|
|
884
|
+
resolvedOrgId = await fetchOrganizationId(token);
|
|
487
885
|
} 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
886
|
const msg = err instanceof Error ? err.message : String(err);
|
|
493
|
-
if (msg.includes("
|
|
887
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
494
888
|
console.error(
|
|
495
|
-
|
|
889
|
+
"Authentication failed. Run 'vellum login' to refresh.",
|
|
496
890
|
);
|
|
497
|
-
console.error(`Try: vellum wake ${entry.assistantId}`);
|
|
498
891
|
process.exit(1);
|
|
499
892
|
}
|
|
500
893
|
throw err;
|
|
501
894
|
}
|
|
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
895
|
}
|
|
539
896
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
897
|
+
const result = await hatchAssistant(token, resolvedOrgId);
|
|
898
|
+
const entry: AssistantEntry = {
|
|
899
|
+
assistantId: result.id,
|
|
900
|
+
runtimeUrl: getPlatformUrl(),
|
|
901
|
+
cloud: "vellum",
|
|
902
|
+
species: "vellum",
|
|
903
|
+
hatchedAt: new Date().toISOString(),
|
|
904
|
+
};
|
|
905
|
+
saveAssistantEntry(entry);
|
|
906
|
+
setActiveAssistant(result.id);
|
|
907
|
+
console.log(`Hatched new platform assistant: ${result.id}`);
|
|
908
|
+
return entry;
|
|
545
909
|
}
|
|
546
910
|
|
|
547
|
-
console.error(
|
|
548
|
-
"Teleport only supports local and platform assistants as target.",
|
|
549
|
-
);
|
|
911
|
+
console.error(`Error: Unknown target environment '${targetEnv}'.`);
|
|
550
912
|
process.exit(1);
|
|
551
913
|
}
|
|
552
914
|
|
|
@@ -701,19 +1063,36 @@ function printImportSummary(result: ImportResponse): void {
|
|
|
701
1063
|
|
|
702
1064
|
export async function teleport(): Promise<void> {
|
|
703
1065
|
const args = process.argv.slice(3);
|
|
704
|
-
const { from, to, dryRun, help } =
|
|
1066
|
+
const { from, to, targetEnv, targetName, keepSource, dryRun, help } =
|
|
1067
|
+
parseArgs(args);
|
|
705
1068
|
|
|
706
1069
|
if (help) {
|
|
707
1070
|
printHelp();
|
|
708
1071
|
process.exit(0);
|
|
709
1072
|
}
|
|
710
1073
|
|
|
711
|
-
|
|
1074
|
+
// Legacy --to flag deprecation
|
|
1075
|
+
if (to) {
|
|
1076
|
+
console.error("Error: --to is deprecated. Use environment flags instead:");
|
|
1077
|
+
console.error(
|
|
1078
|
+
" vellum teleport --from <source> --local|--docker|--platform [name]",
|
|
1079
|
+
);
|
|
1080
|
+
console.error("");
|
|
1081
|
+
console.error("Run 'vellum teleport --help' for details.");
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (!from) {
|
|
712
1086
|
printHelp();
|
|
713
1087
|
process.exit(1);
|
|
714
1088
|
}
|
|
715
1089
|
|
|
716
|
-
|
|
1090
|
+
if (!targetEnv) {
|
|
1091
|
+
printHelp();
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Look up source assistant
|
|
717
1096
|
const fromEntry = findAssistantByName(from);
|
|
718
1097
|
if (!fromEntry) {
|
|
719
1098
|
console.error(
|
|
@@ -722,25 +1101,205 @@ export async function teleport(): Promise<void> {
|
|
|
722
1101
|
process.exit(1);
|
|
723
1102
|
}
|
|
724
1103
|
|
|
725
|
-
const
|
|
726
|
-
|
|
1104
|
+
const fromCloud = resolveCloud(fromEntry);
|
|
1105
|
+
|
|
1106
|
+
// Early same-environment guard — compare source cloud against the CLI flag
|
|
1107
|
+
// BEFORE exporting or hatching, to avoid creating orphaned assistants.
|
|
1108
|
+
const normalizedSourceEnv = fromCloud === "vellum" ? "platform" : fromCloud;
|
|
1109
|
+
if (normalizedSourceEnv === targetEnv) {
|
|
727
1110
|
console.error(
|
|
728
|
-
`
|
|
1111
|
+
`Cannot teleport between two ${targetEnv} assistants. Teleport transfers data across different environments.`,
|
|
729
1112
|
);
|
|
730
1113
|
process.exit(1);
|
|
731
1114
|
}
|
|
732
1115
|
|
|
733
|
-
|
|
734
|
-
|
|
1116
|
+
// Dry-run without an existing target: skip export, hatch, and import —
|
|
1117
|
+
// just report what would happen.
|
|
1118
|
+
if (dryRun) {
|
|
1119
|
+
const existingTarget = targetName ? findAssistantByName(targetName) : null;
|
|
1120
|
+
|
|
1121
|
+
if (existingTarget) {
|
|
1122
|
+
// Target exists — validate cloud matches the flag, then run preflight
|
|
1123
|
+
const toCloud = resolveCloud(existingTarget);
|
|
1124
|
+
const normalizedTargetEnv = toCloud === "vellum" ? "platform" : toCloud;
|
|
1125
|
+
if (normalizedTargetEnv !== targetEnv) {
|
|
1126
|
+
console.error(
|
|
1127
|
+
`Error: Assistant '${targetName}' is a ${normalizedTargetEnv} assistant, not ${targetEnv}. ` +
|
|
1128
|
+
`Use --${normalizedTargetEnv} to target it.`,
|
|
1129
|
+
);
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
if (normalizedSourceEnv === normalizedTargetEnv) {
|
|
1133
|
+
console.error(
|
|
1134
|
+
`Cannot teleport between two ${normalizedTargetEnv} assistants. Teleport transfers data across different environments.`,
|
|
1135
|
+
);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
1140
|
+
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
1141
|
+
console.log(`Importing to ${existingTarget.assistantId} (${toCloud})...`);
|
|
1142
|
+
await importToAssistant(existingTarget, toCloud, bundleData, true);
|
|
1143
|
+
} else {
|
|
1144
|
+
// No existing target — just describe what would happen
|
|
1145
|
+
console.log("Dry run summary:");
|
|
1146
|
+
console.log(` Would export data from: ${from} (${fromCloud})`);
|
|
1147
|
+
if (targetEnv === "platform") {
|
|
1148
|
+
// For platform targets, reflect the reordered flow
|
|
1149
|
+
console.log(` Would upload bundle via signed URL (if available)`);
|
|
1150
|
+
console.log(
|
|
1151
|
+
` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
|
|
1152
|
+
);
|
|
1153
|
+
console.log(` Would import data into the new assistant`);
|
|
1154
|
+
} else {
|
|
1155
|
+
console.log(
|
|
1156
|
+
` Would hatch a new ${targetEnv} assistant${targetName ? ` named '${targetName}'` : ""}`,
|
|
1157
|
+
);
|
|
1158
|
+
console.log(` Would import data into the new assistant`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
console.log(`Dry run complete — no changes were made.`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
735
1165
|
|
|
736
1166
|
// Export from source
|
|
737
1167
|
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
738
1168
|
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
739
1169
|
|
|
1170
|
+
// Platform target: reordered flow — upload to GCS before hatching so that
|
|
1171
|
+
// if upload fails, no empty assistant is left dangling on the platform.
|
|
1172
|
+
if (targetEnv === "platform") {
|
|
1173
|
+
// Step B — Auth + Org ID
|
|
1174
|
+
const token = readPlatformToken();
|
|
1175
|
+
if (!token) {
|
|
1176
|
+
console.error("Not logged in. Run 'vellum login' first.");
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// If targeting an existing assistant, validate cloud match early — before
|
|
1181
|
+
// uploading — so we don't waste a GCS upload on an invalid command.
|
|
1182
|
+
const existingTarget = targetName ? findAssistantByName(targetName) : null;
|
|
1183
|
+
if (existingTarget) {
|
|
1184
|
+
const existingCloud = resolveCloud(existingTarget);
|
|
1185
|
+
if (existingCloud !== "vellum") {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`Error: Assistant '${targetName}' is a ${existingCloud} assistant, not platform. ` +
|
|
1188
|
+
`Use --${existingCloud} to target it.`,
|
|
1189
|
+
);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Use the existing target's runtimeUrl for all platform calls so upload,
|
|
1195
|
+
// org ID fetch, and import hit the same instance.
|
|
1196
|
+
const targetPlatformUrl = existingTarget?.runtimeUrl;
|
|
1197
|
+
|
|
1198
|
+
let orgId: string;
|
|
1199
|
+
try {
|
|
1200
|
+
orgId = await fetchOrganizationId(token, targetPlatformUrl);
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1203
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
1204
|
+
console.error("Authentication failed. Run 'vellum login' to refresh.");
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
throw err;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Step C — Upload to GCS
|
|
1211
|
+
// bundleKey: string = uploaded successfully, null = tried but unavailable,
|
|
1212
|
+
// undefined would mean "never tried" (not used here).
|
|
1213
|
+
let bundleKey: string | null = null;
|
|
1214
|
+
try {
|
|
1215
|
+
const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
|
|
1216
|
+
token,
|
|
1217
|
+
orgId,
|
|
1218
|
+
targetPlatformUrl,
|
|
1219
|
+
);
|
|
1220
|
+
bundleKey = key;
|
|
1221
|
+
console.log("Uploading bundle to GCS...");
|
|
1222
|
+
await platformUploadToSignedUrl(uploadUrl, bundleData);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
// If signed uploads unavailable (503), fall back to inline upload later
|
|
1225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1226
|
+
if (msg.includes("not available")) {
|
|
1227
|
+
bundleKey = null;
|
|
1228
|
+
} else {
|
|
1229
|
+
throw err;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Step D — Hatch (upload succeeded or fallback to inline — safe to hatch)
|
|
1234
|
+
const toEntry = await resolveOrHatchTarget(targetEnv, targetName, orgId);
|
|
1235
|
+
const toCloud = resolveCloud(toEntry);
|
|
1236
|
+
|
|
1237
|
+
// Step E — Import from GCS (or inline fallback)
|
|
1238
|
+
// Pass bundleKey (string) or null to signal "already tried, use inline".
|
|
1239
|
+
console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
|
|
1240
|
+
await importToAssistant(toEntry, toCloud, bundleData, false, bundleKey);
|
|
1241
|
+
|
|
1242
|
+
// Success summary
|
|
1243
|
+
console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Non-platform targets (local/docker): existing flow unchanged
|
|
1248
|
+
// For local<->docker transfers, stop (sleep) the source to free up ports
|
|
1249
|
+
// before hatching the target. We do NOT retire yet — if hatch or import
|
|
1250
|
+
// fails, the user can recover by running `vellum wake <source>`.
|
|
1251
|
+
const sourceIsLocalOrDocker = fromCloud === "local" || fromCloud === "docker";
|
|
1252
|
+
const targetIsLocalOrDocker = targetEnv === "local" || targetEnv === "docker";
|
|
1253
|
+
if (sourceIsLocalOrDocker && targetIsLocalOrDocker && !keepSource) {
|
|
1254
|
+
console.log(`Stopping source assistant '${from}' to free ports...`);
|
|
1255
|
+
if (fromCloud === "docker") {
|
|
1256
|
+
const res = dockerResourceNames(fromEntry.assistantId);
|
|
1257
|
+
await sleepContainers(res);
|
|
1258
|
+
} else if (fromEntry.resources) {
|
|
1259
|
+
const pidFile = fromEntry.resources.pidFile;
|
|
1260
|
+
const vellumDir = join(fromEntry.resources.instanceDir, ".vellum");
|
|
1261
|
+
const gatewayPidFile = join(vellumDir, "gateway.pid");
|
|
1262
|
+
await stopProcessByPidFile(pidFile, "assistant");
|
|
1263
|
+
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
1264
|
+
}
|
|
1265
|
+
console.log(`Source assistant '${from}' stopped.`);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Resolve or hatch target (after source is stopped to avoid port conflicts)
|
|
1269
|
+
const toEntry = await resolveOrHatchTarget(targetEnv, targetName);
|
|
1270
|
+
const toCloud = resolveCloud(toEntry);
|
|
1271
|
+
|
|
1272
|
+
// Post-hatch same-environment safety net — uses resolved clouds in case
|
|
1273
|
+
// the resolved target cloud differs from the CLI flag (e.g., --docker
|
|
1274
|
+
// targeting a name that is actually a local entry).
|
|
1275
|
+
const normalizedTargetEnv = toCloud === "vellum" ? "platform" : toCloud;
|
|
1276
|
+
if (normalizedSourceEnv === normalizedTargetEnv) {
|
|
1277
|
+
console.error(
|
|
1278
|
+
`Cannot teleport between two ${normalizedTargetEnv} assistants. Teleport transfers data across different environments.`,
|
|
1279
|
+
);
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
740
1283
|
// Import to target
|
|
741
|
-
console.log(`Importing to ${
|
|
742
|
-
await importToAssistant(toEntry, toCloud, bundleData,
|
|
1284
|
+
console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
|
|
1285
|
+
await importToAssistant(toEntry, toCloud, bundleData, false);
|
|
1286
|
+
|
|
1287
|
+
// Retire source after successful import
|
|
1288
|
+
if (sourceIsLocalOrDocker && targetIsLocalOrDocker) {
|
|
1289
|
+
if (!keepSource) {
|
|
1290
|
+
console.log(`Retiring source assistant '${from}'...`);
|
|
1291
|
+
if (fromCloud === "docker") {
|
|
1292
|
+
await retireDocker(fromEntry.assistantId);
|
|
1293
|
+
} else {
|
|
1294
|
+
await retireLocal(fromEntry.assistantId, fromEntry);
|
|
1295
|
+
}
|
|
1296
|
+
removeAssistantEntry(fromEntry.assistantId);
|
|
1297
|
+
console.log(`Source assistant '${from}' retired.`);
|
|
1298
|
+
} else {
|
|
1299
|
+
console.log(`Source assistant '${from}' kept (--keep-source).`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
743
1302
|
|
|
744
1303
|
// Success summary
|
|
745
|
-
console.log(`Teleport complete: ${from} → ${
|
|
1304
|
+
console.log(`Teleport complete: ${from} → ${toEntry.assistantId}`);
|
|
746
1305
|
}
|