@vellumai/cli 0.7.0 → 0.7.1
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/README.md +49 -0
- package/package.json +1 -1
- package/src/__tests__/backup.test.ts +475 -0
- package/src/__tests__/config-utils.test.ts +35 -48
- package/src/__tests__/teleport.test.ts +86 -28
- package/src/commands/backup.ts +117 -71
- package/src/commands/client.ts +10 -9
- package/src/commands/exec.ts +21 -8
- package/src/commands/hatch.ts +2 -6
- package/src/commands/login.ts +15 -33
- package/src/commands/logs.ts +2 -7
- package/src/commands/ps.ts +41 -6
- package/src/commands/restore.ts +26 -47
- package/src/commands/ssh.ts +2 -5
- package/src/commands/teleport.ts +38 -24
- package/src/commands/tunnel.ts +2 -7
- package/src/commands/upgrade.ts +108 -7
- package/src/components/DefaultMainScreen.tsx +25 -3
- package/src/index.ts +2 -7
- package/src/lib/__tests__/local-runtime-client.test.ts +122 -25
- package/src/lib/__tests__/platform-client-signed-url.test.ts +2 -2
- package/src/lib/__tests__/runtime-url.test.ts +87 -0
- package/src/lib/__tests__/terminal-session.test.ts +202 -0
- package/src/lib/assistant-client.ts +5 -21
- package/src/lib/assistant-config.ts +34 -16
- package/src/lib/cli-error.ts +1 -0
- package/src/lib/client-identity.ts +1 -1
- package/src/lib/config-utils.ts +1 -97
- package/src/lib/docker.ts +2 -2
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/local-runtime-client.ts +81 -28
- package/src/lib/local.ts +27 -58
- package/src/lib/platform-client.ts +1 -220
- package/src/lib/platform-releases.ts +23 -0
- package/src/lib/runtime-url.ts +30 -0
- package/src/lib/sync-cloud-assistants.ts +126 -0
- package/src/lib/terminal-client.ts +6 -1
- package/src/lib/terminal-session.ts +127 -48
- package/src/lib/tui-log.ts +60 -0
- package/src/lib/xdg-log.ts +10 -4
package/src/commands/login.ts
CHANGED
|
@@ -3,12 +3,10 @@ import { spawn } from "child_process";
|
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
findAssistantByName,
|
|
7
6
|
getActiveAssistant,
|
|
8
|
-
|
|
7
|
+
resolveAssistant,
|
|
9
8
|
loadAllAssistants,
|
|
10
9
|
removeAssistantEntry,
|
|
11
|
-
saveAssistantEntry,
|
|
12
10
|
setActiveAssistant,
|
|
13
11
|
} from "../lib/assistant-config";
|
|
14
12
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
@@ -26,6 +24,7 @@ import {
|
|
|
26
24
|
reprovisionAssistantApiKey,
|
|
27
25
|
savePlatformToken,
|
|
28
26
|
} from "../lib/platform-client";
|
|
27
|
+
import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
29
28
|
|
|
30
29
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
31
30
|
|
|
@@ -205,10 +204,7 @@ export async function login(): Promise<void> {
|
|
|
205
204
|
// Register the local assistant with the platform (non-fatal).
|
|
206
205
|
// Mirrors the desktop app's LocalAssistantBootstrapService flow.
|
|
207
206
|
try {
|
|
208
|
-
const
|
|
209
|
-
const entry = activeName
|
|
210
|
-
? findAssistantByName(activeName)
|
|
211
|
-
: loadLatestAssistant();
|
|
207
|
+
const entry = resolveAssistant();
|
|
212
208
|
|
|
213
209
|
// Skip managed ("vellum") assistants — they are handled by the platform.
|
|
214
210
|
if (entry && entry.cloud !== "vellum") {
|
|
@@ -282,36 +278,22 @@ export async function login(): Promise<void> {
|
|
|
282
278
|
// This ensures `vellum ps` shows managed assistants immediately
|
|
283
279
|
// after login (e.g. after a retire-and-rehatch cycle).
|
|
284
280
|
try {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
let synced = 0;
|
|
293
|
-
for (const pa of platformAssistants) {
|
|
294
|
-
if (!existingIds.has(pa.id)) {
|
|
295
|
-
saveAssistantEntry({
|
|
296
|
-
assistantId: pa.id,
|
|
297
|
-
runtimeUrl: getPlatformUrl(),
|
|
298
|
-
cloud: "vellum",
|
|
299
|
-
species: "vellum",
|
|
300
|
-
hatchedAt: new Date().toISOString(),
|
|
301
|
-
});
|
|
302
|
-
synced++;
|
|
281
|
+
const result = await syncCloudAssistants();
|
|
282
|
+
if (result) {
|
|
283
|
+
const total = result.added + result.removed;
|
|
284
|
+
if (total > 0) {
|
|
285
|
+
console.log(
|
|
286
|
+
`Synced cloud assistants (${result.added} added, ${result.removed} removed).`,
|
|
287
|
+
);
|
|
303
288
|
}
|
|
304
289
|
}
|
|
305
290
|
|
|
306
|
-
if (synced > 0) {
|
|
307
|
-
console.log(
|
|
308
|
-
`Synced ${synced} cloud assistant${synced > 1 ? "s" : ""} to local lockfile.`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
291
|
// If no active assistant is set, activate the first cloud one.
|
|
313
|
-
if (!getActiveAssistant()
|
|
314
|
-
|
|
292
|
+
if (!getActiveAssistant()) {
|
|
293
|
+
const platformAssistants = await fetchPlatformAssistants(token);
|
|
294
|
+
if (platformAssistants.length > 0) {
|
|
295
|
+
setActiveAssistant(platformAssistants[0].id);
|
|
296
|
+
}
|
|
315
297
|
}
|
|
316
298
|
} catch {
|
|
317
299
|
// Non-fatal — login succeeded even if sync fails
|
package/src/commands/logs.ts
CHANGED
|
@@ -4,10 +4,7 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { watch } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
findAssistantByName,
|
|
9
|
-
loadLatestAssistant,
|
|
10
|
-
} from "../lib/assistant-config";
|
|
7
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
11
8
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
12
9
|
import { dockerResourceNames } from "../lib/docker";
|
|
13
10
|
import { getLogDir } from "../lib/xdg-log";
|
|
@@ -593,9 +590,7 @@ async function showAwsLogs(
|
|
|
593
590
|
export async function logs(): Promise<void> {
|
|
594
591
|
const opts = parseArgs();
|
|
595
592
|
|
|
596
|
-
const entry = opts.name
|
|
597
|
-
? findAssistantByName(opts.name)
|
|
598
|
-
: loadLatestAssistant();
|
|
593
|
+
const entry = resolveAssistant(opts.name);
|
|
599
594
|
|
|
600
595
|
if (!entry) {
|
|
601
596
|
if (opts.name) {
|
package/src/commands/ps.ts
CHANGED
|
@@ -28,6 +28,10 @@ import { pgrepExact } from "../lib/pgrep";
|
|
|
28
28
|
import { probePort } from "../lib/port-probe";
|
|
29
29
|
import { withStatusEmoji } from "../lib/status-emoji";
|
|
30
30
|
import { execOutput } from "../lib/step-runner";
|
|
31
|
+
import {
|
|
32
|
+
syncCloudAssistants,
|
|
33
|
+
type SyncLogger,
|
|
34
|
+
} from "../lib/sync-cloud-assistants";
|
|
31
35
|
|
|
32
36
|
// ── Table formatting helpers ────────────────────────────────────
|
|
33
37
|
|
|
@@ -468,7 +472,7 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
468
472
|
|
|
469
473
|
// ── List all assistants (no arg) ────────────────────────────────
|
|
470
474
|
|
|
471
|
-
async function listAllAssistants(): Promise<void> {
|
|
475
|
+
async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
472
476
|
const { name: envName, source: envSource } = resolveEnvironmentSource();
|
|
473
477
|
const sourceLabels: Record<typeof envSource, string> = {
|
|
474
478
|
flag: "--environment flag",
|
|
@@ -476,7 +480,31 @@ async function listAllAssistants(): Promise<void> {
|
|
|
476
480
|
config: "~/.config/vellum/environment",
|
|
477
481
|
default: "default",
|
|
478
482
|
};
|
|
479
|
-
console.log(`Environment: ${envName} (${sourceLabels[envSource]})
|
|
483
|
+
console.log(`Environment: ${envName} (${sourceLabels[envSource]})`);
|
|
484
|
+
|
|
485
|
+
const log: SyncLogger | undefined = verbose
|
|
486
|
+
? (msg) => console.log(` [verbose] ${msg}`)
|
|
487
|
+
: undefined;
|
|
488
|
+
|
|
489
|
+
// Refresh cloud assistants from the platform before listing.
|
|
490
|
+
const syncResult = await syncCloudAssistants({ log });
|
|
491
|
+
|
|
492
|
+
// Show platform login status
|
|
493
|
+
if (syncResult) {
|
|
494
|
+
const parts = [`Platform: logged in`];
|
|
495
|
+
if (syncResult.email) parts[0] += ` as ${syncResult.email}`;
|
|
496
|
+
if (syncResult.added > 0 || syncResult.removed > 0) {
|
|
497
|
+
const changes: string[] = [];
|
|
498
|
+
if (syncResult.added > 0) changes.push(`${syncResult.added} added`);
|
|
499
|
+
if (syncResult.removed > 0)
|
|
500
|
+
changes.push(`${syncResult.removed} removed`);
|
|
501
|
+
parts.push(`(${changes.join(", ")})`);
|
|
502
|
+
}
|
|
503
|
+
console.log(parts.join(" "));
|
|
504
|
+
} else {
|
|
505
|
+
console.log("Platform: not logged in");
|
|
506
|
+
}
|
|
507
|
+
console.log("");
|
|
480
508
|
|
|
481
509
|
const assistants = loadAllAssistants();
|
|
482
510
|
const activeId = getActiveAssistant();
|
|
@@ -599,21 +627,28 @@ async function listAllAssistants(): Promise<void> {
|
|
|
599
627
|
export async function ps(): Promise<void> {
|
|
600
628
|
const args = process.argv.slice(3);
|
|
601
629
|
if (args.includes("--help") || args.includes("-h")) {
|
|
602
|
-
console.log("Usage: vellum ps [<name>]");
|
|
630
|
+
console.log("Usage: vellum ps [<name>] [--verbose]");
|
|
603
631
|
console.log("");
|
|
604
632
|
console.log(
|
|
605
633
|
"List all assistants, or show processes for a specific assistant.",
|
|
606
634
|
);
|
|
607
635
|
console.log("");
|
|
608
636
|
console.log("Arguments:");
|
|
609
|
-
console.log(" <name>
|
|
637
|
+
console.log(" <name> Show processes for the named assistant");
|
|
638
|
+
console.log("");
|
|
639
|
+
console.log("Options:");
|
|
640
|
+
console.log(
|
|
641
|
+
" --verbose Show diagnostic logs (platform sync, auth issues)",
|
|
642
|
+
);
|
|
610
643
|
process.exit(0);
|
|
611
644
|
}
|
|
612
645
|
|
|
613
|
-
const
|
|
646
|
+
const verbose = args.includes("--verbose");
|
|
647
|
+
const positional = args.filter((a) => !a.startsWith("--"));
|
|
648
|
+
const assistantId = positional[0];
|
|
614
649
|
|
|
615
650
|
if (!assistantId) {
|
|
616
|
-
await listAllAssistants();
|
|
651
|
+
await listAllAssistants(verbose);
|
|
617
652
|
return;
|
|
618
653
|
}
|
|
619
654
|
|
package/src/commands/restore.ts
CHANGED
|
@@ -9,13 +9,11 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
readPlatformToken,
|
|
11
11
|
rollbackPlatformAssistant,
|
|
12
|
-
|
|
13
|
-
platformImportBundle,
|
|
14
|
-
platformRequestUploadUrl,
|
|
12
|
+
platformRequestSignedUrl,
|
|
15
13
|
platformUploadToSignedUrl,
|
|
16
14
|
platformImportPreflightFromGcs,
|
|
17
15
|
platformImportBundleFromGcs,
|
|
18
|
-
|
|
16
|
+
platformPollJobStatus,
|
|
19
17
|
} from "../lib/platform-client.js";
|
|
20
18
|
import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
|
|
21
19
|
|
|
@@ -181,24 +179,14 @@ async function restorePlatform(
|
|
|
181
179
|
process.exit(1);
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
// Step 1.5 — Upload to GCS via signed URL
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log("Uploading bundle...");
|
|
193
|
-
await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
|
|
194
|
-
} catch (err) {
|
|
195
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
-
if (msg.includes("not available")) {
|
|
197
|
-
bundleKey = null;
|
|
198
|
-
} else {
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
182
|
+
// Step 1.5 — Upload to GCS via signed URL
|
|
183
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
184
|
+
{ operation: "upload" },
|
|
185
|
+
token,
|
|
186
|
+
entry.runtimeUrl,
|
|
187
|
+
);
|
|
188
|
+
console.log("Uploading bundle...");
|
|
189
|
+
await platformUploadToSignedUrl(uploadUrl, new Uint8Array(bundleData));
|
|
202
190
|
|
|
203
191
|
// Step 2 — Dry-run path
|
|
204
192
|
if (opts.dryRun) {
|
|
@@ -213,17 +201,11 @@ async function restorePlatform(
|
|
|
213
201
|
|
|
214
202
|
let preflightResult: { statusCode: number; body: Record<string, unknown> };
|
|
215
203
|
try {
|
|
216
|
-
preflightResult =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
: await platformImportPreflight(
|
|
223
|
-
new Uint8Array(bundleData),
|
|
224
|
-
token,
|
|
225
|
-
entry.runtimeUrl,
|
|
226
|
-
);
|
|
204
|
+
preflightResult = await platformImportPreflightFromGcs(
|
|
205
|
+
bundleKey,
|
|
206
|
+
token,
|
|
207
|
+
entry.runtimeUrl,
|
|
208
|
+
);
|
|
227
209
|
} catch (err) {
|
|
228
210
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
229
211
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -353,13 +335,11 @@ async function restorePlatform(
|
|
|
353
335
|
|
|
354
336
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
355
337
|
try {
|
|
356
|
-
importResult =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
entry.runtimeUrl,
|
|
362
|
-
);
|
|
338
|
+
importResult = await platformImportBundleFromGcs(
|
|
339
|
+
bundleKey,
|
|
340
|
+
token,
|
|
341
|
+
entry.runtimeUrl,
|
|
342
|
+
);
|
|
363
343
|
} catch (err) {
|
|
364
344
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
365
345
|
console.error("Error: Import request timed out after 5 minutes.");
|
|
@@ -420,13 +400,9 @@ async function restorePlatform(
|
|
|
420
400
|
while (Date.now() < deadline) {
|
|
421
401
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
422
402
|
|
|
423
|
-
let status:
|
|
424
|
-
status: string;
|
|
425
|
-
result?: Record<string, unknown>;
|
|
426
|
-
error?: string;
|
|
427
|
-
};
|
|
403
|
+
let status: Awaited<ReturnType<typeof platformPollJobStatus>>;
|
|
428
404
|
try {
|
|
429
|
-
status = await
|
|
405
|
+
status = await platformPollJobStatus(jobId, token, entry.runtimeUrl);
|
|
430
406
|
} catch (err) {
|
|
431
407
|
const msg = err instanceof Error ? err.message : String(err);
|
|
432
408
|
if (msg.includes("not found")) {
|
|
@@ -451,7 +427,10 @@ async function restorePlatform(
|
|
|
451
427
|
}
|
|
452
428
|
|
|
453
429
|
if (status.status === "complete") {
|
|
454
|
-
importResult = {
|
|
430
|
+
importResult = {
|
|
431
|
+
statusCode: 200,
|
|
432
|
+
body: (status.result as Record<string, unknown>) ?? {},
|
|
433
|
+
};
|
|
455
434
|
break;
|
|
456
435
|
}
|
|
457
436
|
|
package/src/commands/ssh.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
findAssistantByName,
|
|
5
|
-
loadLatestAssistant,
|
|
6
|
-
} from "../lib/assistant-config";
|
|
3
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
7
4
|
import type { AssistantEntry } from "../lib/assistant-config";
|
|
8
5
|
import { dockerResourceNames } from "../lib/docker";
|
|
9
6
|
import { getPlatformUrl, readPlatformToken } from "../lib/platform-client";
|
|
@@ -58,7 +55,7 @@ export async function ssh(): Promise<void> {
|
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
const name = process.argv[3];
|
|
61
|
-
const entry =
|
|
58
|
+
const entry = resolveAssistant(name);
|
|
62
59
|
|
|
63
60
|
if (!entry) {
|
|
64
61
|
if (name) {
|
package/src/commands/teleport.ts
CHANGED
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
getPlatformUrl,
|
|
18
18
|
hatchAssistant,
|
|
19
19
|
checkExistingPlatformAssistant,
|
|
20
|
-
platformInitiateExport,
|
|
21
20
|
platformPollJobStatus,
|
|
22
21
|
platformImportBundleFromGcs,
|
|
23
22
|
platformImportPreflightFromGcs,
|
|
@@ -391,7 +390,7 @@ async function exportFromAssistant(
|
|
|
391
390
|
entry.runtimeUrl,
|
|
392
391
|
entry.assistantId,
|
|
393
392
|
async (token) => {
|
|
394
|
-
const r = await localRuntimeExportToGcs(entry
|
|
393
|
+
const r = await localRuntimeExportToGcs(entry, token, {
|
|
395
394
|
uploadUrl,
|
|
396
395
|
description: "teleport export",
|
|
397
396
|
});
|
|
@@ -418,8 +417,7 @@ async function exportFromAssistant(
|
|
|
418
417
|
|
|
419
418
|
const terminal = await pollJobUntilDone({
|
|
420
419
|
label: "local-runtime export",
|
|
421
|
-
poll: () =>
|
|
422
|
-
localRuntimePollJobStatus(entry.runtimeUrl, accessToken, jobId),
|
|
420
|
+
poll: () => localRuntimePollJobStatus(entry, accessToken, jobId),
|
|
423
421
|
// Large exports can take longer than a guardian-token lease. If the
|
|
424
422
|
// runtime returns 401 mid-poll, re-lease a fresh token and rebind the
|
|
425
423
|
// closure variable so the next poll uses it.
|
|
@@ -442,22 +440,46 @@ async function exportFromAssistant(
|
|
|
442
440
|
}
|
|
443
441
|
|
|
444
442
|
if (cloud === "vellum") {
|
|
445
|
-
// Platform source —
|
|
446
|
-
// the bundle
|
|
447
|
-
//
|
|
448
|
-
|
|
443
|
+
// Platform source — request a signed upload URL on the same platform
|
|
444
|
+
// instance the bundle will eventually be imported from, then ask the
|
|
445
|
+
// managed runtime to export directly to GCS. The runtime endpoint is
|
|
446
|
+
// reached via the platform's wildcard runtime proxy at
|
|
447
|
+
// `/v1/assistants/<id>/migrations/export-to-gcs` — the
|
|
448
|
+
// `localRuntimeExportToGcs` helper uses `resolveRuntimeMigrationUrl` to
|
|
449
|
+
// pick that shape for `cloud === "vellum"` and `migrationRequestHeaders`
|
|
450
|
+
// to send platform-token auth (no guardian-token bootstrap).
|
|
451
|
+
const { url: uploadUrl, bundleKey } = await platformRequestSignedUrl(
|
|
452
|
+
{ operation: "upload" },
|
|
449
453
|
platformToken,
|
|
450
|
-
|
|
451
|
-
entry.runtimeUrl,
|
|
454
|
+
bundlePlatformUrl,
|
|
452
455
|
);
|
|
453
456
|
|
|
457
|
+
let jobId: string;
|
|
458
|
+
let exportPlatformToken = platformToken;
|
|
459
|
+
try {
|
|
460
|
+
({ jobId } = await localRuntimeExportToGcs(entry, exportPlatformToken, {
|
|
461
|
+
uploadUrl,
|
|
462
|
+
description: "teleport export",
|
|
463
|
+
}));
|
|
464
|
+
} catch (err) {
|
|
465
|
+
if (err instanceof MigrationInProgressError) {
|
|
466
|
+
console.error(
|
|
467
|
+
`Error: Another teleport export is already in progress on '${entry.assistantId}' (job ${err.existingJobId}). Wait for it to finish or check its status, then re-run.`,
|
|
468
|
+
);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
|
|
454
474
|
console.log(`Export started (job ${jobId})...`);
|
|
455
475
|
|
|
456
|
-
|
|
476
|
+
// Polling also goes through the wildcard proxy — `localRuntimePollJobStatus`
|
|
477
|
+
// builds `/v1/assistants/<id>/migrations/jobs/<jobId>` for `cloud === "vellum"`
|
|
478
|
+
// (the dedicated `/v1/migrations/jobs/{id}/` endpoint queries platform-side
|
|
479
|
+
// ImportJob records and 404s on runtime-created job IDs).
|
|
457
480
|
const terminal = await pollJobUntilDone({
|
|
458
481
|
label: "platform export",
|
|
459
|
-
poll: () =>
|
|
460
|
-
platformPollJobStatus(jobId, exportPlatformToken, entry.runtimeUrl),
|
|
482
|
+
poll: () => localRuntimePollJobStatus(entry, exportPlatformToken, jobId),
|
|
461
483
|
// The platform token is normally static per-process, but re-reading the
|
|
462
484
|
// on-disk credential covers the case where the user ran `vellum login`
|
|
463
485
|
// in another terminal during a long migration. A persistent 401 after
|
|
@@ -478,14 +500,7 @@ async function exportFromAssistant(
|
|
|
478
500
|
process.exit(1);
|
|
479
501
|
}
|
|
480
502
|
|
|
481
|
-
|
|
482
|
-
console.error(
|
|
483
|
-
"Export completed but the platform did not return a bundle_key. Is the platform up to date?",
|
|
484
|
-
);
|
|
485
|
-
process.exit(1);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return { bundleKey: terminal.bundleKey };
|
|
503
|
+
return { bundleKey };
|
|
489
504
|
}
|
|
490
505
|
|
|
491
506
|
console.error(
|
|
@@ -659,7 +674,7 @@ async function importToAssistant(
|
|
|
659
674
|
entry.runtimeUrl,
|
|
660
675
|
entry.assistantId,
|
|
661
676
|
async (token) => {
|
|
662
|
-
const r = await localRuntimeImportFromGcs(entry
|
|
677
|
+
const r = await localRuntimeImportFromGcs(entry, token, {
|
|
663
678
|
bundleUrl,
|
|
664
679
|
});
|
|
665
680
|
return { jobId: r.jobId, token };
|
|
@@ -682,8 +697,7 @@ async function importToAssistant(
|
|
|
682
697
|
|
|
683
698
|
const terminal = await pollJobUntilDone({
|
|
684
699
|
label: "local-runtime import",
|
|
685
|
-
poll: () =>
|
|
686
|
-
localRuntimePollJobStatus(entry.runtimeUrl, accessToken, jobId),
|
|
700
|
+
poll: () => localRuntimePollJobStatus(entry, accessToken, jobId),
|
|
687
701
|
refreshOn401: async () => {
|
|
688
702
|
accessToken = await getAccessToken(
|
|
689
703
|
entry.runtimeUrl,
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findAssistantByName,
|
|
3
|
-
loadLatestAssistant,
|
|
4
|
-
} from "../lib/assistant-config";
|
|
1
|
+
import { resolveAssistant } from "../lib/assistant-config";
|
|
5
2
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
6
3
|
|
|
7
4
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
@@ -63,9 +60,7 @@ function parseArgs(): TunnelArgs {
|
|
|
63
60
|
export async function tunnel(): Promise<void> {
|
|
64
61
|
const { assistantName, provider } = parseArgs();
|
|
65
62
|
|
|
66
|
-
const entry = assistantName
|
|
67
|
-
? findAssistantByName(assistantName)
|
|
68
|
-
: loadLatestAssistant();
|
|
63
|
+
const entry = resolveAssistant(assistantName ?? undefined);
|
|
69
64
|
|
|
70
65
|
if (!entry) {
|
|
71
66
|
if (assistantName) {
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
2
3
|
|
|
3
4
|
import cliPkg from "../../package.json";
|
|
4
5
|
|
|
@@ -16,7 +17,10 @@ import {
|
|
|
16
17
|
startContainers,
|
|
17
18
|
stopContainers,
|
|
18
19
|
} from "../lib/docker";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
fetchLatestStableVersion,
|
|
22
|
+
resolveImageRefs,
|
|
23
|
+
} from "../lib/platform-releases";
|
|
20
24
|
import {
|
|
21
25
|
authHeaders,
|
|
22
26
|
getPlatformUrl,
|
|
@@ -47,6 +51,7 @@ import { compareVersions } from "../lib/version-compat.js";
|
|
|
47
51
|
interface UpgradeArgs {
|
|
48
52
|
name: string | null;
|
|
49
53
|
version: string | null;
|
|
54
|
+
latest: boolean;
|
|
50
55
|
prepare: boolean;
|
|
51
56
|
finalize: boolean;
|
|
52
57
|
}
|
|
@@ -55,6 +60,7 @@ function parseArgs(): UpgradeArgs {
|
|
|
55
60
|
const args = process.argv.slice(3);
|
|
56
61
|
let name: string | null = null;
|
|
57
62
|
let version: string | null = null;
|
|
63
|
+
let latest = false;
|
|
58
64
|
let prepare = false;
|
|
59
65
|
let finalize = false;
|
|
60
66
|
|
|
@@ -73,7 +79,10 @@ function parseArgs(): UpgradeArgs {
|
|
|
73
79
|
console.log("");
|
|
74
80
|
console.log("Options:");
|
|
75
81
|
console.log(
|
|
76
|
-
" --version <version> Target version to upgrade to (default:
|
|
82
|
+
" --version <version> Target version to upgrade to (default: CLI version)",
|
|
83
|
+
);
|
|
84
|
+
console.log(
|
|
85
|
+
" --latest Upgrade to the latest stable release, updating the CLI first if needed",
|
|
77
86
|
);
|
|
78
87
|
console.log(
|
|
79
88
|
" --prepare Run pre-upgrade steps only (backup, notify) without swapping versions",
|
|
@@ -84,7 +93,10 @@ function parseArgs(): UpgradeArgs {
|
|
|
84
93
|
console.log("");
|
|
85
94
|
console.log("Examples:");
|
|
86
95
|
console.log(
|
|
87
|
-
" vellum upgrade # Upgrade the active assistant to the
|
|
96
|
+
" vellum upgrade # Upgrade the active assistant to the CLI's version",
|
|
97
|
+
);
|
|
98
|
+
console.log(
|
|
99
|
+
" vellum upgrade --latest # Upgrade CLI + assistant to the latest stable release",
|
|
88
100
|
);
|
|
89
101
|
console.log(
|
|
90
102
|
" vellum upgrade my-assistant # Upgrade a specific assistant by name",
|
|
@@ -102,6 +114,8 @@ function parseArgs(): UpgradeArgs {
|
|
|
102
114
|
}
|
|
103
115
|
version = next;
|
|
104
116
|
i++;
|
|
117
|
+
} else if (arg === "--latest") {
|
|
118
|
+
latest = true;
|
|
105
119
|
} else if (arg === "--prepare") {
|
|
106
120
|
prepare = true;
|
|
107
121
|
} else if (arg === "--finalize") {
|
|
@@ -121,7 +135,13 @@ function parseArgs(): UpgradeArgs {
|
|
|
121
135
|
process.exit(1);
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
|
|
138
|
+
if (latest && version) {
|
|
139
|
+
console.error("Error: --latest and --version are mutually exclusive.");
|
|
140
|
+
emitCliError("UNKNOWN", "--latest and --version are mutually exclusive");
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { name, version, latest, prepare, finalize };
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
function resolveCloud(entry: AssistantEntry): string {
|
|
@@ -867,8 +887,80 @@ async function upgradeFinalize(
|
|
|
867
887
|
);
|
|
868
888
|
}
|
|
869
889
|
|
|
890
|
+
/**
|
|
891
|
+
* When `--latest` is passed, resolve the latest stable version from the
|
|
892
|
+
* platform API. If the running CLI is older than that version, self-update
|
|
893
|
+
* the CLI via `bun install -g` and re-exec so the new CLI's upgrade logic
|
|
894
|
+
* (and its cliPkg.version) drives the rest of the upgrade.
|
|
895
|
+
*
|
|
896
|
+
* Returns the resolved latest version string (e.g. "v0.7.0") for callers
|
|
897
|
+
* that need it. If the CLI was updated and re-exec'd, this function never
|
|
898
|
+
* returns — the process is replaced.
|
|
899
|
+
*/
|
|
900
|
+
async function resolveLatestAndMaybeSelfUpdate(
|
|
901
|
+
name: string | null,
|
|
902
|
+
): Promise<string> {
|
|
903
|
+
console.log("🔍 Fetching latest stable release...");
|
|
904
|
+
const latestVersion = await fetchLatestStableVersion();
|
|
905
|
+
if (!latestVersion) {
|
|
906
|
+
console.error(
|
|
907
|
+
"Error: Could not determine the latest stable release from the platform API.",
|
|
908
|
+
);
|
|
909
|
+
emitCliError(
|
|
910
|
+
"UNKNOWN",
|
|
911
|
+
"Could not determine the latest stable release from the platform API",
|
|
912
|
+
);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const latestTag = latestVersion.startsWith("v")
|
|
917
|
+
? latestVersion
|
|
918
|
+
: `v${latestVersion}`;
|
|
919
|
+
const currentTag = cliPkg.version ? `v${cliPkg.version}` : null;
|
|
920
|
+
|
|
921
|
+
console.log(` Latest stable: ${latestTag}`);
|
|
922
|
+
console.log(` CLI version: ${currentTag ?? "unknown"}\n`);
|
|
923
|
+
|
|
924
|
+
// Check if the CLI needs updating
|
|
925
|
+
const cmp = currentTag ? compareVersions(latestTag, currentTag) : null;
|
|
926
|
+
if (cmp !== null && cmp > 0) {
|
|
927
|
+
console.log(`🔄 Updating CLI to ${latestTag}...`);
|
|
928
|
+
const installResult = spawnSync(
|
|
929
|
+
"bun",
|
|
930
|
+
["install", "-g", `vellum@${latestVersion}`],
|
|
931
|
+
{ stdio: "inherit" },
|
|
932
|
+
);
|
|
933
|
+
if (installResult.error || installResult.status !== 0) {
|
|
934
|
+
const detail =
|
|
935
|
+
installResult.error?.message ?? `exited with code ${installResult.status}`;
|
|
936
|
+
console.error(`\n❌ CLI self-update failed: ${detail}`);
|
|
937
|
+
emitCliError("CLI_UPDATE_FAILED", "CLI self-update failed", detail);
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
console.log(`✅ CLI updated to ${latestTag}\n`);
|
|
941
|
+
|
|
942
|
+
// Re-exec with the updated CLI. Pass --version instead of --latest
|
|
943
|
+
// to avoid re-fetching and to prevent infinite re-exec loops.
|
|
944
|
+
const reexecArgs = ["upgrade"];
|
|
945
|
+
if (name) reexecArgs.push(name);
|
|
946
|
+
reexecArgs.push("--version", latestTag);
|
|
947
|
+
|
|
948
|
+
console.log(`🚀 Re-running upgrade with updated CLI...\n`);
|
|
949
|
+
const reexecResult = spawnSync("vellum", reexecArgs, {
|
|
950
|
+
stdio: "inherit",
|
|
951
|
+
});
|
|
952
|
+
process.exit(reexecResult.status ?? 1);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (cmp !== null && cmp === 0) {
|
|
956
|
+
console.log(`✅ CLI is already on the latest version (${latestTag})\n`);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return latestTag;
|
|
960
|
+
}
|
|
961
|
+
|
|
870
962
|
export async function upgrade(): Promise<void> {
|
|
871
|
-
const { name, version, prepare, finalize } = parseArgs();
|
|
963
|
+
const { name, version, latest, prepare, finalize } = parseArgs();
|
|
872
964
|
const entry = resolveTargetAssistant(name);
|
|
873
965
|
|
|
874
966
|
if (prepare) {
|
|
@@ -881,16 +973,25 @@ export async function upgrade(): Promise<void> {
|
|
|
881
973
|
return;
|
|
882
974
|
}
|
|
883
975
|
|
|
976
|
+
// When --latest is passed, resolve the target from the platform API and
|
|
977
|
+
// self-update the CLI if it's behind. The resolved version is then used
|
|
978
|
+
// as the explicit target for the rest of the upgrade flow.
|
|
979
|
+
let effectiveVersion = version;
|
|
980
|
+
if (latest) {
|
|
981
|
+
const latestTag = await resolveLatestAndMaybeSelfUpdate(name);
|
|
982
|
+
effectiveVersion = latestTag;
|
|
983
|
+
}
|
|
984
|
+
|
|
884
985
|
const cloud = resolveCloud(entry);
|
|
885
986
|
|
|
886
987
|
try {
|
|
887
988
|
if (cloud === "docker") {
|
|
888
|
-
await upgradeDocker(entry,
|
|
989
|
+
await upgradeDocker(entry, effectiveVersion);
|
|
889
990
|
return;
|
|
890
991
|
}
|
|
891
992
|
|
|
892
993
|
if (cloud === "vellum") {
|
|
893
|
-
await upgradePlatform(entry,
|
|
994
|
+
await upgradePlatform(entry, effectiveVersion);
|
|
894
995
|
return;
|
|
895
996
|
}
|
|
896
997
|
} catch (err) {
|