@vellumai/cli 0.6.3 → 0.6.4
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 +12 -2
- package/README.md +3 -3
- package/bunfig.toml +6 -0
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +124 -0
- package/src/__tests__/env-drift.test.ts +87 -0
- package/src/__tests__/guardian-token.test.ts +172 -0
- package/src/__tests__/multi-local.test.ts +61 -14
- package/src/__tests__/orphan-detection.test.ts +214 -0
- package/src/__tests__/platform-client.test.ts +204 -0
- package/src/__tests__/preload.ts +27 -0
- package/src/__tests__/ssh-user-guard.test.ts +28 -0
- package/src/__tests__/teleport.test.ts +1073 -56
- package/src/commands/backup.ts +8 -0
- package/src/commands/hatch.ts +1 -1
- package/src/commands/login.ts +178 -9
- package/src/commands/logs.ts +652 -0
- package/src/commands/pair.ts +9 -1
- package/src/commands/ps.ts +37 -7
- package/src/commands/recover.ts +8 -4
- package/src/commands/restore.ts +8 -0
- package/src/commands/retire.ts +16 -9
- package/src/commands/rollback.ts +32 -33
- package/src/commands/ssh-apple-container.ts +162 -0
- package/src/commands/ssh.ts +7 -0
- package/src/commands/teleport.ts +226 -1
- package/src/commands/upgrade.ts +43 -52
- package/src/commands/wake.ts +14 -10
- package/src/components/DefaultMainScreen.tsx +7 -1
- package/src/index.ts +3 -0
- package/src/lib/__tests__/docker.test.ts +78 -0
- package/src/lib/assistant-config.ts +48 -87
- package/src/lib/aws.ts +12 -1
- package/src/lib/constants.ts +0 -10
- package/src/lib/docker.ts +70 -4
- package/src/lib/environments/__tests__/paths.test.ts +234 -0
- package/src/lib/environments/__tests__/resolve.test.ts +226 -0
- package/src/lib/environments/paths.ts +110 -0
- package/src/lib/environments/resolve.ts +96 -0
- package/src/lib/environments/seeds.ts +46 -0
- package/src/lib/environments/types.ts +60 -0
- package/src/lib/gcp.ts +12 -1
- package/src/lib/guardian-token.ts +8 -10
- package/src/lib/hatch-local.ts +24 -19
- package/src/lib/local.ts +46 -5
- package/src/lib/orphan-detection.ts +28 -12
- package/src/lib/platform-client.ts +220 -24
- package/src/lib/retire-apple-container.ts +102 -0
- package/src/lib/upgrade-lifecycle.ts +101 -28
package/src/commands/teleport.ts
CHANGED
|
@@ -9,11 +9,13 @@ import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
|
9
9
|
import {
|
|
10
10
|
loadGuardianToken,
|
|
11
11
|
leaseGuardianToken,
|
|
12
|
+
computeDeviceId,
|
|
12
13
|
} from "../lib/guardian-token.js";
|
|
13
14
|
import {
|
|
14
15
|
readPlatformToken,
|
|
15
16
|
getPlatformUrl,
|
|
16
17
|
hatchAssistant,
|
|
18
|
+
checkExistingPlatformAssistant,
|
|
17
19
|
platformInitiateExport,
|
|
18
20
|
platformPollExportStatus,
|
|
19
21
|
platformDownloadExport,
|
|
@@ -24,6 +26,10 @@ import {
|
|
|
24
26
|
platformImportPreflightFromGcs,
|
|
25
27
|
platformImportBundleFromGcs,
|
|
26
28
|
platformPollImportStatus,
|
|
29
|
+
ensureSelfHostedLocalRegistration,
|
|
30
|
+
injectCredentialsIntoAssistant,
|
|
31
|
+
fetchCurrentUser,
|
|
32
|
+
fetchOrganizationId,
|
|
27
33
|
} from "../lib/platform-client.js";
|
|
28
34
|
import {
|
|
29
35
|
hatchDocker,
|
|
@@ -35,6 +41,8 @@ import { hatchLocal } from "../lib/hatch-local.js";
|
|
|
35
41
|
import { retireLocal } from "../lib/retire-local.js";
|
|
36
42
|
import { validateAssistantName } from "../lib/retire-archive.js";
|
|
37
43
|
import { stopProcessByPidFile } from "../lib/process.js";
|
|
44
|
+
import { fetchCurrentVersion } from "../lib/upgrade-lifecycle.js";
|
|
45
|
+
import { compareVersions } from "../lib/version-compat.js";
|
|
38
46
|
import { join } from "node:path";
|
|
39
47
|
|
|
40
48
|
function printHelp(): void {
|
|
@@ -606,6 +614,13 @@ interface ImportResponse {
|
|
|
606
614
|
files_skipped: number;
|
|
607
615
|
backups_created: number;
|
|
608
616
|
};
|
|
617
|
+
credentialsImported?: {
|
|
618
|
+
total: number;
|
|
619
|
+
succeeded: number;
|
|
620
|
+
failed: number;
|
|
621
|
+
failedAccounts: string[];
|
|
622
|
+
skippedPlatform?: number;
|
|
623
|
+
};
|
|
609
624
|
}
|
|
610
625
|
|
|
611
626
|
async function importToAssistant(
|
|
@@ -895,7 +910,29 @@ export async function resolveOrHatchTarget(
|
|
|
895
910
|
process.exit(1);
|
|
896
911
|
}
|
|
897
912
|
|
|
898
|
-
const result = await hatchAssistant(token);
|
|
913
|
+
const { assistant: result, reusedExisting } = await hatchAssistant(token);
|
|
914
|
+
|
|
915
|
+
// Defensive safety net — should not happen because of the pre-check in
|
|
916
|
+
// teleport(), but guards against a TOCTOU race between the pre-check and
|
|
917
|
+
// hatch (e.g. another client hatches in the GCS-upload window).
|
|
918
|
+
if (reusedExisting) {
|
|
919
|
+
const entry: AssistantEntry = {
|
|
920
|
+
assistantId: result.id,
|
|
921
|
+
runtimeUrl: getPlatformUrl(),
|
|
922
|
+
cloud: "vellum",
|
|
923
|
+
species: "vellum",
|
|
924
|
+
hatchedAt: new Date().toISOString(),
|
|
925
|
+
};
|
|
926
|
+
saveAssistantEntry(entry);
|
|
927
|
+
console.error(
|
|
928
|
+
`Error: You already have a platform assistant '${result.id}'.`,
|
|
929
|
+
);
|
|
930
|
+
console.error(
|
|
931
|
+
`Retire it first with 'vellum retire ${result.id}', then retry the teleport.`,
|
|
932
|
+
);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
|
|
899
936
|
const entry: AssistantEntry = {
|
|
900
937
|
assistantId: result.id,
|
|
901
938
|
runtimeUrl: getPlatformUrl(),
|
|
@@ -1048,6 +1085,20 @@ function printImportSummary(result: ImportResponse): void {
|
|
|
1048
1085
|
console.log(` Files skipped: ${summary.files_skipped}`);
|
|
1049
1086
|
console.log(` Backups created: ${summary.backups_created}`);
|
|
1050
1087
|
|
|
1088
|
+
const creds = result.credentialsImported;
|
|
1089
|
+
if (creds) {
|
|
1090
|
+
console.log(` Credentials imported: ${creds.succeeded}/${creds.total}`);
|
|
1091
|
+
if (creds.skippedPlatform) {
|
|
1092
|
+
console.log(` Platform credentials skipped: ${creds.skippedPlatform}`);
|
|
1093
|
+
}
|
|
1094
|
+
if (creds.failed > 0) {
|
|
1095
|
+
console.log(` Credentials failed: ${creds.failed}`);
|
|
1096
|
+
for (const account of creds.failedAccounts) {
|
|
1097
|
+
console.log(` - ${account}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1051
1102
|
const warnings = result.warnings ?? [];
|
|
1052
1103
|
if (warnings.length > 0) {
|
|
1053
1104
|
console.log("");
|
|
@@ -1058,6 +1109,54 @@ function printImportSummary(result: ImportResponse): void {
|
|
|
1058
1109
|
}
|
|
1059
1110
|
}
|
|
1060
1111
|
|
|
1112
|
+
/**
|
|
1113
|
+
* After teleporting to a local/docker target, register the assistant with
|
|
1114
|
+
* the platform and inject fresh platform credentials — mirroring the
|
|
1115
|
+
* login flow. Non-fatal: failures are logged as warnings.
|
|
1116
|
+
*/
|
|
1117
|
+
async function tryInjectPlatformCredentials(
|
|
1118
|
+
entry: AssistantEntry,
|
|
1119
|
+
): Promise<void> {
|
|
1120
|
+
const token = readPlatformToken();
|
|
1121
|
+
if (!token) {
|
|
1122
|
+
console.log(" Skipped platform credential injection (not logged in).");
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
try {
|
|
1127
|
+
const user = await fetchCurrentUser(token);
|
|
1128
|
+
const orgId = await fetchOrganizationId(token);
|
|
1129
|
+
const clientInstallationId = computeDeviceId();
|
|
1130
|
+
const registration = await ensureSelfHostedLocalRegistration(
|
|
1131
|
+
token,
|
|
1132
|
+
orgId,
|
|
1133
|
+
clientInstallationId,
|
|
1134
|
+
entry.assistantId,
|
|
1135
|
+
"cli",
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
const allInjected = await injectCredentialsIntoAssistant({
|
|
1139
|
+
gatewayUrl: entry.runtimeUrl,
|
|
1140
|
+
bearerToken: entry.bearerToken,
|
|
1141
|
+
assistantApiKey: registration.assistant_api_key,
|
|
1142
|
+
platformAssistantId: registration.assistant.id,
|
|
1143
|
+
platformBaseUrl: getPlatformUrl(),
|
|
1144
|
+
organizationId: orgId,
|
|
1145
|
+
userId: user.id,
|
|
1146
|
+
webhookSecret: registration.webhook_secret,
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
if (allInjected) {
|
|
1150
|
+
console.log(" Platform credentials injected.");
|
|
1151
|
+
} else {
|
|
1152
|
+
console.warn(" Some platform credentials could not be injected.");
|
|
1153
|
+
}
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1156
|
+
console.warn(` Platform credential injection skipped: ${msg}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1061
1160
|
// ---------------------------------------------------------------------------
|
|
1062
1161
|
// Main entry point
|
|
1063
1162
|
// ---------------------------------------------------------------------------
|
|
@@ -1104,6 +1203,13 @@ export async function teleport(): Promise<void> {
|
|
|
1104
1203
|
|
|
1105
1204
|
const fromCloud = resolveCloud(fromEntry);
|
|
1106
1205
|
|
|
1206
|
+
if (fromCloud === "apple-container") {
|
|
1207
|
+
console.error(
|
|
1208
|
+
`Error: '${from}' uses the Apple Containers runtime. Teleport is not yet supported for this topology.`,
|
|
1209
|
+
);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1107
1213
|
// Early same-environment guard — compare source cloud against the CLI flag
|
|
1108
1214
|
// BEFORE exporting or hatching, to avoid creating orphaned assistants.
|
|
1109
1215
|
const normalizedSourceEnv = fromCloud === "vellum" ? "platform" : fromCloud;
|
|
@@ -1137,6 +1243,28 @@ export async function teleport(): Promise<void> {
|
|
|
1137
1243
|
process.exit(1);
|
|
1138
1244
|
}
|
|
1139
1245
|
|
|
1246
|
+
// Version guard: block platform→non-platform when target is behind
|
|
1247
|
+
if (fromCloud === "vellum" && toCloud !== "vellum") {
|
|
1248
|
+
const [sourceVersion, targetVersion] = await Promise.all([
|
|
1249
|
+
fetchCurrentVersion(fromEntry.runtimeUrl),
|
|
1250
|
+
fetchCurrentVersion(existingTarget.runtimeUrl),
|
|
1251
|
+
]);
|
|
1252
|
+
const cmp =
|
|
1253
|
+
sourceVersion && targetVersion
|
|
1254
|
+
? compareVersions(targetVersion, sourceVersion)
|
|
1255
|
+
: null;
|
|
1256
|
+
if (cmp !== null && cmp < 0) {
|
|
1257
|
+
console.error(
|
|
1258
|
+
`Error: Target assistant '${existingTarget.assistantId}' is running ${targetVersion}, ` +
|
|
1259
|
+
`but the platform source is on ${sourceVersion}.`,
|
|
1260
|
+
);
|
|
1261
|
+
console.error(
|
|
1262
|
+
`Upgrade your ${toCloud} assistant first: vellum upgrade ${existingTarget.assistantId}`,
|
|
1263
|
+
);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1140
1268
|
console.log(`Exporting from ${from} (${fromCloud})...`);
|
|
1141
1269
|
const bundleData = await exportFromAssistant(fromEntry, fromCloud);
|
|
1142
1270
|
console.log(`Importing to ${existingTarget.assistantId} (${toCloud})...`);
|
|
@@ -1196,6 +1324,31 @@ export async function teleport(): Promise<void> {
|
|
|
1196
1324
|
// and import hit the same instance.
|
|
1197
1325
|
const targetPlatformUrl = existingTarget?.runtimeUrl;
|
|
1198
1326
|
|
|
1327
|
+
// Step B2 — Pre-check: block if the user already has a platform assistant.
|
|
1328
|
+
// This runs BEFORE the expensive GCS upload so we don't waste bandwidth.
|
|
1329
|
+
if (!existingTarget) {
|
|
1330
|
+
const existing = await checkExistingPlatformAssistant(
|
|
1331
|
+
token,
|
|
1332
|
+
targetPlatformUrl,
|
|
1333
|
+
);
|
|
1334
|
+
if (existing) {
|
|
1335
|
+
saveAssistantEntry({
|
|
1336
|
+
assistantId: existing.id,
|
|
1337
|
+
runtimeUrl: getPlatformUrl(),
|
|
1338
|
+
cloud: "vellum",
|
|
1339
|
+
species: "vellum",
|
|
1340
|
+
hatchedAt: new Date().toISOString(),
|
|
1341
|
+
});
|
|
1342
|
+
console.error(
|
|
1343
|
+
`Error: You already have a platform assistant '${existing.id}'.`,
|
|
1344
|
+
);
|
|
1345
|
+
console.error(
|
|
1346
|
+
`Retire it first with 'vellum retire ${existing.id}', then retry the teleport.`,
|
|
1347
|
+
);
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1199
1352
|
// Step C — Upload to GCS
|
|
1200
1353
|
// bundleKey: string = uploaded successfully, null = tried but unavailable,
|
|
1201
1354
|
// undefined would mean "never tried" (not used here).
|
|
@@ -1238,6 +1391,36 @@ export async function teleport(): Promise<void> {
|
|
|
1238
1391
|
// fails, the user can recover by running `vellum wake <source>`.
|
|
1239
1392
|
const sourceIsLocalOrDocker = fromCloud === "local" || fromCloud === "docker";
|
|
1240
1393
|
const targetIsLocalOrDocker = targetEnv === "local" || targetEnv === "docker";
|
|
1394
|
+
|
|
1395
|
+
// Version guard (pre-hatch): for existing targets, check BEFORE hatching
|
|
1396
|
+
// to avoid creating orphaned assistants when the version check would fail.
|
|
1397
|
+
let versionGuardPassed = false;
|
|
1398
|
+
if (fromCloud === "vellum" && targetIsLocalOrDocker && targetName) {
|
|
1399
|
+
const existingTarget = findAssistantByName(targetName);
|
|
1400
|
+
if (existingTarget) {
|
|
1401
|
+
const [sourceVersion, existingVersion] = await Promise.all([
|
|
1402
|
+
fetchCurrentVersion(fromEntry.runtimeUrl),
|
|
1403
|
+
fetchCurrentVersion(existingTarget.runtimeUrl),
|
|
1404
|
+
]);
|
|
1405
|
+
const cmp =
|
|
1406
|
+
sourceVersion && existingVersion
|
|
1407
|
+
? compareVersions(existingVersion, sourceVersion)
|
|
1408
|
+
: null;
|
|
1409
|
+
if (cmp !== null && cmp < 0) {
|
|
1410
|
+
console.error(
|
|
1411
|
+
`Error: Target assistant '${existingTarget.assistantId}' is running ${existingVersion}, ` +
|
|
1412
|
+
`but the platform source is on ${sourceVersion}.`,
|
|
1413
|
+
);
|
|
1414
|
+
console.error(
|
|
1415
|
+
`Upgrade your ${targetEnv} assistant first: vellum upgrade ${existingTarget.assistantId}`,
|
|
1416
|
+
);
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
// Pre-hatch check passed (or was best-effort skipped) — skip post-hatch
|
|
1420
|
+
versionGuardPassed = true;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1241
1424
|
if (sourceIsLocalOrDocker && targetIsLocalOrDocker && !keepSource) {
|
|
1242
1425
|
console.log(`Stopping source assistant '${from}' to free ports...`);
|
|
1243
1426
|
if (fromCloud === "docker") {
|
|
@@ -1268,10 +1451,52 @@ export async function teleport(): Promise<void> {
|
|
|
1268
1451
|
process.exit(1);
|
|
1269
1452
|
}
|
|
1270
1453
|
|
|
1454
|
+
// Version guard (post-hatch): for newly hatched targets we must check after
|
|
1455
|
+
// hatch because the assistant doesn't exist yet before. If it fails, clean
|
|
1456
|
+
// up the freshly hatched assistant to avoid orphans.
|
|
1457
|
+
// Skip if the pre-hatch guard already ran for an existing target.
|
|
1458
|
+
if (!versionGuardPassed && fromCloud === "vellum" && toCloud !== "vellum") {
|
|
1459
|
+
const [sourceVersion, targetVersion] = await Promise.all([
|
|
1460
|
+
fetchCurrentVersion(fromEntry.runtimeUrl),
|
|
1461
|
+
fetchCurrentVersion(toEntry.runtimeUrl),
|
|
1462
|
+
]);
|
|
1463
|
+
const cmp =
|
|
1464
|
+
sourceVersion && targetVersion
|
|
1465
|
+
? compareVersions(targetVersion, sourceVersion)
|
|
1466
|
+
: null;
|
|
1467
|
+
if (cmp !== null && cmp < 0) {
|
|
1468
|
+
// Clean up the freshly hatched assistant to avoid orphans
|
|
1469
|
+
console.error(
|
|
1470
|
+
`Cleaning up newly hatched assistant '${toEntry.assistantId}'...`,
|
|
1471
|
+
);
|
|
1472
|
+
if (toCloud === "docker") {
|
|
1473
|
+
await retireDocker(toEntry.assistantId);
|
|
1474
|
+
} else {
|
|
1475
|
+
await retireLocal(toEntry.assistantId, toEntry);
|
|
1476
|
+
}
|
|
1477
|
+
removeAssistantEntry(toEntry.assistantId);
|
|
1478
|
+
console.error(
|
|
1479
|
+
`Error: Target assistant '${toEntry.assistantId}' was running ${targetVersion}, ` +
|
|
1480
|
+
`but the platform source is on ${sourceVersion}.`,
|
|
1481
|
+
);
|
|
1482
|
+
console.error(
|
|
1483
|
+
`Upgrade your ${toCloud} environment first, then retry the teleport.`,
|
|
1484
|
+
);
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1271
1489
|
// Import to target
|
|
1272
1490
|
console.log(`Importing to ${toEntry.assistantId} (${toCloud})...`);
|
|
1273
1491
|
await importToAssistant(toEntry, toCloud, bundleData, false);
|
|
1274
1492
|
|
|
1493
|
+
// After successful import, inject fresh platform credentials if the
|
|
1494
|
+
// user is logged in — replaces the source's stale vellum:* credentials
|
|
1495
|
+
// that were filtered during import.
|
|
1496
|
+
if (fromCloud === "vellum") {
|
|
1497
|
+
await tryInjectPlatformCredentials(toEntry);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1275
1500
|
// Retire source after successful import
|
|
1276
1501
|
if (sourceIsLocalOrDocker && targetIsLocalOrDocker) {
|
|
1277
1502
|
if (!keepSource) {
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -189,18 +189,9 @@ async function upgradeDocker(
|
|
|
189
189
|
const versionTag =
|
|
190
190
|
version ?? (cliPkg.version ? `v${cliPkg.version}` : "latest");
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
if (currentVersion && versionTag) {
|
|
196
|
-
const cmp = compareVersions(versionTag, currentVersion);
|
|
197
|
-
if (cmp !== null && cmp < 0) {
|
|
198
|
-
const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`vellum rollback --version ${versionTag}\` instead.`;
|
|
199
|
-
console.error(msg);
|
|
200
|
-
emitCliError("VERSION_DIRECTION", msg);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
192
|
+
// Fetch the current running version from the health endpoint.
|
|
193
|
+
// This is used for logging, commit messages, and version-direction guards.
|
|
194
|
+
let currentVersion: string | undefined;
|
|
204
195
|
|
|
205
196
|
console.log("🔍 Resolving image references...");
|
|
206
197
|
const { imageTags } = await resolveImageRefs(versionTag);
|
|
@@ -225,7 +216,7 @@ async function upgradeDocker(
|
|
|
225
216
|
);
|
|
226
217
|
}
|
|
227
218
|
|
|
228
|
-
// Capture current migration state for rollback targeting.
|
|
219
|
+
// Capture current migration state and running version for rollback targeting.
|
|
229
220
|
// Must happen while daemon is still running (before containers are stopped).
|
|
230
221
|
let preMigrationState: {
|
|
231
222
|
dbVersion?: number;
|
|
@@ -240,26 +231,47 @@ async function upgradeDocker(
|
|
|
240
231
|
);
|
|
241
232
|
if (healthResp.ok) {
|
|
242
233
|
const health = (await healthResp.json()) as {
|
|
234
|
+
version?: string;
|
|
243
235
|
migrations?: { dbVersion?: number; lastWorkspaceMigrationId?: string };
|
|
244
236
|
};
|
|
245
237
|
preMigrationState = health.migrations ?? {};
|
|
238
|
+
currentVersion = health.version;
|
|
246
239
|
}
|
|
247
240
|
} catch {
|
|
248
241
|
// Best-effort — if we can't get migration state, rollback will skip migration reversal
|
|
249
242
|
}
|
|
250
243
|
|
|
244
|
+
// Reject downgrades — `vellum upgrade` only handles forward version changes.
|
|
245
|
+
// Users should use `vellum rollback --version <version>` for downgrades.
|
|
246
|
+
if (!currentVersion && versionTag) {
|
|
247
|
+
console.warn(
|
|
248
|
+
"⚠️ Could not determine current version from health endpoint — skipping version-direction check.\n",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (currentVersion && versionTag) {
|
|
252
|
+
const cmp = compareVersions(versionTag, currentVersion);
|
|
253
|
+
if (cmp !== null && cmp < 0) {
|
|
254
|
+
const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`vellum rollback --version ${versionTag}\` instead.`;
|
|
255
|
+
console.error(msg);
|
|
256
|
+
emitCliError("VERSION_DIRECTION", msg);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
251
261
|
// Persist rollback state to lockfile BEFORE any destructive changes.
|
|
252
262
|
// This enables the `vellum rollback` command to restore the previous version.
|
|
253
|
-
if (entry.
|
|
263
|
+
if (entry.containerInfo) {
|
|
254
264
|
const rollbackEntry: AssistantEntry = {
|
|
255
265
|
...entry,
|
|
256
|
-
previousServiceGroupVersion: entry.serviceGroupVersion,
|
|
257
266
|
previousContainerInfo: { ...entry.containerInfo },
|
|
267
|
+
previousVersion: currentVersion,
|
|
258
268
|
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
259
269
|
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
260
270
|
};
|
|
261
271
|
saveAssistantEntry(rollbackEntry);
|
|
262
|
-
|
|
272
|
+
if (currentVersion) {
|
|
273
|
+
console.log(` Saved rollback state: ${currentVersion}\n`);
|
|
274
|
+
}
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
// Record version transition start in workspace git history
|
|
@@ -269,7 +281,7 @@ async function upgradeDocker(
|
|
|
269
281
|
buildUpgradeCommitMessage({
|
|
270
282
|
action: "upgrade",
|
|
271
283
|
phase: "starting",
|
|
272
|
-
from:
|
|
284
|
+
from: currentVersion ?? "unknown",
|
|
273
285
|
to: versionTag,
|
|
274
286
|
topology: "docker",
|
|
275
287
|
assistantId: entry.assistantId,
|
|
@@ -321,7 +333,7 @@ async function upgradeDocker(
|
|
|
321
333
|
await broadcastUpgradeEvent(
|
|
322
334
|
entry.runtimeUrl,
|
|
323
335
|
entry.assistantId,
|
|
324
|
-
buildCompleteEvent(
|
|
336
|
+
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
325
337
|
);
|
|
326
338
|
emitCliError("IMAGE_PULL_FAILED", "Failed to pull Docker images", detail);
|
|
327
339
|
process.exit(1);
|
|
@@ -361,7 +373,7 @@ async function upgradeDocker(
|
|
|
361
373
|
console.log("📦 Creating pre-upgrade backup...");
|
|
362
374
|
const backupPath = await createBackup(entry.runtimeUrl, entry.assistantId, {
|
|
363
375
|
prefix: `${entry.assistantId}-pre-upgrade`,
|
|
364
|
-
description: `Pre-upgrade snapshot before ${
|
|
376
|
+
description: `Pre-upgrade snapshot before ${currentVersion ?? "unknown"} → ${versionTag}`,
|
|
365
377
|
});
|
|
366
378
|
if (backupPath) {
|
|
367
379
|
console.log(` Backup saved: ${backupPath}\n`);
|
|
@@ -434,7 +446,6 @@ async function upgradeDocker(
|
|
|
434
446
|
const newDigests = await captureImageRefs(res);
|
|
435
447
|
const updatedEntry: AssistantEntry = {
|
|
436
448
|
...entry,
|
|
437
|
-
serviceGroupVersion: versionTag,
|
|
438
449
|
containerInfo: {
|
|
439
450
|
assistantImage: imageTags.assistant,
|
|
440
451
|
gatewayImage: imageTags.gateway,
|
|
@@ -444,7 +455,6 @@ async function upgradeDocker(
|
|
|
444
455
|
cesDigest: newDigests?.["credential-executor"],
|
|
445
456
|
networkName: res.network,
|
|
446
457
|
},
|
|
447
|
-
previousServiceGroupVersion: entry.serviceGroupVersion,
|
|
448
458
|
previousContainerInfo: entry.containerInfo,
|
|
449
459
|
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
450
460
|
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
@@ -467,7 +477,7 @@ async function upgradeDocker(
|
|
|
467
477
|
buildUpgradeCommitMessage({
|
|
468
478
|
action: "upgrade",
|
|
469
479
|
phase: "complete",
|
|
470
|
-
from:
|
|
480
|
+
from: currentVersion ?? "unknown",
|
|
471
481
|
to: versionTag,
|
|
472
482
|
topology: "docker",
|
|
473
483
|
assistantId: entry.assistantId,
|
|
@@ -584,7 +594,6 @@ async function upgradeDocker(
|
|
|
584
594
|
previousImageRefs["credential-executor"],
|
|
585
595
|
networkName: res.network,
|
|
586
596
|
},
|
|
587
|
-
previousServiceGroupVersion: undefined,
|
|
588
597
|
previousContainerInfo: undefined,
|
|
589
598
|
previousDbMigrationVersion: undefined,
|
|
590
599
|
previousWorkspaceMigrationId: undefined,
|
|
@@ -598,9 +607,9 @@ async function upgradeDocker(
|
|
|
598
607
|
entry.runtimeUrl,
|
|
599
608
|
entry.assistantId,
|
|
600
609
|
buildCompleteEvent(
|
|
601
|
-
|
|
610
|
+
currentVersion ?? "unknown",
|
|
602
611
|
false,
|
|
603
|
-
|
|
612
|
+
currentVersion,
|
|
604
613
|
),
|
|
605
614
|
);
|
|
606
615
|
|
|
@@ -621,7 +630,7 @@ async function upgradeDocker(
|
|
|
621
630
|
await broadcastUpgradeEvent(
|
|
622
631
|
entry.runtimeUrl,
|
|
623
632
|
entry.assistantId,
|
|
624
|
-
buildCompleteEvent(
|
|
633
|
+
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
625
634
|
);
|
|
626
635
|
emitCliError(
|
|
627
636
|
"ROLLBACK_FAILED",
|
|
@@ -641,7 +650,7 @@ async function upgradeDocker(
|
|
|
641
650
|
await broadcastUpgradeEvent(
|
|
642
651
|
entry.runtimeUrl,
|
|
643
652
|
entry.assistantId,
|
|
644
|
-
buildCompleteEvent(
|
|
653
|
+
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
645
654
|
);
|
|
646
655
|
emitCliError(
|
|
647
656
|
"ROLLBACK_FAILED",
|
|
@@ -657,7 +666,7 @@ async function upgradeDocker(
|
|
|
657
666
|
await broadcastUpgradeEvent(
|
|
658
667
|
entry.runtimeUrl,
|
|
659
668
|
entry.assistantId,
|
|
660
|
-
buildCompleteEvent(
|
|
669
|
+
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
661
670
|
);
|
|
662
671
|
emitCliError(
|
|
663
672
|
"ROLLBACK_NO_STATE",
|
|
@@ -678,22 +687,6 @@ async function upgradePlatform(
|
|
|
678
687
|
entry: AssistantEntry,
|
|
679
688
|
version: string | null,
|
|
680
689
|
): Promise<void> {
|
|
681
|
-
// Reject downgrades — `vellum upgrade` only handles forward version changes.
|
|
682
|
-
// Users should use `vellum rollback --version <version>` for downgrades.
|
|
683
|
-
// Only enforce this guard when the user explicitly passed `--version`.
|
|
684
|
-
// When version is null the platform API decides the actual target, so
|
|
685
|
-
// we must not block the request based on the local CLI version.
|
|
686
|
-
const currentVersion = entry.serviceGroupVersion;
|
|
687
|
-
if (version && currentVersion) {
|
|
688
|
-
const cmp = compareVersions(version, currentVersion);
|
|
689
|
-
if (cmp !== null && cmp < 0) {
|
|
690
|
-
const msg = `Cannot upgrade to an older version (${version} < ${currentVersion}). Use \`vellum rollback --version ${version}\` instead.`;
|
|
691
|
-
console.error(msg);
|
|
692
|
-
emitCliError("VERSION_DIRECTION", msg);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
690
|
console.log(
|
|
698
691
|
`🔄 Upgrading platform-hosted assistant '${entry.assistantId}'...\n`,
|
|
699
692
|
);
|
|
@@ -733,7 +726,7 @@ async function upgradePlatform(
|
|
|
733
726
|
await broadcastUpgradeEvent(
|
|
734
727
|
entry.runtimeUrl,
|
|
735
728
|
entry.assistantId,
|
|
736
|
-
buildCompleteEvent(
|
|
729
|
+
buildCompleteEvent("unknown", false),
|
|
737
730
|
);
|
|
738
731
|
} catch {
|
|
739
732
|
// Best-effort — broadcast may fail if the assistant is unreachable
|
|
@@ -755,7 +748,7 @@ async function upgradePlatform(
|
|
|
755
748
|
await broadcastUpgradeEvent(
|
|
756
749
|
entry.runtimeUrl,
|
|
757
750
|
entry.assistantId,
|
|
758
|
-
buildCompleteEvent(
|
|
751
|
+
buildCompleteEvent("unknown", false),
|
|
759
752
|
);
|
|
760
753
|
} catch {
|
|
761
754
|
// Best-effort — broadcast may fail if the assistant is unreachable
|
|
@@ -788,8 +781,8 @@ async function upgradePrepare(
|
|
|
788
781
|
entry: AssistantEntry,
|
|
789
782
|
version: string | null,
|
|
790
783
|
): Promise<void> {
|
|
791
|
-
const targetVersion = version ??
|
|
792
|
-
const currentVersion =
|
|
784
|
+
const targetVersion = version ?? "unknown";
|
|
785
|
+
const currentVersion = "unknown";
|
|
793
786
|
|
|
794
787
|
// 1. Broadcast "starting" so the UI shows the progress spinner
|
|
795
788
|
await broadcastUpgradeEvent(
|
|
@@ -857,9 +850,7 @@ async function upgradeFinalize(
|
|
|
857
850
|
}
|
|
858
851
|
|
|
859
852
|
const fromVersion = version;
|
|
860
|
-
const currentVersion = cliPkg.version
|
|
861
|
-
? `v${cliPkg.version}`
|
|
862
|
-
: (entry.serviceGroupVersion ?? "unknown");
|
|
853
|
+
const currentVersion = cliPkg.version ? `v${cliPkg.version}` : "unknown";
|
|
863
854
|
|
|
864
855
|
// 1. Broadcast "complete" so the UI clears the progress spinner
|
|
865
856
|
await broadcastUpgradeEvent(
|
|
@@ -911,7 +902,7 @@ export async function upgrade(): Promise<void> {
|
|
|
911
902
|
await broadcastUpgradeEvent(
|
|
912
903
|
entry.runtimeUrl,
|
|
913
904
|
entry.assistantId,
|
|
914
|
-
buildCompleteEvent(
|
|
905
|
+
buildCompleteEvent("unknown", false),
|
|
915
906
|
);
|
|
916
907
|
emitCliError(categorizeUpgradeError(err), "Upgrade failed", detail);
|
|
917
908
|
process.exit(1);
|
package/src/commands/wake.ts
CHANGED
|
@@ -183,18 +183,22 @@ export async function wake(): Promise<void> {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// Auto-start ngrok if webhook integrations (e.g. Telegram) are configured.
|
|
186
|
-
//
|
|
186
|
+
// Scope BASE_DATA_DIR to the woken instance so ngrok reads the correct
|
|
187
|
+
// instance config, then restore on any exit path.
|
|
187
188
|
const prevBaseDataDir = process.env.BASE_DATA_DIR;
|
|
188
189
|
process.env.BASE_DATA_DIR = resources.instanceDir;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
try {
|
|
191
|
+
const ngrokChild = await maybeStartNgrokTunnel(resources.gatewayPort);
|
|
192
|
+
if (ngrokChild?.pid) {
|
|
193
|
+
const ngrokPidFile = join(resources.instanceDir, ".vellum", "ngrok.pid");
|
|
194
|
+
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
if (prevBaseDataDir !== undefined) {
|
|
198
|
+
process.env.BASE_DATA_DIR = prevBaseDataDir;
|
|
199
|
+
} else {
|
|
200
|
+
delete process.env.BASE_DATA_DIR;
|
|
201
|
+
}
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
console.log("Wake complete.");
|
|
@@ -1939,8 +1939,14 @@ function ChatApp({
|
|
|
1939
1939
|
);
|
|
1940
1940
|
}
|
|
1941
1941
|
|
|
1942
|
+
let username: string;
|
|
1943
|
+
try {
|
|
1944
|
+
username = userInfo().username;
|
|
1945
|
+
} catch {
|
|
1946
|
+
username = "";
|
|
1947
|
+
}
|
|
1942
1948
|
const hostId = createHash("sha256")
|
|
1943
|
-
.update(hostname() +
|
|
1949
|
+
.update(hostname() + username)
|
|
1944
1950
|
.digest("hex");
|
|
1945
1951
|
const payload = JSON.stringify({
|
|
1946
1952
|
type: "vellum-assistant",
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { client } from "./commands/client";
|
|
|
7
7
|
import { events } from "./commands/events";
|
|
8
8
|
import { hatch } from "./commands/hatch";
|
|
9
9
|
import { login, logout, whoami } from "./commands/login";
|
|
10
|
+
import { logs } from "./commands/logs";
|
|
10
11
|
import { message } from "./commands/message";
|
|
11
12
|
import { pair } from "./commands/pair";
|
|
12
13
|
import { ps } from "./commands/ps";
|
|
@@ -39,6 +40,7 @@ const commands = {
|
|
|
39
40
|
hatch,
|
|
40
41
|
login,
|
|
41
42
|
logout,
|
|
43
|
+
logs,
|
|
42
44
|
message,
|
|
43
45
|
pair,
|
|
44
46
|
ps,
|
|
@@ -68,6 +70,7 @@ function printHelp(): void {
|
|
|
68
70
|
console.log(" client Connect to a hatched assistant");
|
|
69
71
|
console.log(" events Stream events from a running assistant");
|
|
70
72
|
console.log(" hatch Create a new assistant instance");
|
|
73
|
+
console.log(" logs View logs from an assistant instance");
|
|
71
74
|
console.log(" login Log in to the Vellum platform");
|
|
72
75
|
console.log(" logout Log out of the Vellum platform");
|
|
73
76
|
console.log(" message Send a message to a running assistant");
|