@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.
Files changed (86) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/package.json +3 -2
  9. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  10. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  11. package/src/__tests__/backup-ops.test.ts +105 -0
  12. package/src/__tests__/backup.test.ts +114 -673
  13. package/src/__tests__/cloud-releases.test.ts +82 -0
  14. package/src/__tests__/commands-mock.ts +101 -0
  15. package/src/__tests__/debug.test.ts +17 -91
  16. package/src/__tests__/devices.test.ts +2 -2
  17. package/src/__tests__/gateway-token.test.ts +267 -0
  18. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  19. package/src/__tests__/host-teleport.test.ts +405 -0
  20. package/src/__tests__/nginx-ingress.test.ts +182 -23
  21. package/src/__tests__/orphan-detection.test.ts +1 -1
  22. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  23. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  24. package/src/__tests__/root-backup-docker.test.ts +294 -0
  25. package/src/__tests__/root-backup.test.ts +527 -0
  26. package/src/__tests__/root-restore-command.test.ts +147 -0
  27. package/src/__tests__/root-signing-key.test.ts +50 -0
  28. package/src/__tests__/setup.test.ts +3 -3
  29. package/src/__tests__/sleep.test.ts +11 -0
  30. package/src/__tests__/teleport.test.ts +68 -2996
  31. package/src/__tests__/tunnel.test.ts +1 -1
  32. package/src/__tests__/upgrade-local.test.ts +21 -11
  33. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  34. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  35. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  36. package/src/commands/backup.ts +44 -399
  37. package/src/commands/client.ts +3 -3
  38. package/src/commands/completions.ts +0 -2
  39. package/src/commands/debug.ts +75 -131
  40. package/src/commands/devices.ts +51 -17
  41. package/src/commands/gateway/token.ts +77 -23
  42. package/src/commands/pair.ts +2 -2
  43. package/src/commands/restore.ts +77 -444
  44. package/src/commands/retire.ts +2 -3
  45. package/src/commands/sleep.ts +15 -2
  46. package/src/commands/teleport.ts +47 -1559
  47. package/src/commands/upgrade.ts +78 -156
  48. package/src/commands/wake.ts +1 -1
  49. package/src/index.ts +0 -4
  50. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  51. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  52. package/src/lib/__tests__/web-dist.test.ts +27 -1
  53. package/src/lib/assistant-client.ts +1 -1
  54. package/src/lib/assistant-config.ts +0 -18
  55. package/src/lib/backup-ops.ts +37 -251
  56. package/src/lib/cloud-client.ts +10 -13
  57. package/src/lib/cloud-login.ts +2 -1
  58. package/src/lib/cloud-releases.ts +54 -10
  59. package/src/lib/cloud-values.ts +1 -1
  60. package/src/lib/confirm-action.ts +2 -2
  61. package/src/lib/docker.ts +59 -22
  62. package/src/lib/github-repository.ts +2 -0
  63. package/src/lib/guardian-token.ts +1 -23
  64. package/src/lib/hatch-local.ts +17 -1
  65. package/src/lib/host-teleport.ts +506 -0
  66. package/src/lib/http-client.ts +25 -0
  67. package/src/lib/live-voice/tunnel.test.ts +1 -1
  68. package/src/lib/live-voice/tunnel.ts +1 -1
  69. package/src/lib/local.ts +4 -2
  70. package/src/lib/nginx-ingress.ts +134 -18
  71. package/src/lib/process.ts +13 -0
  72. package/src/lib/root-backup.ts +917 -0
  73. package/src/lib/root-signing-key.ts +31 -0
  74. package/src/lib/runtime-url.ts +0 -29
  75. package/src/lib/teleport-backup.ts +3 -16
  76. package/src/lib/upgrade-lifecycle.ts +63 -141
  77. package/src/lib/web-dist.ts +23 -1
  78. package/src/__tests__/bundle-staging.test.ts +0 -268
  79. package/src/__tests__/unpair.test.ts +0 -160
  80. package/src/commands/rollback.ts +0 -479
  81. package/src/commands/unpair.ts +0 -122
  82. package/src/lib/__tests__/job-polling.test.ts +0 -278
  83. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  84. package/src/lib/bundle-staging.ts +0 -220
  85. package/src/lib/job-polling.ts +0 -195
  86. package/src/lib/local-runtime-client.ts +0 -374
@@ -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 rollback`.");
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
- // Users should use `cadet rollback --version <version>` for downgrades.
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 rollback --version ${versionTag}\` instead.`;
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
- // Create pre-upgrade backup (best-effort, daemon must be running)
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
- console.warn("⚠️ Pre-upgrade backup failed, continuing with upgrade\n");
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
- { kind: "docker", assistantId: entry.assistantId },
625
+ {},
656
626
  );
657
627
  if (restored) {
658
628
  console.log(" ✅ Data restored successfully\n");
659
629
  } else {
660
- console.warn(
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 { currentVersion, preMigrationState } = await fetchLocalUpgradeState(
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 rollback --version ${targetVersion}\` instead.`;
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
- console.warn("⚠️ Pre-upgrade backup failed, continuing with upgrade\n");
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
- entryChanged ||
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
- // Bring CES, daemon, and gateway up in parallel, the way the Docker
1003
- // topology starts its sibling processes together. startCes always
1004
- // launches the CES sibling.
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
- const workspaceDir = join(entry.resources.instanceDir, "workspace");
1025
- await restoreTunnelEdgeAndAutoTunnel(
1026
- entry.assistantId,
1027
- entry.resources.gatewayPort,
1028
- workspaceDir,
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
- console.log("Waiting for assistant to become ready...");
1032
- const ready = await waitForReady(adminUrl);
1033
- if (!ready) {
1034
- await broadcastUpgradeEvent(
1035
- adminUrl,
934
+ const workspaceDir = join(entry.resources.instanceDir, "workspace");
935
+ await restoreTunnelEdgeAndAutoTunnel(
1036
936
  entry.assistantId,
1037
- buildCompleteEvent(currentVersion ?? "unknown", false),
937
+ entry.resources.gatewayPort,
938
+ workspaceDir,
1038
939
  );
1039
- const msg = `Upgrade to ${targetVersion} failed: local assistant did not become ready.`;
1040
- console.error(`\n❌ ${msg}`);
1041
- emitCliError("READINESS_TIMEOUT", msg);
1042
- process.exit(1);
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 rollback --version ${resolution.target}\` instead.`;
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 the Cadet Cloud API.",
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 the cloud API",
1389
+ "Could not determine the latest stable release from Cadet Cloud or GitHub Releases",
1468
1390
  );
1469
1391
  process.exit(1);
1470
1392
  }
@@ -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("cadet cloud uses wildcard-proxy /v1/assistants/<id>/<subpath>", () => {
43
+ test("cloud home uses wildcard-proxy /v1/assistants/<id>/<subpath>", () => {
116
44
  const entry = makeEntry({
117
- cloud: "cadet",
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. Mirrors localRuntimeIdentity in local-runtime-client.ts.
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. */