@vellumai/cli 0.5.7 → 0.5.9
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/package.json +1 -1
- package/src/commands/backup.ts +124 -0
- package/src/commands/hatch.ts +24 -5
- package/src/commands/restore.ts +359 -16
- package/src/commands/rollback.ts +197 -103
- package/src/commands/upgrade.ts +204 -220
- package/src/index.ts +4 -4
- package/src/lib/aws.ts +14 -9
- package/src/lib/cli-error.ts +2 -0
- package/src/lib/docker.ts +54 -13
- package/src/lib/gcp.ts +15 -10
- package/src/lib/guardian-token.ts +4 -42
- package/src/lib/local.ts +1 -0
- package/src/lib/platform-client.ts +206 -18
- package/src/lib/upgrade-lifecycle.ts +612 -5
- package/src/lib/workspace-git.ts +0 -39
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomBytes } from "crypto";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
|
|
4
3
|
import cliPkg from "../../package.json";
|
|
5
4
|
|
|
@@ -25,10 +24,6 @@ import {
|
|
|
25
24
|
getPlatformUrl,
|
|
26
25
|
readPlatformToken,
|
|
27
26
|
} from "../lib/platform-client";
|
|
28
|
-
import {
|
|
29
|
-
loadBootstrapSecret,
|
|
30
|
-
saveBootstrapSecret,
|
|
31
|
-
} from "../lib/guardian-token";
|
|
32
27
|
import {
|
|
33
28
|
createBackup,
|
|
34
29
|
pruneOldBackups,
|
|
@@ -43,30 +38,35 @@ import {
|
|
|
43
38
|
buildStartingEvent,
|
|
44
39
|
buildUpgradeCommitMessage,
|
|
45
40
|
captureContainerEnv,
|
|
41
|
+
commitWorkspaceViaGateway,
|
|
46
42
|
CONTAINER_ENV_EXCLUDE_KEYS,
|
|
47
43
|
rollbackMigrations,
|
|
48
44
|
UPGRADE_PROGRESS,
|
|
49
45
|
waitForReady,
|
|
50
46
|
} from "../lib/upgrade-lifecycle.js";
|
|
51
47
|
import { parseVersion } from "../lib/version-compat.js";
|
|
52
|
-
import { commitWorkspaceState } from "../lib/workspace-git.js";
|
|
53
48
|
|
|
54
49
|
interface UpgradeArgs {
|
|
55
50
|
name: string | null;
|
|
56
51
|
version: string | null;
|
|
52
|
+
prepare: boolean;
|
|
53
|
+
finalize: boolean;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
function parseArgs(): UpgradeArgs {
|
|
60
57
|
const args = process.argv.slice(3);
|
|
61
58
|
let name: string | null = null;
|
|
62
59
|
let version: string | null = null;
|
|
60
|
+
let prepare = false;
|
|
61
|
+
let finalize = false;
|
|
63
62
|
|
|
64
63
|
for (let i = 0; i < args.length; i++) {
|
|
65
64
|
const arg = args[i];
|
|
66
65
|
if (arg === "--help" || arg === "-h") {
|
|
67
66
|
console.log("Usage: vellum upgrade [<name>] [options]");
|
|
68
67
|
console.log("");
|
|
69
|
-
console.log("Upgrade an assistant to
|
|
68
|
+
console.log("Upgrade an assistant to a newer version.");
|
|
69
|
+
console.log("To roll back to a previous version, use `vellum rollback`.");
|
|
70
70
|
console.log("");
|
|
71
71
|
console.log("Arguments:");
|
|
72
72
|
console.log(
|
|
@@ -77,6 +77,12 @@ function parseArgs(): UpgradeArgs {
|
|
|
77
77
|
console.log(
|
|
78
78
|
" --version <version> Target version to upgrade to (default: latest)",
|
|
79
79
|
);
|
|
80
|
+
console.log(
|
|
81
|
+
" --prepare Run pre-upgrade steps only (backup, notify) without swapping versions",
|
|
82
|
+
);
|
|
83
|
+
console.log(
|
|
84
|
+
" --finalize Run post-upgrade steps only (broadcast complete, workspace commit)",
|
|
85
|
+
);
|
|
80
86
|
console.log("");
|
|
81
87
|
console.log("Examples:");
|
|
82
88
|
console.log(
|
|
@@ -98,6 +104,10 @@ function parseArgs(): UpgradeArgs {
|
|
|
98
104
|
}
|
|
99
105
|
version = next;
|
|
100
106
|
i++;
|
|
107
|
+
} else if (arg === "--prepare") {
|
|
108
|
+
prepare = true;
|
|
109
|
+
} else if (arg === "--finalize") {
|
|
110
|
+
finalize = true;
|
|
101
111
|
} else if (!arg.startsWith("-")) {
|
|
102
112
|
name = arg;
|
|
103
113
|
} else {
|
|
@@ -107,7 +117,13 @@ function parseArgs(): UpgradeArgs {
|
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
if (prepare && finalize) {
|
|
121
|
+
console.error("Error: --prepare and --finalize are mutually exclusive.");
|
|
122
|
+
emitCliError("UNKNOWN", "--prepare and --finalize are mutually exclusive");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { name, version, prepare, finalize };
|
|
111
127
|
}
|
|
112
128
|
|
|
113
129
|
function resolveCloud(entry: AssistantEntry): string {
|
|
@@ -171,12 +187,32 @@ async function upgradeDocker(
|
|
|
171
187
|
): Promise<void> {
|
|
172
188
|
const instanceName = entry.assistantId;
|
|
173
189
|
const res = dockerResourceNames(instanceName);
|
|
174
|
-
const workspaceDir = entry.resources
|
|
175
|
-
? join(entry.resources.instanceDir, ".vellum", "workspace")
|
|
176
|
-
: null;
|
|
177
190
|
|
|
178
191
|
const versionTag =
|
|
179
192
|
version ?? (cliPkg.version ? `v${cliPkg.version}` : "latest");
|
|
193
|
+
|
|
194
|
+
// Reject downgrades — `vellum upgrade` only handles forward version changes.
|
|
195
|
+
// Users should use `vellum rollback --version <version>` for downgrades.
|
|
196
|
+
const currentVersion = entry.serviceGroupVersion;
|
|
197
|
+
if (currentVersion && versionTag) {
|
|
198
|
+
const current = parseVersion(currentVersion);
|
|
199
|
+
const target = parseVersion(versionTag);
|
|
200
|
+
if (current && target) {
|
|
201
|
+
const isOlder =
|
|
202
|
+
target.major < current.major ||
|
|
203
|
+
(target.major === current.major && target.minor < current.minor) ||
|
|
204
|
+
(target.major === current.major &&
|
|
205
|
+
target.minor === current.minor &&
|
|
206
|
+
target.patch < current.patch);
|
|
207
|
+
if (isOlder) {
|
|
208
|
+
const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`vellum rollback --version ${versionTag}\` instead.`;
|
|
209
|
+
console.error(msg);
|
|
210
|
+
emitCliError("VERSION_DIRECTION", msg);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
180
216
|
console.log("🔍 Resolving image references...");
|
|
181
217
|
const { imageTags } = await resolveImageRefs(versionTag);
|
|
182
218
|
|
|
@@ -223,63 +259,6 @@ async function upgradeDocker(
|
|
|
223
259
|
// Best-effort — if we can't get migration state, rollback will skip migration reversal
|
|
224
260
|
}
|
|
225
261
|
|
|
226
|
-
// Detect if this upgrade is actually a downgrade (user picked an older
|
|
227
|
-
// version via the version picker). Used after readiness succeeds to align
|
|
228
|
-
// the DB schema with the now-running old daemon.
|
|
229
|
-
const currentVersion = entry.serviceGroupVersion;
|
|
230
|
-
const isDowngrade =
|
|
231
|
-
currentVersion &&
|
|
232
|
-
versionTag &&
|
|
233
|
-
(() => {
|
|
234
|
-
const current = parseVersion(currentVersion);
|
|
235
|
-
const target = parseVersion(versionTag);
|
|
236
|
-
if (!current || !target) return false;
|
|
237
|
-
if (target.major !== current.major) return target.major < current.major;
|
|
238
|
-
if (target.minor !== current.minor) return target.minor < current.minor;
|
|
239
|
-
return target.patch < current.patch;
|
|
240
|
-
})();
|
|
241
|
-
|
|
242
|
-
// For downgrades, fetch the target version's migration ceiling from the
|
|
243
|
-
// releases API. This tells us exactly which DB migration version and
|
|
244
|
-
// workspace migration the target version expects, enabling a precise
|
|
245
|
-
// rollback on the CURRENT (newer) daemon before swapping containers.
|
|
246
|
-
let targetMigrationCeiling: {
|
|
247
|
-
dbVersion?: number;
|
|
248
|
-
workspaceMigrationId?: string;
|
|
249
|
-
} = {};
|
|
250
|
-
if (isDowngrade) {
|
|
251
|
-
try {
|
|
252
|
-
const platformUrl = getPlatformUrl();
|
|
253
|
-
const releasesResp = await fetch(
|
|
254
|
-
`${platformUrl}/v1/releases/?stable=true`,
|
|
255
|
-
{ signal: AbortSignal.timeout(10000) },
|
|
256
|
-
);
|
|
257
|
-
if (releasesResp.ok) {
|
|
258
|
-
const releases = (await releasesResp.json()) as Array<{
|
|
259
|
-
version: string;
|
|
260
|
-
db_migration_version?: number | null;
|
|
261
|
-
last_workspace_migration_id?: string;
|
|
262
|
-
}>;
|
|
263
|
-
const normalizedTag = versionTag.replace(/^v/, "");
|
|
264
|
-
const targetRelease = releases.find(
|
|
265
|
-
(r) => r.version?.replace(/^v/, "") === normalizedTag,
|
|
266
|
-
);
|
|
267
|
-
if (
|
|
268
|
-
targetRelease?.db_migration_version != null ||
|
|
269
|
-
targetRelease?.last_workspace_migration_id
|
|
270
|
-
) {
|
|
271
|
-
targetMigrationCeiling = {
|
|
272
|
-
dbVersion: targetRelease.db_migration_version ?? undefined,
|
|
273
|
-
workspaceMigrationId:
|
|
274
|
-
targetRelease.last_workspace_migration_id || undefined,
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
} catch {
|
|
279
|
-
// Best-effort — fall back to rollbackToRegistryCeiling post-swap
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
262
|
// Persist rollback state to lockfile BEFORE any destructive changes.
|
|
284
263
|
// This enables the `vellum rollback` command to restore the previous version.
|
|
285
264
|
if (entry.serviceGroupVersion && entry.containerInfo) {
|
|
@@ -295,25 +274,18 @@ async function upgradeDocker(
|
|
|
295
274
|
}
|
|
296
275
|
|
|
297
276
|
// Record version transition start in workspace git history
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
);
|
|
311
|
-
} catch (err) {
|
|
312
|
-
console.warn(
|
|
313
|
-
`⚠️ Failed to create pre-upgrade workspace commit: ${err instanceof Error ? err.message : String(err)}`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
277
|
+
await commitWorkspaceViaGateway(
|
|
278
|
+
entry.runtimeUrl,
|
|
279
|
+
entry.assistantId,
|
|
280
|
+
buildUpgradeCommitMessage({
|
|
281
|
+
action: "upgrade",
|
|
282
|
+
phase: "starting",
|
|
283
|
+
from: entry.serviceGroupVersion ?? "unknown",
|
|
284
|
+
to: versionTag,
|
|
285
|
+
topology: "docker",
|
|
286
|
+
assistantId: entry.assistantId,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
317
289
|
|
|
318
290
|
console.log("💾 Capturing existing container environment...");
|
|
319
291
|
const capturedEnv = await captureContainerEnv(res.assistantContainer);
|
|
@@ -381,16 +353,6 @@ async function upgradeDocker(
|
|
|
381
353
|
const cesServiceToken =
|
|
382
354
|
capturedEnv["CES_SERVICE_TOKEN"] || randomBytes(32).toString("hex");
|
|
383
355
|
|
|
384
|
-
// Retrieve or generate a bootstrap secret for the gateway. The secret was
|
|
385
|
-
// persisted to disk during hatch; older instances won't have one yet.
|
|
386
|
-
// This runs BEFORE stopping containers so a write failure (disk full,
|
|
387
|
-
// permissions) doesn't leave the assistant offline.
|
|
388
|
-
const loadedSecret = loadBootstrapSecret(instanceName);
|
|
389
|
-
const bootstrapSecret = loadedSecret || randomBytes(32).toString("hex");
|
|
390
|
-
if (!loadedSecret) {
|
|
391
|
-
saveBootstrapSecret(instanceName, bootstrapSecret);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
356
|
// Extract or generate the shared JWT signing key. Pre-env-var instances
|
|
395
357
|
// won't have it in capturedEnv, so generate fresh in that case.
|
|
396
358
|
const signingKey =
|
|
@@ -434,32 +396,6 @@ async function upgradeDocker(
|
|
|
434
396
|
buildProgressEvent(UPGRADE_PROGRESS.INSTALLING),
|
|
435
397
|
);
|
|
436
398
|
|
|
437
|
-
// If we have the target version's migration ceiling, run a PRECISE
|
|
438
|
-
// rollback on the CURRENT (newer) daemon before stopping it. The current
|
|
439
|
-
// daemon has the `down()` code for all migrations it applied, so it can
|
|
440
|
-
// cleanly revert to the target version's ceiling. This is critical for
|
|
441
|
-
// multi-version downgrades where the old daemon wouldn't know about
|
|
442
|
-
// migrations introduced after its release.
|
|
443
|
-
let preSwapRollbackOk = true;
|
|
444
|
-
if (
|
|
445
|
-
isDowngrade &&
|
|
446
|
-
(targetMigrationCeiling.dbVersion !== undefined ||
|
|
447
|
-
targetMigrationCeiling.workspaceMigrationId !== undefined)
|
|
448
|
-
) {
|
|
449
|
-
console.log("🔄 Reverting database changes for downgrade...");
|
|
450
|
-
await broadcastUpgradeEvent(
|
|
451
|
-
entry.runtimeUrl,
|
|
452
|
-
entry.assistantId,
|
|
453
|
-
buildProgressEvent(UPGRADE_PROGRESS.REVERTING_MIGRATIONS),
|
|
454
|
-
);
|
|
455
|
-
preSwapRollbackOk = await rollbackMigrations(
|
|
456
|
-
entry.runtimeUrl,
|
|
457
|
-
entry.assistantId,
|
|
458
|
-
targetMigrationCeiling.dbVersion,
|
|
459
|
-
targetMigrationCeiling.workspaceMigrationId,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
399
|
console.log("🛑 Stopping existing containers...");
|
|
464
400
|
await stopContainers(res);
|
|
465
401
|
console.log("✅ Containers stopped\n");
|
|
@@ -491,7 +427,6 @@ async function upgradeDocker(
|
|
|
491
427
|
await startContainers(
|
|
492
428
|
{
|
|
493
429
|
signingKey,
|
|
494
|
-
bootstrapSecret,
|
|
495
430
|
cesServiceToken,
|
|
496
431
|
extraAssistantEnv,
|
|
497
432
|
gatewayPort,
|
|
@@ -529,27 +464,6 @@ async function upgradeDocker(
|
|
|
529
464
|
};
|
|
530
465
|
saveAssistantEntry(updatedEntry);
|
|
531
466
|
|
|
532
|
-
// After a downgrade, fall back to asking the now-running old daemon
|
|
533
|
-
// to roll back migrations above its own registry ceiling when either:
|
|
534
|
-
// (a) no release metadata was available for a precise pre-swap rollback, or
|
|
535
|
-
// (b) the precise pre-swap rollback failed (timeout, daemon crash, etc.).
|
|
536
|
-
// This is a no-op for multi-version jumps where the old daemon doesn't
|
|
537
|
-
// know about the newer migrations, but correct for single-step rollbacks.
|
|
538
|
-
if (
|
|
539
|
-
isDowngrade &&
|
|
540
|
-
(!preSwapRollbackOk ||
|
|
541
|
-
(targetMigrationCeiling.dbVersion === undefined &&
|
|
542
|
-
targetMigrationCeiling.workspaceMigrationId === undefined))
|
|
543
|
-
) {
|
|
544
|
-
await rollbackMigrations(
|
|
545
|
-
entry.runtimeUrl,
|
|
546
|
-
entry.assistantId,
|
|
547
|
-
undefined,
|
|
548
|
-
undefined,
|
|
549
|
-
true,
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
467
|
// Notify clients on the new service group that the upgrade succeeded.
|
|
554
468
|
await broadcastUpgradeEvent(
|
|
555
469
|
entry.runtimeUrl,
|
|
@@ -558,26 +472,19 @@ async function upgradeDocker(
|
|
|
558
472
|
);
|
|
559
473
|
|
|
560
474
|
// Record successful upgrade in workspace git history
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
575
|
-
} catch (err) {
|
|
576
|
-
console.warn(
|
|
577
|
-
`⚠️ Failed to create post-upgrade workspace commit: ${err instanceof Error ? err.message : String(err)}`,
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
475
|
+
await commitWorkspaceViaGateway(
|
|
476
|
+
entry.runtimeUrl,
|
|
477
|
+
entry.assistantId,
|
|
478
|
+
buildUpgradeCommitMessage({
|
|
479
|
+
action: "upgrade",
|
|
480
|
+
phase: "complete",
|
|
481
|
+
from: entry.serviceGroupVersion ?? "unknown",
|
|
482
|
+
to: versionTag,
|
|
483
|
+
topology: "docker",
|
|
484
|
+
assistantId: entry.assistantId,
|
|
485
|
+
result: "success",
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
581
488
|
|
|
582
489
|
console.log(
|
|
583
490
|
`\n✅ Docker assistant '${instanceName}' upgraded to ${versionTag}.`,
|
|
@@ -621,7 +528,6 @@ async function upgradeDocker(
|
|
|
621
528
|
await startContainers(
|
|
622
529
|
{
|
|
623
530
|
signingKey,
|
|
624
|
-
bootstrapSecret,
|
|
625
531
|
cesServiceToken,
|
|
626
532
|
extraAssistantEnv,
|
|
627
533
|
gatewayPort,
|
|
@@ -785,28 +691,28 @@ async function upgradePlatform(
|
|
|
785
691
|
entry: AssistantEntry,
|
|
786
692
|
version: string | null,
|
|
787
693
|
): Promise<void> {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
694
|
+
// Reject downgrades — `vellum upgrade` only handles forward version changes.
|
|
695
|
+
// Users should use `vellum rollback --version <version>` for downgrades.
|
|
696
|
+
// Only enforce this guard when the user explicitly passed `--version`.
|
|
697
|
+
// When version is null the platform API decides the actual target, so
|
|
698
|
+
// we must not block the request based on the local CLI version.
|
|
699
|
+
const currentVersion = entry.serviceGroupVersion;
|
|
700
|
+
if (version && currentVersion) {
|
|
701
|
+
const current = parseVersion(currentVersion);
|
|
702
|
+
const target = parseVersion(version);
|
|
703
|
+
if (current && target) {
|
|
704
|
+
const isOlder =
|
|
705
|
+
target.major < current.major ||
|
|
706
|
+
(target.major === current.major && target.minor < current.minor) ||
|
|
707
|
+
(target.major === current.major &&
|
|
708
|
+
target.minor === current.minor &&
|
|
709
|
+
target.patch < current.patch);
|
|
710
|
+
if (isOlder) {
|
|
711
|
+
const msg = `Cannot upgrade to an older version (${version} < ${currentVersion}). Use \`vellum rollback --version ${version}\` instead.`;
|
|
712
|
+
console.error(msg);
|
|
713
|
+
emitCliError("VERSION_DIRECTION", msg);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
810
716
|
}
|
|
811
717
|
}
|
|
812
718
|
|
|
@@ -833,15 +739,6 @@ async function upgradePlatform(
|
|
|
833
739
|
body.version = version;
|
|
834
740
|
}
|
|
835
741
|
|
|
836
|
-
// Notify connected clients that an upgrade is about to begin.
|
|
837
|
-
const targetVersion = version ?? `v${cliPkg.version}`;
|
|
838
|
-
console.log("📢 Notifying connected clients...");
|
|
839
|
-
await broadcastUpgradeEvent(
|
|
840
|
-
entry.runtimeUrl,
|
|
841
|
-
entry.assistantId,
|
|
842
|
-
buildStartingEvent(targetVersion, 90),
|
|
843
|
-
);
|
|
844
|
-
|
|
845
742
|
const response = await fetch(url, {
|
|
846
743
|
method: "POST",
|
|
847
744
|
headers: {
|
|
@@ -879,37 +776,124 @@ async function upgradePlatform(
|
|
|
879
776
|
// version-change detection (DaemonConnection.swift) once the new
|
|
880
777
|
// version actually appears after the platform restarts the service group.
|
|
881
778
|
|
|
882
|
-
// Record successful upgrade in workspace git history
|
|
883
|
-
if (workspaceDir) {
|
|
884
|
-
try {
|
|
885
|
-
await commitWorkspaceState(
|
|
886
|
-
workspaceDir,
|
|
887
|
-
buildUpgradeCommitMessage({
|
|
888
|
-
action: "upgrade",
|
|
889
|
-
phase: "complete",
|
|
890
|
-
from: entry.serviceGroupVersion ?? "unknown",
|
|
891
|
-
to: version ?? "latest",
|
|
892
|
-
topology: "managed",
|
|
893
|
-
assistantId: entry.assistantId,
|
|
894
|
-
result: "success",
|
|
895
|
-
}),
|
|
896
|
-
);
|
|
897
|
-
} catch (err) {
|
|
898
|
-
console.warn(
|
|
899
|
-
`⚠️ Failed to create post-upgrade workspace commit: ${err instanceof Error ? err.message : String(err)}`,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
779
|
console.log(`✅ ${result.detail}`);
|
|
905
780
|
if (result.version) {
|
|
906
781
|
console.log(` Version: ${result.version}`);
|
|
907
782
|
}
|
|
908
783
|
}
|
|
909
784
|
|
|
785
|
+
/**
|
|
786
|
+
* Pre-upgrade steps for Sparkle (macOS app) lifecycle.
|
|
787
|
+
* Runs the pre-update orchestration without actually swapping containers:
|
|
788
|
+
* broadcasts SSE events, creates a workspace commit, creates a backup,
|
|
789
|
+
* prunes old backups, and outputs the backup path.
|
|
790
|
+
*/
|
|
791
|
+
async function upgradePrepare(
|
|
792
|
+
entry: AssistantEntry,
|
|
793
|
+
version: string | null,
|
|
794
|
+
): Promise<void> {
|
|
795
|
+
const targetVersion = version ?? entry.serviceGroupVersion ?? "unknown";
|
|
796
|
+
const currentVersion = entry.serviceGroupVersion ?? "unknown";
|
|
797
|
+
|
|
798
|
+
// 1. Broadcast "starting" so the UI shows the progress spinner
|
|
799
|
+
await broadcastUpgradeEvent(
|
|
800
|
+
entry.runtimeUrl,
|
|
801
|
+
entry.assistantId,
|
|
802
|
+
buildStartingEvent(targetVersion, 30),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
// 2. Workspace commit: record pre-update state
|
|
806
|
+
await commitWorkspaceViaGateway(
|
|
807
|
+
entry.runtimeUrl,
|
|
808
|
+
entry.assistantId,
|
|
809
|
+
`[sparkle-update] Starting: ${currentVersion} → ${targetVersion}`,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
// 3. Progress: saving backup
|
|
813
|
+
await broadcastUpgradeEvent(
|
|
814
|
+
entry.runtimeUrl,
|
|
815
|
+
entry.assistantId,
|
|
816
|
+
buildProgressEvent("Saving a backup of your data…"),
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
// 4. Create backup
|
|
820
|
+
const backupPath = await createBackup(entry.runtimeUrl, entry.assistantId, {
|
|
821
|
+
prefix: `${entry.assistantId}-pre-upgrade`,
|
|
822
|
+
description: `Pre-upgrade snapshot before ${currentVersion} → ${targetVersion}`,
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// 5. Prune old backups (keep 3)
|
|
826
|
+
if (backupPath) {
|
|
827
|
+
pruneOldBackups(entry.assistantId, 3);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// 6. Progress: installing update
|
|
831
|
+
await broadcastUpgradeEvent(
|
|
832
|
+
entry.runtimeUrl,
|
|
833
|
+
entry.assistantId,
|
|
834
|
+
buildProgressEvent(UPGRADE_PROGRESS.INSTALLING),
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
// 7. Output backup path to stdout for the macOS app to parse
|
|
838
|
+
if (backupPath) {
|
|
839
|
+
console.log(`BACKUP_PATH:${backupPath}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Post-upgrade steps for Sparkle (macOS app) lifecycle.
|
|
845
|
+
* Called after the app has been replaced and the daemon is back up.
|
|
846
|
+
* Broadcasts a "complete" SSE event and creates a workspace commit.
|
|
847
|
+
*/
|
|
848
|
+
async function upgradeFinalize(
|
|
849
|
+
entry: AssistantEntry,
|
|
850
|
+
version: string | null,
|
|
851
|
+
): Promise<void> {
|
|
852
|
+
if (!version) {
|
|
853
|
+
console.error(
|
|
854
|
+
"Error: --finalize requires --version <from-version> to record the transition.",
|
|
855
|
+
);
|
|
856
|
+
emitCliError(
|
|
857
|
+
"UNKNOWN",
|
|
858
|
+
"--finalize requires --version <from-version> to record the transition",
|
|
859
|
+
);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const fromVersion = version;
|
|
864
|
+
const currentVersion = cliPkg.version
|
|
865
|
+
? `v${cliPkg.version}`
|
|
866
|
+
: (entry.serviceGroupVersion ?? "unknown");
|
|
867
|
+
|
|
868
|
+
// 1. Broadcast "complete" so the UI clears the progress spinner
|
|
869
|
+
await broadcastUpgradeEvent(
|
|
870
|
+
entry.runtimeUrl,
|
|
871
|
+
entry.assistantId,
|
|
872
|
+
buildCompleteEvent(currentVersion, true),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// 2. Workspace commit: record successful update
|
|
876
|
+
await commitWorkspaceViaGateway(
|
|
877
|
+
entry.runtimeUrl,
|
|
878
|
+
entry.assistantId,
|
|
879
|
+
`[sparkle-update] Complete: ${fromVersion} → ${currentVersion}\n\nresult: success`,
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
910
883
|
export async function upgrade(): Promise<void> {
|
|
911
|
-
const { name, version } = parseArgs();
|
|
884
|
+
const { name, version, prepare, finalize } = parseArgs();
|
|
912
885
|
const entry = resolveTargetAssistant(name);
|
|
886
|
+
|
|
887
|
+
if (prepare) {
|
|
888
|
+
await upgradePrepare(entry, version);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (finalize) {
|
|
893
|
+
await upgradeFinalize(entry, version);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
913
897
|
const cloud = resolveCloud(entry);
|
|
914
898
|
|
|
915
899
|
try {
|
package/src/index.ts
CHANGED
|
@@ -68,16 +68,16 @@ function printHelp(): void {
|
|
|
68
68
|
" ps List assistants (or processes for a specific assistant)",
|
|
69
69
|
);
|
|
70
70
|
console.log(" recover Restore a previously retired local assistant");
|
|
71
|
-
console.log(" restore Restore a .vbundle backup into a running assistant");
|
|
72
|
-
console.log(" retire Delete an assistant instance");
|
|
73
71
|
console.log(
|
|
74
|
-
"
|
|
72
|
+
" restore Restore data (and optionally version) from a .vbundle backup",
|
|
75
73
|
);
|
|
74
|
+
console.log(" retire Delete an assistant instance");
|
|
75
|
+
console.log(" rollback Roll back an assistant to a previous version");
|
|
76
76
|
console.log(" setup Configure API keys interactively");
|
|
77
77
|
console.log(" sleep Stop the assistant process");
|
|
78
78
|
console.log(" ssh SSH into a remote assistant instance");
|
|
79
79
|
console.log(" tunnel Create a tunnel for a locally hosted assistant");
|
|
80
|
-
console.log(" upgrade Upgrade an assistant to
|
|
80
|
+
console.log(" upgrade Upgrade an assistant to a newer version");
|
|
81
81
|
console.log(" use Set the active assistant for commands");
|
|
82
82
|
console.log(" wake Start the assistant and gateway");
|
|
83
83
|
console.log(" whoami Show current logged-in user");
|
package/src/lib/aws.ts
CHANGED
|
@@ -443,14 +443,15 @@ export async function hatchAws(
|
|
|
443
443
|
console.log("\u{1F50D} Finding latest Debian AMI...");
|
|
444
444
|
const amiId = await getLatestDebianAmi(region);
|
|
445
445
|
|
|
446
|
-
const startupScript
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
446
|
+
const { script: startupScript, laptopBootstrapSecret } =
|
|
447
|
+
await buildStartupScript(
|
|
448
|
+
species,
|
|
449
|
+
sshUser,
|
|
450
|
+
providerApiKeys,
|
|
451
|
+
instanceName,
|
|
452
|
+
"aws",
|
|
453
|
+
configValues,
|
|
454
|
+
);
|
|
454
455
|
const startupScriptPath = join(tmpdir(), `${instanceName}-startup.sh`);
|
|
455
456
|
writeFileSync(startupScriptPath, startupScript);
|
|
456
457
|
|
|
@@ -539,7 +540,11 @@ export async function hatchAws(
|
|
|
539
540
|
}
|
|
540
541
|
|
|
541
542
|
try {
|
|
542
|
-
await leaseGuardianToken(
|
|
543
|
+
await leaseGuardianToken(
|
|
544
|
+
runtimeUrl,
|
|
545
|
+
instanceName,
|
|
546
|
+
laptopBootstrapSecret,
|
|
547
|
+
);
|
|
543
548
|
} catch (err) {
|
|
544
549
|
console.warn(
|
|
545
550
|
`\u26a0\ufe0f Could not lease guardian token: ${err instanceof Error ? err.message : err}`,
|
package/src/lib/cli-error.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
export type CliErrorCategory =
|
|
12
12
|
| "DOCKER_NOT_RUNNING"
|
|
13
13
|
| "IMAGE_PULL_FAILED"
|
|
14
|
+
| "MISSING_VERSION"
|
|
14
15
|
| "READINESS_TIMEOUT"
|
|
15
16
|
| "ROLLBACK_FAILED"
|
|
16
17
|
| "ROLLBACK_NO_STATE"
|
|
18
|
+
| "VERSION_DIRECTION"
|
|
17
19
|
| "AUTH_FAILED"
|
|
18
20
|
| "NETWORK_ERROR"
|
|
19
21
|
| "UNSUPPORTED_TOPOLOGY"
|