@vellumai/cli 0.6.2 ā 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 -57
- package/src/commands/backup.ts +8 -0
- package/src/commands/hatch.ts +5 -28
- 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 +124 -12
- package/src/commands/retire.ts +17 -3
- package/src/commands/rollback.ts +32 -33
- package/src/commands/sleep.ts +7 -0
- package/src/commands/ssh-apple-container.ts +162 -0
- package/src/commands/ssh.ts +7 -0
- package/src/commands/teleport.ts +307 -3
- package/src/commands/upgrade.ts +43 -52
- package/src/commands/wake.ts +21 -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 +54 -87
- package/src/lib/aws.ts +12 -1
- package/src/lib/constants.ts +0 -10
- package/src/lib/docker.ts +73 -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 +30 -35
- package/src/lib/local.ts +46 -5
- package/src/lib/orphan-detection.ts +28 -12
- package/src/lib/platform-client.ts +261 -25
- package/src/lib/retire-apple-container.ts +102 -0
- package/src/lib/upgrade-lifecycle.ts +101 -28
package/src/commands/ps.ts
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
findAssistantByName,
|
|
5
5
|
getActiveAssistant,
|
|
6
6
|
loadAllAssistants,
|
|
7
|
-
updateServiceGroupVersion,
|
|
8
7
|
type AssistantEntry,
|
|
9
8
|
} from "../lib/assistant-config";
|
|
10
9
|
import { loadGuardianToken } from "../lib/guardian-token";
|
|
11
10
|
import { checkHealth, checkManagedHealth } from "../lib/health-check";
|
|
12
11
|
import { dockerResourceNames } from "../lib/docker";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
13
|
import {
|
|
14
14
|
classifyProcess,
|
|
15
15
|
detectOrphanedProcesses,
|
|
@@ -335,6 +335,31 @@ async function showAssistantProcesses(entry: AssistantEntry): Promise<void> {
|
|
|
335
335
|
return;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
if (cloud === "apple-container") {
|
|
339
|
+
const mgmtSocket = entry.mgmtSocket as string | undefined;
|
|
340
|
+
const socketAlive = mgmtSocket ? existsSync(mgmtSocket) : false;
|
|
341
|
+
const rows: TableRow[] = [
|
|
342
|
+
{
|
|
343
|
+
name: "container",
|
|
344
|
+
status: withStatusEmoji(socketAlive ? "running" : "not running"),
|
|
345
|
+
info: socketAlive
|
|
346
|
+
? `mgmt ${mgmtSocket}`
|
|
347
|
+
: "management socket not found",
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
if (entry.runtimeUrl) {
|
|
351
|
+
const token = loadGuardianToken(entry.assistantId)?.accessToken;
|
|
352
|
+
const health = await checkHealth(entry.runtimeUrl, token);
|
|
353
|
+
rows.push({
|
|
354
|
+
name: "gateway",
|
|
355
|
+
status: withStatusEmoji(health.status),
|
|
356
|
+
info: entry.runtimeUrl + (health.detail ? ` | ${health.detail}` : ""),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
printTable(rows);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
338
363
|
let output: string;
|
|
339
364
|
try {
|
|
340
365
|
if (cloud === "gcp") {
|
|
@@ -395,7 +420,8 @@ async function listAllAssistants(): Promise<void> {
|
|
|
395
420
|
}
|
|
396
421
|
|
|
397
422
|
const rows: TableRow[] = assistants.map((a) => {
|
|
398
|
-
const infoParts = [
|
|
423
|
+
const infoParts: string[] = [];
|
|
424
|
+
if (a.runtimeUrl) infoParts.push(a.runtimeUrl);
|
|
399
425
|
if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
|
|
400
426
|
if (a.species) infoParts.push(`species: ${a.species}`);
|
|
401
427
|
const prefix = a.assistantId === activeId ? "* " : " ";
|
|
@@ -445,6 +471,13 @@ async function listAllAssistants(): Promise<void> {
|
|
|
445
471
|
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
446
472
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
447
473
|
}
|
|
474
|
+
} else if (a.cloud === "apple-container") {
|
|
475
|
+
// Apple containers are managed by the macOS app. Probe the gateway
|
|
476
|
+
// (runtimeUrl is always written to the lockfile during hatch).
|
|
477
|
+
const token = loadGuardianToken(a.assistantId)?.accessToken;
|
|
478
|
+
health = a.runtimeUrl
|
|
479
|
+
? await checkHealth(a.runtimeUrl, token)
|
|
480
|
+
: { status: "unknown" as const, detail: "no runtime URL" };
|
|
448
481
|
} else if (a.cloud === "vellum") {
|
|
449
482
|
health = await checkManagedHealth(a.runtimeUrl, a.assistantId);
|
|
450
483
|
} else {
|
|
@@ -452,11 +485,8 @@ async function listAllAssistants(): Promise<void> {
|
|
|
452
485
|
health = await checkHealth(a.localUrl ?? a.runtimeUrl, token);
|
|
453
486
|
}
|
|
454
487
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const infoParts = [a.runtimeUrl];
|
|
488
|
+
const infoParts: string[] = [];
|
|
489
|
+
if (a.runtimeUrl) infoParts.push(a.runtimeUrl);
|
|
460
490
|
if (a.cloud) infoParts.push(`cloud: ${a.cloud}`);
|
|
461
491
|
if (a.species) infoParts.push(`species: ${a.species}`);
|
|
462
492
|
if (health.detail) infoParts.push(health.detail);
|
package/src/commands/recover.ts
CHANGED
|
@@ -51,16 +51,20 @@ export async function recover(): Promise<void> {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// 3. Check
|
|
55
|
-
const
|
|
56
|
-
if (existsSync(
|
|
54
|
+
// 3. Check that the recovering entry's own target directory is free.
|
|
55
|
+
const target = join(entry.resources.instanceDir, ".vellum");
|
|
56
|
+
if (existsSync(target)) {
|
|
57
57
|
console.error(
|
|
58
|
-
|
|
58
|
+
`Error: ${target} already exists (owned by ${entry.assistantId}). ` +
|
|
59
|
+
`Retire the current assistant first.`,
|
|
59
60
|
);
|
|
60
61
|
process.exit(1);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// 4. Extract archive
|
|
65
|
+
// TODO: extraction target is hardcoded to homedir(); multi-instance entries
|
|
66
|
+
// whose instanceDir differs from homedir will extract to the wrong
|
|
67
|
+
// location. Tracked separately from the collision-check regression.
|
|
64
68
|
await exec("tar", ["xzf", archivePath, "-C", homedir()]);
|
|
65
69
|
|
|
66
70
|
// 5. Restore lockfile entry
|
package/src/commands/restore.ts
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
rollbackPlatformAssistant,
|
|
12
12
|
platformImportPreflight,
|
|
13
13
|
platformImportBundle,
|
|
14
|
+
platformRequestUploadUrl,
|
|
15
|
+
platformUploadToSignedUrl,
|
|
16
|
+
platformImportPreflightFromGcs,
|
|
17
|
+
platformImportBundleFromGcs,
|
|
18
|
+
platformPollImportStatus,
|
|
14
19
|
} from "../lib/platform-client.js";
|
|
15
20
|
import { performDockerRollback } from "../lib/upgrade-lifecycle.js";
|
|
16
21
|
|
|
@@ -176,6 +181,25 @@ async function restorePlatform(
|
|
|
176
181
|
process.exit(1);
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
// Step 1.5 ā Upload to GCS via signed URL (with fallback to inline)
|
|
185
|
+
let bundleKey: string | null = null;
|
|
186
|
+
try {
|
|
187
|
+
const { uploadUrl, bundleKey: key } = await platformRequestUploadUrl(
|
|
188
|
+
token,
|
|
189
|
+
entry.runtimeUrl,
|
|
190
|
+
);
|
|
191
|
+
bundleKey = key;
|
|
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
|
+
}
|
|
202
|
+
|
|
179
203
|
// Step 2 ā Dry-run path
|
|
180
204
|
if (opts.dryRun) {
|
|
181
205
|
if (opts.version) {
|
|
@@ -189,11 +213,17 @@ async function restorePlatform(
|
|
|
189
213
|
|
|
190
214
|
let preflightResult: { statusCode: number; body: Record<string, unknown> };
|
|
191
215
|
try {
|
|
192
|
-
preflightResult =
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
preflightResult = bundleKey
|
|
217
|
+
? await platformImportPreflightFromGcs(
|
|
218
|
+
bundleKey,
|
|
219
|
+
token,
|
|
220
|
+
entry.runtimeUrl,
|
|
221
|
+
)
|
|
222
|
+
: await platformImportPreflight(
|
|
223
|
+
new Uint8Array(bundleData),
|
|
224
|
+
token,
|
|
225
|
+
entry.runtimeUrl,
|
|
226
|
+
);
|
|
197
227
|
} catch (err) {
|
|
198
228
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
199
229
|
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
@@ -323,14 +353,16 @@ async function restorePlatform(
|
|
|
323
353
|
|
|
324
354
|
let importResult: { statusCode: number; body: Record<string, unknown> };
|
|
325
355
|
try {
|
|
326
|
-
importResult =
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
356
|
+
importResult = bundleKey
|
|
357
|
+
? await platformImportBundleFromGcs(bundleKey, token, entry.runtimeUrl)
|
|
358
|
+
: await platformImportBundle(
|
|
359
|
+
new Uint8Array(bundleData),
|
|
360
|
+
token,
|
|
361
|
+
entry.runtimeUrl,
|
|
362
|
+
);
|
|
331
363
|
} catch (err) {
|
|
332
364
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
333
|
-
console.error("Error: Import request timed out after
|
|
365
|
+
console.error("Error: Import request timed out after 5 minutes.");
|
|
334
366
|
process.exit(1);
|
|
335
367
|
}
|
|
336
368
|
throw err;
|
|
@@ -364,11 +396,83 @@ async function restorePlatform(
|
|
|
364
396
|
process.exit(1);
|
|
365
397
|
}
|
|
366
398
|
|
|
367
|
-
if (
|
|
399
|
+
if (
|
|
400
|
+
importResult.statusCode !== 202 &&
|
|
401
|
+
(importResult.statusCode < 200 || importResult.statusCode >= 300)
|
|
402
|
+
) {
|
|
368
403
|
console.error(`Error: Import failed (${importResult.statusCode})`);
|
|
369
404
|
process.exit(1);
|
|
370
405
|
}
|
|
371
406
|
|
|
407
|
+
// Async import ā poll until complete
|
|
408
|
+
if (importResult.statusCode === 202) {
|
|
409
|
+
const jobId = (importResult.body as { job_id?: string }).job_id;
|
|
410
|
+
if (!jobId) {
|
|
411
|
+
console.error("Error: Import accepted but no job ID returned.");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
416
|
+
const TIMEOUT_MS = 10 * 60 * 1_000; // 10 minutes
|
|
417
|
+
const startTime = Date.now();
|
|
418
|
+
const deadline = startTime + TIMEOUT_MS;
|
|
419
|
+
|
|
420
|
+
while (Date.now() < deadline) {
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
422
|
+
|
|
423
|
+
let status: {
|
|
424
|
+
status: string;
|
|
425
|
+
result?: Record<string, unknown>;
|
|
426
|
+
error?: string;
|
|
427
|
+
};
|
|
428
|
+
try {
|
|
429
|
+
status = await platformPollImportStatus(jobId, token, entry.runtimeUrl);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
432
|
+
if (msg.includes("not found")) {
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
// Fail fast on auth errors from authHeaders() which don't
|
|
436
|
+
// match the "status check failed: NNN" format
|
|
437
|
+
if (msg.includes("401") || msg.includes("403")) {
|
|
438
|
+
throw err;
|
|
439
|
+
}
|
|
440
|
+
// Re-throw permanent 4xx errors, retry transient 5xx
|
|
441
|
+
const statusMatch = msg.match(/status check failed: (\d+)/);
|
|
442
|
+
if (statusMatch) {
|
|
443
|
+
const statusCode = parseInt(statusMatch[1], 10);
|
|
444
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Transient error (5xx, network) ā retry
|
|
449
|
+
console.warn(`Polling failed, retrying... (${msg})`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (status.status === "complete") {
|
|
454
|
+
importResult = { statusCode: 200, body: status.result ?? {} };
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (status.status === "failed") {
|
|
459
|
+
console.error(`Import failed: ${status.error ?? "unknown error"}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
464
|
+
process.stdout.write(`\rImporting... ${elapsed}s elapsed`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Clear the progress line
|
|
468
|
+
process.stdout.write("\r" + " ".repeat(40) + "\r");
|
|
469
|
+
|
|
470
|
+
if (importResult.statusCode === 202) {
|
|
471
|
+
console.error("Import timed out after 10 minutes.");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
372
476
|
const result = importResult.body as unknown as ImportResponse;
|
|
373
477
|
|
|
374
478
|
if (!result.success) {
|
|
@@ -459,6 +563,14 @@ export async function restore(): Promise<void> {
|
|
|
459
563
|
// Detect topology and route platform assistants through Django import
|
|
460
564
|
const cloud =
|
|
461
565
|
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
|
|
566
|
+
|
|
567
|
+
if (cloud === "apple-container") {
|
|
568
|
+
console.error(
|
|
569
|
+
`Error: '${name}' uses the Apple Containers runtime. Restore is not yet supported for this topology.`,
|
|
570
|
+
);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
462
574
|
if (cloud === "vellum") {
|
|
463
575
|
await restorePlatform(entry, name, bundleData, { version, dryRun });
|
|
464
576
|
return;
|
package/src/commands/retire.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { retireInstance as retireAwsInstance } from "../lib/aws";
|
|
|
12
12
|
import { retireDocker } from "../lib/docker";
|
|
13
13
|
import { retireInstance as retireGcpInstance } from "../lib/gcp";
|
|
14
14
|
import { retireLocal } from "../lib/retire-local";
|
|
15
|
+
import { retireAppleContainer } from "../lib/retire-apple-container";
|
|
15
16
|
import { exec } from "../lib/step-runner";
|
|
16
17
|
import {
|
|
17
18
|
openLogFile,
|
|
@@ -100,7 +101,12 @@ async function retireVellum(
|
|
|
100
101
|
headers: await authHeaders(token, runtimeUrl),
|
|
101
102
|
});
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
// Treat 404 as success: the assistant is already gone from the platform
|
|
105
|
+
// (previously retired, deleted from the web UI, or retired from another
|
|
106
|
+
// device) so the caller's job is done. Falling through to the lockfile
|
|
107
|
+
// cleanup avoids leaving a stale entry that would otherwise wedge the
|
|
108
|
+
// macOS app in a permanent health-check loop.
|
|
109
|
+
if (!response.ok && response.status !== 404) {
|
|
104
110
|
const body = await response.text();
|
|
105
111
|
console.error(
|
|
106
112
|
`Error: Platform retire failed (${response.status}): ${body}`,
|
|
@@ -108,7 +114,13 @@ async function retireVellum(
|
|
|
108
114
|
process.exit(1);
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
117
|
+
if (response.status === 404) {
|
|
118
|
+
console.log(
|
|
119
|
+
"\u2705 Platform-hosted instance already retired (404) ā cleaning up local state.",
|
|
120
|
+
);
|
|
121
|
+
} else {
|
|
122
|
+
console.log("\u2705 Platform-hosted instance retired.");
|
|
123
|
+
}
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
function parseSource(): string | undefined {
|
|
@@ -200,7 +212,9 @@ async function retireInner(): Promise<void> {
|
|
|
200
212
|
const source = parseSource();
|
|
201
213
|
const cloud = resolveCloud(entry);
|
|
202
214
|
|
|
203
|
-
if (cloud === "
|
|
215
|
+
if (cloud === "apple-container") {
|
|
216
|
+
await retireAppleContainer(name, entry);
|
|
217
|
+
} else if (cloud === "gcp") {
|
|
204
218
|
const project = entry.project;
|
|
205
219
|
const zone = entry.zone;
|
|
206
220
|
if (!project || !zone) {
|
package/src/commands/rollback.ts
CHANGED
|
@@ -29,12 +29,13 @@ import {
|
|
|
29
29
|
captureContainerEnv,
|
|
30
30
|
commitWorkspaceViaGateway,
|
|
31
31
|
CONTAINER_ENV_EXCLUDE_KEYS,
|
|
32
|
+
fetchCurrentVersion,
|
|
33
|
+
fetchPreviousVersion,
|
|
32
34
|
performDockerRollback,
|
|
33
35
|
rollbackMigrations,
|
|
34
36
|
UPGRADE_PROGRESS,
|
|
35
37
|
waitForReady,
|
|
36
38
|
} from "../lib/upgrade-lifecycle.js";
|
|
37
|
-
import { compareVersions } from "../lib/version-compat.js";
|
|
38
39
|
|
|
39
40
|
function parseArgs(): { name: string | null; version: string | null } {
|
|
40
41
|
const args = process.argv.slice(3);
|
|
@@ -148,20 +149,7 @@ async function rollbackPlatformViaEndpoint(
|
|
|
148
149
|
entry: AssistantEntry,
|
|
149
150
|
version?: string,
|
|
150
151
|
): Promise<void> {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Step 1 ā Version validation (only if version provided)
|
|
154
|
-
if (version && currentVersion) {
|
|
155
|
-
const cmp = compareVersions(version, currentVersion);
|
|
156
|
-
if (cmp !== null && cmp >= 0) {
|
|
157
|
-
const msg = `Target version ${version} is not older than the current version ${currentVersion}. Use \`vellum upgrade --version ${version}\` to upgrade.`;
|
|
158
|
-
console.error(msg);
|
|
159
|
-
emitCliError("VERSION_DIRECTION", msg);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Step 2 ā Authenticate
|
|
152
|
+
// Step 1 ā Authenticate
|
|
165
153
|
const token = readPlatformToken();
|
|
166
154
|
if (!token) {
|
|
167
155
|
const msg =
|
|
@@ -171,6 +159,9 @@ async function rollbackPlatformViaEndpoint(
|
|
|
171
159
|
process.exit(1);
|
|
172
160
|
}
|
|
173
161
|
|
|
162
|
+
// Fetch current version from health endpoint (best-effort)
|
|
163
|
+
const currentVersion = await fetchCurrentVersion(entry.runtimeUrl);
|
|
164
|
+
|
|
174
165
|
// Step 3 ā Call rollback endpoint
|
|
175
166
|
if (version) {
|
|
176
167
|
console.log(`Rolling back to ${version}...`);
|
|
@@ -215,7 +206,7 @@ async function rollbackPlatformViaEndpoint(
|
|
|
215
206
|
await broadcastUpgradeEvent(
|
|
216
207
|
entry.runtimeUrl,
|
|
217
208
|
entry.assistantId,
|
|
218
|
-
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
209
|
+
buildCompleteEvent(currentVersion ?? version ?? "unknown", false),
|
|
219
210
|
);
|
|
220
211
|
process.exit(1);
|
|
221
212
|
}
|
|
@@ -234,6 +225,13 @@ export async function rollback(): Promise<void> {
|
|
|
234
225
|
const entry = resolveTargetAssistant(name);
|
|
235
226
|
const cloud = resolveCloud(entry);
|
|
236
227
|
|
|
228
|
+
if (cloud === "apple-container") {
|
|
229
|
+
console.error(
|
|
230
|
+
`Error: '${entry.assistantId}' uses the Apple Containers runtime. Rollback is not yet supported for this topology.`,
|
|
231
|
+
);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
237
235
|
// ---------- Managed (Vellum platform) rollback ----------
|
|
238
236
|
if (cloud === "vellum") {
|
|
239
237
|
await rollbackPlatformViaEndpoint(entry, version ?? undefined);
|
|
@@ -258,7 +256,7 @@ export async function rollback(): Promise<void> {
|
|
|
258
256
|
await broadcastUpgradeEvent(
|
|
259
257
|
entry.runtimeUrl,
|
|
260
258
|
entry.assistantId,
|
|
261
|
-
buildCompleteEvent(
|
|
259
|
+
buildCompleteEvent(version ?? "unknown", false),
|
|
262
260
|
);
|
|
263
261
|
emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
|
|
264
262
|
process.exit(1);
|
|
@@ -268,8 +266,14 @@ export async function rollback(): Promise<void> {
|
|
|
268
266
|
|
|
269
267
|
// ---------- Docker: Saved-state rollback (no --version) ----------
|
|
270
268
|
|
|
269
|
+
// Fetch current + previous version from live APIs
|
|
270
|
+
const currentVersion = await fetchCurrentVersion(entry.runtimeUrl);
|
|
271
|
+
const previousVersion =
|
|
272
|
+
(await fetchPreviousVersion(currentVersion, entry.previousVersion)) ??
|
|
273
|
+
"unknown";
|
|
274
|
+
|
|
271
275
|
// Verify rollback state exists
|
|
272
|
-
if (!entry.
|
|
276
|
+
if (!entry.previousContainerInfo) {
|
|
273
277
|
const msg =
|
|
274
278
|
"No rollback state available. Run `vellum upgrade` first to create a rollback point.";
|
|
275
279
|
console.error(msg);
|
|
@@ -305,15 +309,15 @@ export async function rollback(): Promise<void> {
|
|
|
305
309
|
buildUpgradeCommitMessage({
|
|
306
310
|
action: "rollback",
|
|
307
311
|
phase: "starting",
|
|
308
|
-
from:
|
|
309
|
-
to:
|
|
312
|
+
from: currentVersion ?? "unknown",
|
|
313
|
+
to: previousVersion,
|
|
310
314
|
topology: "docker",
|
|
311
315
|
assistantId: entry.assistantId,
|
|
312
316
|
}),
|
|
313
317
|
);
|
|
314
318
|
|
|
315
319
|
console.log(
|
|
316
|
-
`š Rolling back Docker assistant '${instanceName}' to ${
|
|
320
|
+
`š Rolling back Docker assistant '${instanceName}' to ${previousVersion}...\n`,
|
|
317
321
|
);
|
|
318
322
|
|
|
319
323
|
// Capture current container env
|
|
@@ -367,7 +371,7 @@ export async function rollback(): Promise<void> {
|
|
|
367
371
|
await broadcastUpgradeEvent(
|
|
368
372
|
entry.runtimeUrl,
|
|
369
373
|
entry.assistantId,
|
|
370
|
-
buildStartingEvent(
|
|
374
|
+
buildStartingEvent(previousVersion),
|
|
371
375
|
);
|
|
372
376
|
// Brief pause to allow SSE delivery before containers stop.
|
|
373
377
|
await new Promise((r) => setTimeout(r, 500));
|
|
@@ -428,7 +432,6 @@ export async function rollback(): Promise<void> {
|
|
|
428
432
|
// Swap current/previous state to enable "rollback the rollback"
|
|
429
433
|
const updatedEntry: AssistantEntry = {
|
|
430
434
|
...entry,
|
|
431
|
-
serviceGroupVersion: entry.previousServiceGroupVersion,
|
|
432
435
|
containerInfo: {
|
|
433
436
|
assistantImage: prev.assistantImage ?? previousImageRefs.assistant,
|
|
434
437
|
gatewayImage: prev.gatewayImage ?? previousImageRefs.gateway,
|
|
@@ -438,7 +441,6 @@ export async function rollback(): Promise<void> {
|
|
|
438
441
|
cesDigest: newDigests?.["credential-executor"],
|
|
439
442
|
networkName: res.network,
|
|
440
443
|
},
|
|
441
|
-
previousServiceGroupVersion: entry.serviceGroupVersion,
|
|
442
444
|
previousContainerInfo: entry.containerInfo,
|
|
443
445
|
// Clear the backup path ā it belonged to the upgrade we just rolled back
|
|
444
446
|
preUpgradeBackupPath: undefined,
|
|
@@ -451,7 +453,7 @@ export async function rollback(): Promise<void> {
|
|
|
451
453
|
await broadcastUpgradeEvent(
|
|
452
454
|
entry.runtimeUrl,
|
|
453
455
|
entry.assistantId,
|
|
454
|
-
buildCompleteEvent(
|
|
456
|
+
buildCompleteEvent(previousVersion, true),
|
|
455
457
|
);
|
|
456
458
|
|
|
457
459
|
// Record successful rollback in workspace git history
|
|
@@ -461,8 +463,8 @@ export async function rollback(): Promise<void> {
|
|
|
461
463
|
buildUpgradeCommitMessage({
|
|
462
464
|
action: "rollback",
|
|
463
465
|
phase: "complete",
|
|
464
|
-
from:
|
|
465
|
-
to:
|
|
466
|
+
from: currentVersion ?? "unknown",
|
|
467
|
+
to: previousVersion,
|
|
466
468
|
topology: "docker",
|
|
467
469
|
assistantId: entry.assistantId,
|
|
468
470
|
result: "success",
|
|
@@ -470,7 +472,7 @@ export async function rollback(): Promise<void> {
|
|
|
470
472
|
);
|
|
471
473
|
|
|
472
474
|
console.log(
|
|
473
|
-
`\nā
Docker assistant '${instanceName}' rolled back to ${
|
|
475
|
+
`\nā
Docker assistant '${instanceName}' rolled back to ${previousVersion}.`,
|
|
474
476
|
);
|
|
475
477
|
console.log(
|
|
476
478
|
"\nTip: To also restore data from before the upgrade, use `vellum restore --from <backup-path>`.",
|
|
@@ -485,10 +487,7 @@ export async function rollback(): Promise<void> {
|
|
|
485
487
|
await broadcastUpgradeEvent(
|
|
486
488
|
entry.runtimeUrl,
|
|
487
489
|
entry.assistantId,
|
|
488
|
-
buildCompleteEvent(
|
|
489
|
-
entry.previousServiceGroupVersion ?? "unknown",
|
|
490
|
-
false,
|
|
491
|
-
),
|
|
490
|
+
buildCompleteEvent(previousVersion, false),
|
|
492
491
|
);
|
|
493
492
|
emitCliError(
|
|
494
493
|
"READINESS_TIMEOUT",
|
|
@@ -502,7 +501,7 @@ export async function rollback(): Promise<void> {
|
|
|
502
501
|
await broadcastUpgradeEvent(
|
|
503
502
|
entry.runtimeUrl,
|
|
504
503
|
entry.assistantId,
|
|
505
|
-
buildCompleteEvent(
|
|
504
|
+
buildCompleteEvent(previousVersion, false),
|
|
506
505
|
);
|
|
507
506
|
emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
|
|
508
507
|
process.exit(1);
|
package/src/commands/sleep.ts
CHANGED
|
@@ -72,6 +72,13 @@ export async function sleep(): Promise<void> {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
if (entry.cloud === "apple-container") {
|
|
76
|
+
console.error(
|
|
77
|
+
`Error: '${entry.assistantId}' uses the Apple Containers runtime. Its lifecycle is managed by the macOS app ā use the app to stop it.`,
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
if (entry.cloud && entry.cloud !== "local") {
|
|
76
83
|
console.error(
|
|
77
84
|
`Error: 'vellum sleep' only works with local and docker assistants. '${entry.assistantId}' is a ${entry.cloud} instance.`,
|