@mycadet/cli 0.12.11 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/package.json +3 -2
- package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
- package/src/__tests__/backup-ops-matching.test.ts +15 -13
- package/src/__tests__/backup-ops.test.ts +105 -0
- package/src/__tests__/backup.test.ts +114 -673
- package/src/__tests__/cloud-releases.test.ts +82 -0
- package/src/__tests__/commands-mock.ts +101 -0
- package/src/__tests__/debug.test.ts +17 -91
- package/src/__tests__/devices.test.ts +2 -2
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/nginx-ingress.test.ts +182 -23
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/paired-lifecycle.test.ts +2 -2
- package/src/__tests__/ps-cloud-status.test.ts +6 -6
- package/src/__tests__/root-backup-docker.test.ts +294 -0
- package/src/__tests__/root-backup.test.ts +527 -0
- package/src/__tests__/root-restore-command.test.ts +147 -0
- package/src/__tests__/root-signing-key.test.ts +50 -0
- package/src/__tests__/setup.test.ts +3 -3
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +1 -1
- package/src/__tests__/upgrade-local.test.ts +21 -11
- package/src/__tests__/upgrade-replay-env.test.ts +2 -2
- package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
- package/src/__tests__/windows-lifecycle.test.ts +25 -0
- package/src/commands/backup.ts +44 -399
- package/src/commands/client.ts +3 -3
- package/src/commands/completions.ts +0 -2
- package/src/commands/debug.ts +75 -131
- package/src/commands/devices.ts +51 -17
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/pair.ts +2 -2
- package/src/commands/restore.ts +77 -444
- package/src/commands/retire.ts +2 -3
- package/src/commands/sleep.ts +15 -2
- package/src/commands/teleport.ts +47 -1559
- package/src/commands/upgrade.ts +78 -156
- package/src/commands/wake.ts +1 -1
- package/src/index.ts +0 -4
- package/src/lib/__tests__/http-client-version.test.ts +29 -0
- package/src/lib/__tests__/runtime-url.test.ts +3 -75
- package/src/lib/__tests__/web-dist.test.ts +27 -1
- package/src/lib/assistant-client.ts +1 -1
- package/src/lib/assistant-config.ts +0 -18
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login.ts +2 -1
- package/src/lib/cloud-releases.ts +54 -10
- package/src/lib/cloud-values.ts +1 -1
- package/src/lib/confirm-action.ts +2 -2
- package/src/lib/docker.ts +59 -22
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +17 -1
- package/src/lib/host-teleport.ts +506 -0
- package/src/lib/http-client.ts +25 -0
- package/src/lib/live-voice/tunnel.test.ts +1 -1
- package/src/lib/live-voice/tunnel.ts +1 -1
- package/src/lib/local.ts +4 -2
- package/src/lib/nginx-ingress.ts +134 -18
- package/src/lib/process.ts +13 -0
- package/src/lib/root-backup.ts +917 -0
- package/src/lib/root-signing-key.ts +31 -0
- package/src/lib/runtime-url.ts +0 -29
- package/src/lib/teleport-backup.ts +3 -16
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/__tests__/bundle-staging.test.ts +0 -268
- package/src/__tests__/unpair.test.ts +0 -160
- package/src/commands/rollback.ts +0 -479
- package/src/commands/unpair.ts +0 -122
- package/src/lib/__tests__/job-polling.test.ts +0 -278
- package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
- package/src/lib/bundle-staging.ts +0 -220
- package/src/lib/job-polling.ts +0 -195
- package/src/lib/local-runtime-client.ts +0 -374
package/src/commands/upgrade.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
pullImage,
|
|
23
23
|
startContainers,
|
|
24
24
|
stopContainers,
|
|
25
|
+
sleepContainers,
|
|
25
26
|
} from "../lib/docker";
|
|
26
27
|
import {
|
|
27
28
|
fetchLatestStableVersion,
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
stripVersionPrefix,
|
|
68
69
|
versionsEqual,
|
|
69
70
|
} from "../lib/version-compat.js";
|
|
71
|
+
import { fetchCurrentVersion } from "../lib/http-client.js";
|
|
70
72
|
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
71
73
|
import {
|
|
72
74
|
generateLocalSigningKey,
|
|
@@ -117,7 +119,7 @@ function parseArgs(): UpgradeArgs {
|
|
|
117
119
|
console.log("Usage: cadet upgrade [<name>] [options]");
|
|
118
120
|
console.log("");
|
|
119
121
|
console.log("Upgrade an assistant to a newer version.");
|
|
120
|
-
console.log("To roll back to a previous version, use `cadet
|
|
122
|
+
console.log("To roll back to a previous version, use `cadet restore` with a backup.");
|
|
121
123
|
console.log("");
|
|
122
124
|
console.log("Arguments:");
|
|
123
125
|
console.log(
|
|
@@ -294,7 +296,7 @@ async function upgradeDocker(
|
|
|
294
296
|
}
|
|
295
297
|
|
|
296
298
|
// Reject downgrades — `cadet upgrade` only handles forward version changes.
|
|
297
|
-
//
|
|
299
|
+
// Downgrades require a versioned backup restore.
|
|
298
300
|
if (!currentVersion && versionTag) {
|
|
299
301
|
console.warn(
|
|
300
302
|
"⚠️ Could not determine current version from health endpoint — skipping version-direction check.\n",
|
|
@@ -303,7 +305,7 @@ async function upgradeDocker(
|
|
|
303
305
|
if (currentVersion && versionTag) {
|
|
304
306
|
const cmp = compareVersions(versionTag, currentVersion);
|
|
305
307
|
if (cmp !== null && cmp < 0) {
|
|
306
|
-
const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`cadet
|
|
308
|
+
const msg = `Cannot upgrade to an older version (${versionTag} < ${currentVersion}). Use \`cadet restore --from <backup-path> --version ${versionTag}\` instead.`;
|
|
307
309
|
console.error(msg);
|
|
308
310
|
emitCliError("VERSION_DIRECTION", msg);
|
|
309
311
|
process.exit(1);
|
|
@@ -345,22 +347,6 @@ async function upgradeDocker(
|
|
|
345
347
|
);
|
|
346
348
|
}
|
|
347
349
|
|
|
348
|
-
// Persist rollback state to lockfile BEFORE any destructive changes.
|
|
349
|
-
// This enables the `cadet rollback` command to restore the previous version.
|
|
350
|
-
if (entry.containerInfo) {
|
|
351
|
-
const rollbackEntry: AssistantEntry = {
|
|
352
|
-
...entry,
|
|
353
|
-
previousContainerInfo: { ...entry.containerInfo },
|
|
354
|
-
previousVersion: currentVersion,
|
|
355
|
-
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
356
|
-
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
357
|
-
};
|
|
358
|
-
saveAssistantEntry(rollbackEntry);
|
|
359
|
-
if (currentVersion) {
|
|
360
|
-
console.log(` Saved rollback state: ${currentVersion}\n`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
350
|
// Record version transition start in workspace git history
|
|
365
351
|
await commitWorkspaceViaGateway(
|
|
366
352
|
entry.runtimeUrl,
|
|
@@ -440,7 +426,7 @@ async function upgradeDocker(
|
|
|
440
426
|
const assistantPort =
|
|
441
427
|
entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
|
|
442
428
|
|
|
443
|
-
//
|
|
429
|
+
// Capture complete persistent roots before replacing any services.
|
|
444
430
|
await broadcastUpgradeEvent(
|
|
445
431
|
entry.runtimeUrl,
|
|
446
432
|
entry.assistantId,
|
|
@@ -456,20 +442,7 @@ async function upgradeDocker(
|
|
|
456
442
|
// Clean up old pre-upgrade backups, keep last 3
|
|
457
443
|
pruneOldBackups(entry.assistantId, 3);
|
|
458
444
|
} else {
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Persist the backup path so `cadet rollback` can restore the exact backup
|
|
463
|
-
// created for this upgrade attempt — never a stale backup from a prior cycle.
|
|
464
|
-
// Re-read the entry to pick up the rollback state saved earlier.
|
|
465
|
-
{
|
|
466
|
-
const current = findAssistantByName(entry.assistantId);
|
|
467
|
-
if (current) {
|
|
468
|
-
saveAssistantEntry({
|
|
469
|
-
...current,
|
|
470
|
-
preUpgradeBackupPath: backupPath ?? undefined,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
445
|
+
throw new Error("Pre-upgrade root backup failed; upgrade aborted before replacing services.");
|
|
473
446
|
}
|
|
474
447
|
|
|
475
448
|
await broadcastUpgradeEvent(
|
|
@@ -479,6 +452,7 @@ async function upgradeDocker(
|
|
|
479
452
|
);
|
|
480
453
|
|
|
481
454
|
console.log("🛑 Stopping existing containers...");
|
|
455
|
+
await sleepContainers(res);
|
|
482
456
|
await stopContainers(res);
|
|
483
457
|
console.log("✅ Containers stopped\n");
|
|
484
458
|
|
|
@@ -512,11 +486,6 @@ async function upgradeDocker(
|
|
|
512
486
|
cesImage: imageTags["credential-executor"],
|
|
513
487
|
assistantPort,
|
|
514
488
|
},
|
|
515
|
-
previousContainerInfo: entry.containerInfo,
|
|
516
|
-
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
517
|
-
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
518
|
-
// Preserve the backup path so `cadet rollback` can restore it later
|
|
519
|
-
preUpgradeBackupPath: backupPath ?? undefined,
|
|
520
489
|
};
|
|
521
490
|
saveAssistantEntry(updatedEntry);
|
|
522
491
|
// Refresh the host-served web bundle before clients reload on completion.
|
|
@@ -586,6 +555,7 @@ async function upgradeDocker(
|
|
|
586
555
|
);
|
|
587
556
|
}
|
|
588
557
|
|
|
558
|
+
await sleepContainers(res);
|
|
589
559
|
await stopContainers(res);
|
|
590
560
|
|
|
591
561
|
await startContainers(
|
|
@@ -652,14 +622,12 @@ async function upgradeDocker(
|
|
|
652
622
|
entry.runtimeUrl,
|
|
653
623
|
entry.assistantId,
|
|
654
624
|
backupPath,
|
|
655
|
-
{
|
|
625
|
+
{},
|
|
656
626
|
);
|
|
657
627
|
if (restored) {
|
|
658
628
|
console.log(" ✅ Data restored successfully\n");
|
|
659
629
|
} else {
|
|
660
|
-
|
|
661
|
-
" ⚠️ Data restore failed (rollback continues without data restoration)\n",
|
|
662
|
-
);
|
|
630
|
+
throw new Error(`Root restore failed; rollback recovery is incomplete. Backup: ${backupPath}`);
|
|
663
631
|
}
|
|
664
632
|
} else {
|
|
665
633
|
console.log(
|
|
@@ -683,11 +651,6 @@ async function upgradeDocker(
|
|
|
683
651
|
previousImageRefs["credential-executor"],
|
|
684
652
|
assistantPort,
|
|
685
653
|
},
|
|
686
|
-
previousContainerInfo: undefined,
|
|
687
|
-
previousDbMigrationVersion: undefined,
|
|
688
|
-
previousWorkspaceMigrationId: undefined,
|
|
689
|
-
// Clear the backup path — the upgrade that created it just failed
|
|
690
|
-
preUpgradeBackupPath: undefined,
|
|
691
654
|
};
|
|
692
655
|
saveAssistantEntry(rolledBackEntry);
|
|
693
656
|
|
|
@@ -815,40 +778,6 @@ async function ensureGuardianTokenAfterLocalUpgrade(
|
|
|
815
778
|
}
|
|
816
779
|
}
|
|
817
780
|
|
|
818
|
-
async function fetchLocalUpgradeState(
|
|
819
|
-
entry: AssistantEntry,
|
|
820
|
-
adminUrl: string,
|
|
821
|
-
): Promise<{
|
|
822
|
-
currentVersion?: string;
|
|
823
|
-
preMigrationState: {
|
|
824
|
-
dbVersion?: number;
|
|
825
|
-
lastWorkspaceMigrationId?: string;
|
|
826
|
-
};
|
|
827
|
-
}> {
|
|
828
|
-
try {
|
|
829
|
-
const healthResp = await loopbackSafeFetch(
|
|
830
|
-
`${adminUrl}/healthz?include=migrations`,
|
|
831
|
-
{
|
|
832
|
-
signal: AbortSignal.timeout(5000),
|
|
833
|
-
},
|
|
834
|
-
);
|
|
835
|
-
if (healthResp.ok) {
|
|
836
|
-
const health = (await healthResp.json()) as {
|
|
837
|
-
version?: string;
|
|
838
|
-
migrations?: { dbVersion?: number; lastWorkspaceMigrationId?: string };
|
|
839
|
-
};
|
|
840
|
-
return {
|
|
841
|
-
currentVersion: health.version,
|
|
842
|
-
preMigrationState: health.migrations ?? {},
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
// Best-effort — sleeping assistants and unhealthy gateways can still be restarted.
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
return { preMigrationState: {} };
|
|
850
|
-
}
|
|
851
|
-
|
|
852
781
|
async function upgradeLocal(
|
|
853
782
|
entry: AssistantEntry,
|
|
854
783
|
version: string | null,
|
|
@@ -861,6 +790,7 @@ async function upgradeLocal(
|
|
|
861
790
|
process.exit(1);
|
|
862
791
|
}
|
|
863
792
|
|
|
793
|
+
const originalResources = { ...entry.resources };
|
|
864
794
|
const signingKey = resolveExistingLocalSigningKey(entry.resources);
|
|
865
795
|
if (entry.resources.signingKey !== signingKey) {
|
|
866
796
|
entry.resources.signingKey = signingKey;
|
|
@@ -869,10 +799,7 @@ async function upgradeLocal(
|
|
|
869
799
|
|
|
870
800
|
const targetVersion = await targetVersionFromCli(version);
|
|
871
801
|
const adminUrl = localAdminUrl(entry);
|
|
872
|
-
const
|
|
873
|
-
entry,
|
|
874
|
-
adminUrl,
|
|
875
|
-
);
|
|
802
|
+
const currentVersion = await fetchCurrentVersion(adminUrl);
|
|
876
803
|
|
|
877
804
|
if (!currentVersion && targetVersion) {
|
|
878
805
|
console.warn(
|
|
@@ -882,7 +809,7 @@ async function upgradeLocal(
|
|
|
882
809
|
if (currentVersion && targetVersion) {
|
|
883
810
|
const cmp = compareVersions(targetVersion, currentVersion);
|
|
884
811
|
if (cmp !== null && cmp < 0) {
|
|
885
|
-
const msg = `Cannot upgrade to an older version (${targetVersion} < ${currentVersion}). Use \`cadet
|
|
812
|
+
const msg = `Cannot upgrade to an older version (${targetVersion} < ${currentVersion}). Use \`cadet restore --from <backup-path> --version ${targetVersion}\` instead.`;
|
|
886
813
|
console.error(msg);
|
|
887
814
|
emitCliError("VERSION_DIRECTION", msg);
|
|
888
815
|
process.exit(1);
|
|
@@ -938,7 +865,7 @@ async function upgradeLocal(
|
|
|
938
865
|
console.log(` Backup saved: ${backupPath}\n`);
|
|
939
866
|
pruneOldBackups(entry.assistantId, 3);
|
|
940
867
|
} else {
|
|
941
|
-
|
|
868
|
+
throw new Error("Pre-upgrade root backup failed; upgrade aborted before replacing services.");
|
|
942
869
|
}
|
|
943
870
|
|
|
944
871
|
let bootstrapSecret = entry.guardianBootstrapSecret;
|
|
@@ -949,20 +876,8 @@ async function upgradeLocal(
|
|
|
949
876
|
entryChanged = true;
|
|
950
877
|
}
|
|
951
878
|
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
backupPath ||
|
|
955
|
-
currentVersion ||
|
|
956
|
-
preMigrationState.dbVersion !== undefined ||
|
|
957
|
-
preMigrationState.lastWorkspaceMigrationId !== undefined
|
|
958
|
-
) {
|
|
959
|
-
saveAssistantEntry({
|
|
960
|
-
...entry,
|
|
961
|
-
previousVersion: currentVersion,
|
|
962
|
-
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
963
|
-
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
964
|
-
preUpgradeBackupPath: backupPath ?? undefined,
|
|
965
|
-
});
|
|
879
|
+
if (entryChanged) {
|
|
880
|
+
saveAssistantEntry(entry);
|
|
966
881
|
}
|
|
967
882
|
|
|
968
883
|
await broadcastUpgradeEvent(
|
|
@@ -982,64 +897,69 @@ async function upgradeLocal(
|
|
|
982
897
|
runtimeVersion: runtimeInstall.version,
|
|
983
898
|
runtimeInstallDir: runtimeInstall.installDir,
|
|
984
899
|
};
|
|
985
|
-
saveAssistantEntry(
|
|
986
|
-
...entry,
|
|
987
|
-
previousVersion: currentVersion,
|
|
988
|
-
previousDbMigrationVersion: preMigrationState.dbVersion,
|
|
989
|
-
previousWorkspaceMigrationId: preMigrationState.lastWorkspaceMigrationId,
|
|
990
|
-
preUpgradeBackupPath: backupPath ?? undefined,
|
|
991
|
-
});
|
|
900
|
+
saveAssistantEntry(entry);
|
|
992
901
|
console.log(` Runtime installed: ${runtimeInstall.installDir}\n`);
|
|
993
902
|
|
|
994
|
-
console.log("🛑 Stopping local assistant processes...");
|
|
995
|
-
await stopLocalProcesses(entry.resources);
|
|
996
|
-
console.log("✅ Local assistant processes stopped\n");
|
|
997
|
-
|
|
998
|
-
console.log("🚀 Starting upgraded local assistant...");
|
|
999
|
-
const previousAppVersion = process.env.APP_VERSION;
|
|
1000
|
-
process.env.APP_VERSION = stripVersionPrefix(targetVersion);
|
|
1001
903
|
try {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
await Promise.all([
|
|
1006
|
-
startCes(false, entry.resources),
|
|
1007
|
-
startLocalDaemon(false, entry.resources, { signingKey }),
|
|
1008
|
-
startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
|
|
1009
|
-
]);
|
|
1010
|
-
} finally {
|
|
1011
|
-
if (previousAppVersion === undefined) {
|
|
1012
|
-
delete process.env.APP_VERSION;
|
|
1013
|
-
} else {
|
|
1014
|
-
process.env.APP_VERSION = previousAppVersion;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
await ensureGuardianTokenAfterLocalUpgrade(
|
|
1018
|
-
entry,
|
|
1019
|
-
entry.resources.gatewayPort,
|
|
1020
|
-
bootstrapSecret,
|
|
1021
|
-
);
|
|
1022
|
-
console.log("✅ Local assistant processes started\n");
|
|
904
|
+
console.log("🛑 Stopping local assistant processes...");
|
|
905
|
+
await stopLocalProcesses(entry.resources);
|
|
906
|
+
console.log("✅ Local assistant processes stopped\n");
|
|
1023
907
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
908
|
+
console.log("🚀 Starting upgraded local assistant...");
|
|
909
|
+
const previousAppVersion = process.env.APP_VERSION;
|
|
910
|
+
process.env.APP_VERSION = stripVersionPrefix(targetVersion);
|
|
911
|
+
try {
|
|
912
|
+
// Bring CES, daemon, and gateway up in parallel, the way the Docker
|
|
913
|
+
// topology starts its sibling processes together. startCes always
|
|
914
|
+
// launches the CES sibling.
|
|
915
|
+
await Promise.all([
|
|
916
|
+
startCes(false, entry.resources),
|
|
917
|
+
startLocalDaemon(false, entry.resources, { signingKey }),
|
|
918
|
+
startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
|
|
919
|
+
]);
|
|
920
|
+
} finally {
|
|
921
|
+
if (previousAppVersion === undefined) {
|
|
922
|
+
delete process.env.APP_VERSION;
|
|
923
|
+
} else {
|
|
924
|
+
process.env.APP_VERSION = previousAppVersion;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
await ensureGuardianTokenAfterLocalUpgrade(
|
|
928
|
+
entry,
|
|
929
|
+
entry.resources.gatewayPort,
|
|
930
|
+
bootstrapSecret,
|
|
931
|
+
);
|
|
932
|
+
console.log("✅ Local assistant processes started\n");
|
|
1030
933
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
if (!ready) {
|
|
1034
|
-
await broadcastUpgradeEvent(
|
|
1035
|
-
adminUrl,
|
|
934
|
+
const workspaceDir = join(entry.resources.instanceDir, "workspace");
|
|
935
|
+
await restoreTunnelEdgeAndAutoTunnel(
|
|
1036
936
|
entry.assistantId,
|
|
1037
|
-
|
|
937
|
+
entry.resources.gatewayPort,
|
|
938
|
+
workspaceDir,
|
|
1038
939
|
);
|
|
1039
|
-
|
|
1040
|
-
console.
|
|
1041
|
-
|
|
1042
|
-
|
|
940
|
+
|
|
941
|
+
console.log("Waiting for assistant to become ready...");
|
|
942
|
+
const ready = await waitForReady(adminUrl);
|
|
943
|
+
if (!ready) {
|
|
944
|
+
await broadcastUpgradeEvent(
|
|
945
|
+
adminUrl,
|
|
946
|
+
entry.assistantId,
|
|
947
|
+
buildCompleteEvent(currentVersion ?? "unknown", false),
|
|
948
|
+
);
|
|
949
|
+
const msg = `Upgrade to ${targetVersion} failed: local assistant did not become ready.`;
|
|
950
|
+
console.error(`\n❌ ${msg}`);
|
|
951
|
+
throw new Error(msg);
|
|
952
|
+
}
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.error("Local upgrade failed; restoring the previous runtime and service roots...");
|
|
955
|
+
await stopLocalProcesses(entry.resources);
|
|
956
|
+
entry.resources = originalResources;
|
|
957
|
+
saveAssistantEntry(entry);
|
|
958
|
+
const restored = await restoreBackup(adminUrl, entry.assistantId, backupPath, { resume: false });
|
|
959
|
+
if (!restored) throw new Error(`Local recovery failed; services remain stopped. Backup: ${backupPath}`, { cause: error });
|
|
960
|
+
await Promise.all([startCes(false, entry.resources), startLocalDaemon(false, entry.resources, { signingKey }), startGateway(false, entry.resources, { signingKey, bootstrapSecret })]);
|
|
961
|
+
if (!await waitForReady(adminUrl)) throw new Error(`Previous local runtime did not recover. Backup: ${backupPath}`, { cause: error });
|
|
962
|
+
throw error;
|
|
1043
963
|
}
|
|
1044
964
|
|
|
1045
965
|
await broadcastUpgradeEvent(
|
|
@@ -1228,7 +1148,7 @@ async function upgradePlatform(
|
|
|
1228
1148
|
}
|
|
1229
1149
|
|
|
1230
1150
|
if (resolution.isDowngrade && resolution.target && currentVersion) {
|
|
1231
|
-
const msg = `Cannot upgrade to an older version (${resolution.target} < ${currentVersion}). Use \`cadet
|
|
1151
|
+
const msg = `Cannot upgrade to an older version (${resolution.target} < ${currentVersion}). Use \`cadet restore --from <backup-path> --version ${resolution.target}\` instead.`;
|
|
1232
1152
|
console.error(msg);
|
|
1233
1153
|
emitCliError("VERSION_DIRECTION", msg);
|
|
1234
1154
|
process.exit(1);
|
|
@@ -1389,6 +1309,8 @@ async function upgradePrepare(
|
|
|
1389
1309
|
description: `Pre-upgrade snapshot before ${currentVersion} → ${targetVersion}`,
|
|
1390
1310
|
});
|
|
1391
1311
|
|
|
1312
|
+
if (!backupPath) throw new Error("Pre-upgrade root backup failed; update preparation aborted.");
|
|
1313
|
+
|
|
1392
1314
|
// 5. Prune old backups (keep 3)
|
|
1393
1315
|
if (backupPath) {
|
|
1394
1316
|
pruneOldBackups(entry.assistantId, 3);
|
|
@@ -1460,11 +1382,11 @@ async function resolveLatestStableTag(): Promise<string> {
|
|
|
1460
1382
|
const latestVersion = await fetchLatestStableVersion();
|
|
1461
1383
|
if (!latestVersion) {
|
|
1462
1384
|
console.error(
|
|
1463
|
-
"Error: Could not determine the latest stable release from
|
|
1385
|
+
"Error: Could not determine the latest stable release from Cadet Cloud or GitHub Releases.",
|
|
1464
1386
|
);
|
|
1465
1387
|
emitCliError(
|
|
1466
1388
|
"UNKNOWN",
|
|
1467
|
-
"Could not determine the latest stable release from
|
|
1389
|
+
"Could not determine the latest stable release from Cadet Cloud or GitHub Releases",
|
|
1468
1390
|
);
|
|
1469
1391
|
process.exit(1);
|
|
1470
1392
|
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -201,7 +201,7 @@ class WakeExit extends Error {}
|
|
|
201
201
|
* refusals so the caller decides between exiting (single path) and
|
|
202
202
|
* continuing to the next entry (`--all`).
|
|
203
203
|
*/
|
|
204
|
-
async function wakeAssistant(
|
|
204
|
+
export async function wakeAssistant(
|
|
205
205
|
entry: AssistantEntry,
|
|
206
206
|
opts: WakeAssistantOptions,
|
|
207
207
|
): Promise<void> {
|
package/src/index.ts
CHANGED
|
@@ -31,7 +31,6 @@ import { ps } from "./commands/ps";
|
|
|
31
31
|
import { restore } from "./commands/restore";
|
|
32
32
|
import { retire } from "./commands/retire";
|
|
33
33
|
import { restart } from "./commands/restart";
|
|
34
|
-
import { rollback } from "./commands/rollback";
|
|
35
34
|
import { setup } from "./commands/setup";
|
|
36
35
|
import { sleep } from "./commands/sleep";
|
|
37
36
|
import { debug } from "./commands/debug";
|
|
@@ -39,7 +38,6 @@ import { teleport } from "./commands/teleport";
|
|
|
39
38
|
import { terminal } from "./commands/terminal";
|
|
40
39
|
import { tunnel } from "./commands/tunnel";
|
|
41
40
|
import { uninstall } from "./commands/uninstall";
|
|
42
|
-
import { unpair } from "./commands/unpair";
|
|
43
41
|
import { upgrade } from "./commands/upgrade";
|
|
44
42
|
import { use } from "./commands/use";
|
|
45
43
|
import { voice } from "./commands/voice";
|
|
@@ -73,14 +71,12 @@ const commands = {
|
|
|
73
71
|
restore,
|
|
74
72
|
retire,
|
|
75
73
|
restart,
|
|
76
|
-
rollback,
|
|
77
74
|
setup,
|
|
78
75
|
sleep,
|
|
79
76
|
teleport,
|
|
80
77
|
terminal,
|
|
81
78
|
tunnel,
|
|
82
79
|
uninstall,
|
|
83
|
-
unpair,
|
|
84
80
|
upgrade,
|
|
85
81
|
use,
|
|
86
82
|
voice,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { afterAll, afterEach, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { fetchCurrentVersion } from "../http-client.js";
|
|
3
|
+
|
|
4
|
+
const request = spyOn(globalThis, "fetch");
|
|
5
|
+
afterEach(() => request.mockReset());
|
|
6
|
+
afterAll(() => request.mockRestore());
|
|
7
|
+
|
|
8
|
+
test("reads the gateway's version through the public health endpoint", async () => {
|
|
9
|
+
request.mockResolvedValueOnce(
|
|
10
|
+
Response.json({ version: "1.2.3", status: "ok" }),
|
|
11
|
+
);
|
|
12
|
+
expect(await fetchCurrentVersion("http://127.0.0.1:7830")).toBe("1.2.3");
|
|
13
|
+
expect(request.mock.calls[0][0]).toBe("http://127.0.0.1:7830/healthz");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("an unverified gateway response never supplies a release version", async () => {
|
|
17
|
+
for (const value of [{}, null, { version: "" }, { version: 123 }]) {
|
|
18
|
+
request.mockResolvedValueOnce(Response.json(value));
|
|
19
|
+
expect(await fetchCurrentVersion("http://127.0.0.1:7830")).toBeUndefined();
|
|
20
|
+
}
|
|
21
|
+
request.mockResolvedValueOnce(new Response("not JSON"));
|
|
22
|
+
expect(await fetchCurrentVersion("http://127.0.0.1:7830")).toBeUndefined();
|
|
23
|
+
request.mockResolvedValueOnce(
|
|
24
|
+
Response.json({ version: "1.2.3" }, { status: 503 }),
|
|
25
|
+
);
|
|
26
|
+
expect(await fetchCurrentVersion("http://127.0.0.1:7830")).toBeUndefined();
|
|
27
|
+
request.mockRejectedValueOnce(new Error("offline"));
|
|
28
|
+
expect(await fetchCurrentVersion("http://127.0.0.1:7830")).toBeUndefined();
|
|
29
|
+
});
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type { AssistantEntry } from "../assistant-config.js";
|
|
4
|
-
import {
|
|
5
|
-
resolveRuntimeMigrationUrl,
|
|
6
|
-
resolveRuntimeUrl,
|
|
7
|
-
} from "../runtime-url.js";
|
|
4
|
+
import { resolveRuntimeUrl } from "../runtime-url.js";
|
|
8
5
|
|
|
9
6
|
function makeEntry(
|
|
10
7
|
overrides: Partial<AssistantEntry> & {
|
|
@@ -20,75 +17,6 @@ function makeEntry(
|
|
|
20
17
|
};
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
describe("resolveRuntimeMigrationUrl", () => {
|
|
24
|
-
test("local cloud uses gateway-loopback /v1/migrations/<subpath>", () => {
|
|
25
|
-
const entry = makeEntry({
|
|
26
|
-
cloud: "local",
|
|
27
|
-
runtimeUrl: "http://localhost:7821",
|
|
28
|
-
assistantId: "ast-local-1",
|
|
29
|
-
});
|
|
30
|
-
expect(resolveRuntimeMigrationUrl(entry, "export-to-gcs")).toBe(
|
|
31
|
-
"http://localhost:7821/v1/migrations/export-to-gcs",
|
|
32
|
-
);
|
|
33
|
-
expect(resolveRuntimeMigrationUrl(entry, "import-from-gcs")).toBe(
|
|
34
|
-
"http://localhost:7821/v1/migrations/import-from-gcs",
|
|
35
|
-
);
|
|
36
|
-
expect(resolveRuntimeMigrationUrl(entry, "jobs/job-abc")).toBe(
|
|
37
|
-
"http://localhost:7821/v1/migrations/jobs/job-abc",
|
|
38
|
-
);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("docker cloud uses gateway-loopback /v1/migrations/<subpath>", () => {
|
|
42
|
-
const entry = makeEntry({
|
|
43
|
-
cloud: "docker",
|
|
44
|
-
runtimeUrl: "http://localhost:7831",
|
|
45
|
-
assistantId: "ast-docker-1",
|
|
46
|
-
});
|
|
47
|
-
expect(resolveRuntimeMigrationUrl(entry, "export-to-gcs")).toBe(
|
|
48
|
-
"http://localhost:7831/v1/migrations/export-to-gcs",
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("cadet (cloud-managed) cloud uses wildcard-proxy /v1/assistants/<id>/migrations/<subpath>", () => {
|
|
53
|
-
const entry = makeEntry({
|
|
54
|
-
cloud: "cadet",
|
|
55
|
-
runtimeUrl: "https://cloud.mycadet.ai",
|
|
56
|
-
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
57
|
-
});
|
|
58
|
-
expect(resolveRuntimeMigrationUrl(entry, "export-to-gcs")).toBe(
|
|
59
|
-
"https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/export-to-gcs",
|
|
60
|
-
);
|
|
61
|
-
expect(resolveRuntimeMigrationUrl(entry, "import-from-gcs")).toBe(
|
|
62
|
-
"https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/import-from-gcs",
|
|
63
|
-
);
|
|
64
|
-
expect(resolveRuntimeMigrationUrl(entry, "jobs/job-xyz")).toBe(
|
|
65
|
-
"https://cloud.mycadet.ai/v1/assistants/11111111-2222-3333-4444-555555555555/migrations/jobs/job-xyz",
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("dev platform URL still routes through the wildcard prefix", () => {
|
|
70
|
-
const entry = makeEntry({
|
|
71
|
-
cloud: "cadet",
|
|
72
|
-
runtimeUrl: "https://dev-cloud.mycadet.ai",
|
|
73
|
-
assistantId: "ast-dev-1",
|
|
74
|
-
});
|
|
75
|
-
expect(resolveRuntimeMigrationUrl(entry, "export-to-gcs")).toBe(
|
|
76
|
-
"https://dev-cloud.mycadet.ai/v1/assistants/ast-dev-1/migrations/export-to-gcs",
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("a non-cadet, non-local cloud (e.g. gcp) uses the local-shape URL", () => {
|
|
81
|
-
const entry = makeEntry({
|
|
82
|
-
cloud: "gcp",
|
|
83
|
-
runtimeUrl: "http://10.0.0.5:7821",
|
|
84
|
-
assistantId: "ast-gcp-1",
|
|
85
|
-
});
|
|
86
|
-
expect(resolveRuntimeMigrationUrl(entry, "export-to-gcs")).toBe(
|
|
87
|
-
"http://10.0.0.5:7821/v1/migrations/export-to-gcs",
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
20
|
describe("resolveRuntimeUrl", () => {
|
|
93
21
|
test("local cloud uses gateway-loopback /v1/<subpath>", () => {
|
|
94
22
|
const entry = makeEntry({
|
|
@@ -112,9 +40,9 @@ describe("resolveRuntimeUrl", () => {
|
|
|
112
40
|
);
|
|
113
41
|
});
|
|
114
42
|
|
|
115
|
-
test("
|
|
43
|
+
test("cloud home uses wildcard-proxy /v1/assistants/<id>/<subpath>", () => {
|
|
116
44
|
const entry = makeEntry({
|
|
117
|
-
cloud: "
|
|
45
|
+
cloud: "cloud",
|
|
118
46
|
runtimeUrl: "https://cloud.mycadet.ai",
|
|
119
47
|
assistantId: "11111111-2222-3333-4444-555555555555",
|
|
120
48
|
});
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { findWebDistDir } from "../web-dist.js";
|
|
6
|
+
import { findWebDistDir, readWebReleaseVersion } from "../web-dist.js";
|
|
7
7
|
|
|
8
8
|
const tempDirs: string[] = [];
|
|
9
9
|
|
|
@@ -84,3 +84,29 @@ test("preserves installed package and source checkout resolution", () => {
|
|
|
84
84
|
}),
|
|
85
85
|
).toBe(sourceDist);
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
test("reads the built UI release, including a normalized version prefix", () => {
|
|
89
|
+
const root = makeTempDir();
|
|
90
|
+
writeFileSync(
|
|
91
|
+
join(root, "release.json"),
|
|
92
|
+
JSON.stringify({ version: "v1.2.3-dev.5" }),
|
|
93
|
+
);
|
|
94
|
+
expect(readWebReleaseVersion(root)).toBe("1.2.3-dev.5");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("missing or malformed release metadata cannot stand in for a release", () => {
|
|
98
|
+
const root = makeTempDir();
|
|
99
|
+
expect(readWebReleaseVersion(root)).toBeUndefined();
|
|
100
|
+
for (const value of [
|
|
101
|
+
"not JSON",
|
|
102
|
+
"null",
|
|
103
|
+
"{}",
|
|
104
|
+
'{"version":null}',
|
|
105
|
+
'{"version":"latest"}',
|
|
106
|
+
'{"version":""}',
|
|
107
|
+
'{"version":"bundle-hash"}',
|
|
108
|
+
]) {
|
|
109
|
+
writeFileSync(join(root, "release.json"), value);
|
|
110
|
+
expect(readWebReleaseVersion(root)).toBeUndefined();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
@@ -334,7 +334,7 @@ export class AssistantClient {
|
|
|
334
334
|
|
|
335
335
|
// A stale cached Cadet-Organization-Id is the usual cause of a 401 on the
|
|
336
336
|
// platform path (e.g. the user switched orgs elsewhere). Drop it and retry
|
|
337
|
-
// once.
|
|
337
|
+
// once.
|
|
338
338
|
if (response.status === 401 && this.authMode === "platform" && this.token) {
|
|
339
339
|
invalidateOrgIdCache(this.token, this.runtimeUrl);
|
|
340
340
|
this.platformHeaders = undefined;
|
|
@@ -103,16 +103,11 @@ export type AssistantEntry = Omit<
|
|
|
103
103
|
/** Loopback URL for same-machine health checks (e.g. `http://127.0.0.1:7831`).
|
|
104
104
|
* Avoids mDNS resolution issues when the machine checks its own gateway. */
|
|
105
105
|
localUrl?: string;
|
|
106
|
-
/** Public https ingress URL recorded by `cadet tunnel` providers for this
|
|
107
|
-
* instance. The advertised-URL default for remote-web pairing. */
|
|
108
|
-
ingressUrl?: string;
|
|
109
106
|
bearerToken?: string;
|
|
110
107
|
/** True when this entry was registered via `cadet connect import` (a remote
|
|
111
108
|
* pairing). Set alongside `cloud: "paired"`; also backs the re-import /
|
|
112
109
|
* overwrite guard in connect import. */
|
|
113
110
|
paired?: boolean;
|
|
114
|
-
instanceId?: string;
|
|
115
|
-
namespace?: string;
|
|
116
111
|
project?: string;
|
|
117
112
|
region?: string;
|
|
118
113
|
sshUser?: string;
|
|
@@ -123,19 +118,6 @@ export type AssistantEntry = Omit<
|
|
|
123
118
|
guardianBootstrapSecret?: string;
|
|
124
119
|
/** Docker image metadata for rollback. Only present for docker topology entries. */
|
|
125
120
|
containerInfo?: ContainerInfo;
|
|
126
|
-
/** Docker image metadata from before the last upgrade. Enables rollback to the prior version. */
|
|
127
|
-
previousContainerInfo?: ContainerInfo;
|
|
128
|
-
/** Path to the .vbundle backup created for the most recent upgrade. Used by rollback to restore
|
|
129
|
-
* only the backup from the specific upgrade being rolled back — never a stale backup from a
|
|
130
|
-
* previous upgrade cycle. */
|
|
131
|
-
preUpgradeBackupPath?: string;
|
|
132
|
-
/** Running version of the service group at the time of the last upgrade, as reported by
|
|
133
|
-
* the health endpoint. Used by saved-state rollback for logging / broadcast events. */
|
|
134
|
-
previousVersion?: string;
|
|
135
|
-
/** Pre-upgrade DB migration version — used by rollback to know how far back to revert. */
|
|
136
|
-
previousDbMigrationVersion?: number;
|
|
137
|
-
/** Pre-upgrade workspace migration ID — used by rollback to know how far back to revert. */
|
|
138
|
-
previousWorkspaceMigrationId?: string;
|
|
139
121
|
/** Client-minted uuid (web's temp-client-id pattern) the CLI addresses this
|
|
140
122
|
* assistant's chat conversation by. Minted once on first `cadet client`
|
|
141
123
|
* launch and persisted here so every session shares one conversation. */
|