@mycadet/cli 0.12.11 → 0.13.0-dev.202610171617.9ea363e
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/channels.ts +0 -1
- 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/channels.ts +0 -1
- 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/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- package/package.json +8 -3
- 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 +536 -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__/shell-mode.test.ts +83 -0
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +10 -2
- 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__/wake.test.ts +4 -1
- 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/tunnel.ts +48 -7
- 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 +23 -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/ingress-config.ts +31 -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 +454 -79
- 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/tunnel.ts +1 -0
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +79 -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/tunnel.ts
CHANGED
|
@@ -20,9 +20,11 @@ import {
|
|
|
20
20
|
getDefaultWorkspaceDir,
|
|
21
21
|
isLocalContainerEntry,
|
|
22
22
|
parseGatewayPortFromEntryUrls,
|
|
23
|
+
saveShellMode,
|
|
23
24
|
} from "../lib/ingress-config.js";
|
|
24
25
|
import {
|
|
25
26
|
ensureTunnelEdge,
|
|
27
|
+
formatEdgeMode,
|
|
26
28
|
getNginxVersion,
|
|
27
29
|
type TunnelEdge,
|
|
28
30
|
} from "../lib/nginx-ingress.js";
|
|
@@ -51,9 +53,10 @@ interface TunnelArgs {
|
|
|
51
53
|
domain: string | null;
|
|
52
54
|
clearDomain: boolean;
|
|
53
55
|
foreground: boolean;
|
|
56
|
+
shell: "web" | "app" | null;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
|
|
59
|
+
const FLAGS_WITH_VALUES = ["--provider", "--domain", "--shell"] as const;
|
|
57
60
|
|
|
58
61
|
function parseArgs(): TunnelArgs {
|
|
59
62
|
const args = process.argv.slice(3);
|
|
@@ -61,6 +64,7 @@ function parseArgs(): TunnelArgs {
|
|
|
61
64
|
let domain: string | null = null;
|
|
62
65
|
let clearDomain = false;
|
|
63
66
|
let foreground = false;
|
|
67
|
+
let shell: "web" | "app" | null = null;
|
|
64
68
|
|
|
65
69
|
for (let i = 0; i < args.length; i++) {
|
|
66
70
|
const arg = args[i];
|
|
@@ -70,9 +74,7 @@ function parseArgs(): TunnelArgs {
|
|
|
70
74
|
);
|
|
71
75
|
console.log("");
|
|
72
76
|
console.log("Front a locally running assistant with an HTTPS tunnel.");
|
|
73
|
-
console.log(
|
|
74
|
-
"The tunnel URL is saved to the workspace config as the ingress base URL.",
|
|
75
|
-
);
|
|
77
|
+
console.log("The tunnel URL is saved to the workspace config as the ingress base URL.");
|
|
76
78
|
console.log("");
|
|
77
79
|
console.log(
|
|
78
80
|
"How far that URL reaches depends on the provider; omit --provider for",
|
|
@@ -114,6 +116,19 @@ function parseArgs(): TunnelArgs {
|
|
|
114
116
|
console.log(
|
|
115
117
|
" tunnel without one. Cannot be combined with --domain.",
|
|
116
118
|
);
|
|
119
|
+
console.log(
|
|
120
|
+
" --shell <web|app> Which client bundle the edge serves as the outer shell:",
|
|
121
|
+
);
|
|
122
|
+
console.log(
|
|
123
|
+
" web (default) serves the web SPA directly; app serves the",
|
|
124
|
+
);
|
|
125
|
+
console.log(
|
|
126
|
+
" app bundle as the outer shell and iframes the web UI from",
|
|
127
|
+
);
|
|
128
|
+
console.log(
|
|
129
|
+
" /web/assistant/. Saved to the workspace config so `cadet wake`",
|
|
130
|
+
);
|
|
131
|
+
console.log(" restores it; changing it restarts the nginx edge.");
|
|
117
132
|
console.log(
|
|
118
133
|
` --foreground Stream the tunnel's logs to this terminal. By default the tunnel`,
|
|
119
134
|
);
|
|
@@ -191,6 +206,20 @@ function parseArgs(): TunnelArgs {
|
|
|
191
206
|
i++;
|
|
192
207
|
} else if (arg === "--clear-domain") {
|
|
193
208
|
clearDomain = true;
|
|
209
|
+
} else if (arg === "--shell") {
|
|
210
|
+
const next = args[i + 1];
|
|
211
|
+
if (!next || next.startsWith("-")) {
|
|
212
|
+
console.error("Error: --shell requires a value: web or app.");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
if (next !== "web" && next !== "app") {
|
|
216
|
+
console.error(
|
|
217
|
+
`Error: unknown shell '${next}'. Valid shells: web, app.`,
|
|
218
|
+
);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
shell = next;
|
|
222
|
+
i++;
|
|
194
223
|
} else if (arg === "--foreground") {
|
|
195
224
|
foreground = true;
|
|
196
225
|
} else if (arg.startsWith("-")) {
|
|
@@ -230,7 +259,7 @@ function parseArgs(): TunnelArgs {
|
|
|
230
259
|
process.exit(1);
|
|
231
260
|
}
|
|
232
261
|
|
|
233
|
-
return { assistantName, provider, domain, clearDomain, foreground };
|
|
262
|
+
return { assistantName, provider, domain, clearDomain, foreground, shell };
|
|
234
263
|
}
|
|
235
264
|
|
|
236
265
|
/** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
|
|
@@ -456,7 +485,7 @@ export const tunnel = Object.assign(
|
|
|
456
485
|
);
|
|
457
486
|
return;
|
|
458
487
|
}
|
|
459
|
-
const { assistantName, provider, domain, clearDomain, foreground } =
|
|
488
|
+
const { assistantName, provider, domain, clearDomain, foreground, shell } =
|
|
460
489
|
parseArgs();
|
|
461
490
|
|
|
462
491
|
if (!foreground) {
|
|
@@ -474,6 +503,15 @@ export const tunnel = Object.assign(
|
|
|
474
503
|
const preserveIngressUrl =
|
|
475
504
|
provider === "tailscale" && guardTailnetOnlyWebhookIngress(workspaceDir);
|
|
476
505
|
|
|
506
|
+
// Persist the shell mode like the reserved domain: parsed once, saved to
|
|
507
|
+
// the CLI lockfile beside the nginx edge record it configures, and read
|
|
508
|
+
// back by every later edge restore (wake/upgrade), so the choice outlives
|
|
509
|
+
// the command that made it.
|
|
510
|
+
if (shell) {
|
|
511
|
+
saveShellMode(shell);
|
|
512
|
+
console.log(`Shell mode saved: ${shell}.`);
|
|
513
|
+
}
|
|
514
|
+
|
|
477
515
|
if (clearDomain) {
|
|
478
516
|
clearReservedDomain();
|
|
479
517
|
console.log("Cleared the saved reserved domain.");
|
|
@@ -514,7 +552,10 @@ export const tunnel = Object.assign(
|
|
|
514
552
|
}
|
|
515
553
|
|
|
516
554
|
console.log(
|
|
517
|
-
`${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port}
|
|
555
|
+
`${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port} (${formatEdgeMode(
|
|
556
|
+
edge.includesWebApp,
|
|
557
|
+
edge.shellMode,
|
|
558
|
+
)}).`,
|
|
518
559
|
);
|
|
519
560
|
|
|
520
561
|
const baseTunnelOpts = {
|
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
|
+
});
|