@mycadet/cli 0.12.10 ā 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 +10 -10
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- 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-login-poll.test.ts +69 -0
- 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 +64 -2
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/gateway-token.test.ts +267 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/host-teleport-docker.test.ts +371 -0
- package/src/__tests__/host-teleport.test.ts +405 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +233 -36
- 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 +8 -7
- 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 +17 -7
- package/src/__tests__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +13 -3
- 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 +117 -47
- package/src/commands/gateway/token.ts +77 -23
- package/src/commands/hatch.ts +10 -52
- package/src/commands/pair.ts +9 -3
- package/src/commands/ps.ts +4 -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 +43 -4
- package/src/commands/upgrade.ts +84 -157
- 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 +20 -23
- package/src/lib/backup-ops.ts +37 -251
- package/src/lib/cloud-client.ts +10 -13
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +5 -2
- 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 +65 -34
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/github-repository.ts +2 -0
- package/src/lib/guardian-token.ts +1 -23
- package/src/lib/hatch-local.ts +18 -52
- package/src/lib/hatch-next-steps.ts +15 -2
- 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 +3 -3
- package/src/lib/local.ts +10 -8
- package/src/lib/nginx-ingress.ts +254 -36
- 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 +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -1
- package/src/lib/xdg-log.ts +11 -0
- 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
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `cadet unpair <name>`: forget a paired (cloud:"paired") assistant
|
|
3
|
-
* by removing its lockfile entry + guardian token. Refuses non-paired entries.
|
|
4
|
-
*/
|
|
5
|
-
import {
|
|
6
|
-
afterAll,
|
|
7
|
-
afterEach,
|
|
8
|
-
beforeEach,
|
|
9
|
-
describe,
|
|
10
|
-
expect,
|
|
11
|
-
spyOn,
|
|
12
|
-
test,
|
|
13
|
-
} from "bun:test";
|
|
14
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
|
|
18
|
-
const testDir = mkdtempSync(join(tmpdir(), "unpair-test-"));
|
|
19
|
-
const ORIGINAL_LOCKFILE_DIR = process.env.CADET_LOCKFILE_DIR;
|
|
20
|
-
const ORIGINAL_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
|
|
21
|
-
const ORIGINAL_ARGV = [...process.argv];
|
|
22
|
-
|
|
23
|
-
import { unpair } from "../commands/unpair.js";
|
|
24
|
-
import {
|
|
25
|
-
findAssistantByName,
|
|
26
|
-
saveAssistantEntry,
|
|
27
|
-
} from "../lib/assistant-config.js";
|
|
28
|
-
import {
|
|
29
|
-
deleteGuardianToken,
|
|
30
|
-
loadGuardianToken,
|
|
31
|
-
saveGuardianToken,
|
|
32
|
-
} from "../lib/guardian-token.js";
|
|
33
|
-
|
|
34
|
-
function seedToken(assistantId: string): void {
|
|
35
|
-
saveGuardianToken(assistantId, {
|
|
36
|
-
guardianPrincipalId: "imported",
|
|
37
|
-
accessToken: "acc",
|
|
38
|
-
accessTokenExpiresAt: Date.now() + 3_600_000,
|
|
39
|
-
refreshToken: "ref",
|
|
40
|
-
refreshTokenExpiresAt: Date.now() + 3_600_000,
|
|
41
|
-
refreshAfter: "",
|
|
42
|
-
isNew: false,
|
|
43
|
-
deviceId: "dev",
|
|
44
|
-
leasedAt: new Date().toISOString(),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Run unpair with console.error + process.exit spied. */
|
|
49
|
-
async function runUnpair(): Promise<{ exited: boolean; errors: string }> {
|
|
50
|
-
const errors: string[] = [];
|
|
51
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
52
|
-
const errSpy = spyOn(console, "error").mockImplementation(
|
|
53
|
-
(...a: unknown[]) => {
|
|
54
|
-
errors.push(a.join(" "));
|
|
55
|
-
},
|
|
56
|
-
);
|
|
57
|
-
const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
|
|
58
|
-
throw new Error(`exit:${c}`);
|
|
59
|
-
}) as never);
|
|
60
|
-
let exited = false;
|
|
61
|
-
try {
|
|
62
|
-
await unpair();
|
|
63
|
-
} catch (e) {
|
|
64
|
-
exited = (e as Error).message === "exit:1";
|
|
65
|
-
} finally {
|
|
66
|
-
logSpy.mockRestore();
|
|
67
|
-
errSpy.mockRestore();
|
|
68
|
-
exitSpy.mockRestore();
|
|
69
|
-
}
|
|
70
|
-
return { exited, errors: errors.join("\n") };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
describe("cadet unpair", () => {
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
process.env.CADET_LOCKFILE_DIR = testDir;
|
|
76
|
-
process.env.XDG_CONFIG_HOME = testDir;
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
process.argv = [...ORIGINAL_ARGV];
|
|
81
|
-
if (ORIGINAL_LOCKFILE_DIR === undefined)
|
|
82
|
-
delete process.env.CADET_LOCKFILE_DIR;
|
|
83
|
-
else process.env.CADET_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
|
|
84
|
-
if (ORIGINAL_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
85
|
-
else process.env.XDG_CONFIG_HOME = ORIGINAL_CONFIG_HOME;
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
afterAll(() => {
|
|
89
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("removes a paired entry's lockfile entry and guardian token", async () => {
|
|
93
|
-
saveAssistantEntry({
|
|
94
|
-
assistantId: "px",
|
|
95
|
-
name: "Paired Box",
|
|
96
|
-
runtimeUrl: "http://10.0.0.9:7830",
|
|
97
|
-
cloud: "paired",
|
|
98
|
-
paired: true,
|
|
99
|
-
});
|
|
100
|
-
seedToken("px");
|
|
101
|
-
expect(findAssistantByName("px")).not.toBeNull();
|
|
102
|
-
expect(loadGuardianToken("px")).not.toBeNull();
|
|
103
|
-
|
|
104
|
-
// Non-interactive test env ā use --yes to bypass the confirmation prompt.
|
|
105
|
-
process.argv = ["bun", "cadet", "unpair", "px", "--yes"];
|
|
106
|
-
const { exited } = await runUnpair();
|
|
107
|
-
|
|
108
|
-
expect(exited).toBe(false);
|
|
109
|
-
expect(findAssistantByName("px")).toBeNull();
|
|
110
|
-
expect(loadGuardianToken("px")).toBeNull();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("refuses to unpair without --yes in a non-interactive terminal", async () => {
|
|
114
|
-
saveAssistantEntry({
|
|
115
|
-
assistantId: "py",
|
|
116
|
-
name: "Paired Two",
|
|
117
|
-
runtimeUrl: "http://10.0.0.9:7830",
|
|
118
|
-
cloud: "paired",
|
|
119
|
-
paired: true,
|
|
120
|
-
});
|
|
121
|
-
seedToken("py");
|
|
122
|
-
|
|
123
|
-
process.argv = ["bun", "cadet", "unpair", "py"]; // no --yes
|
|
124
|
-
const { exited, errors } = await runUnpair();
|
|
125
|
-
|
|
126
|
-
expect(exited).toBe(true);
|
|
127
|
-
expect(errors).toContain("--yes");
|
|
128
|
-
// Not removed ā confirmation was required.
|
|
129
|
-
expect(findAssistantByName("py")).not.toBeNull();
|
|
130
|
-
expect(loadGuardianToken("py")).not.toBeNull();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("refuses a non-paired (local) assistant and leaves it intact", async () => {
|
|
134
|
-
saveAssistantEntry({
|
|
135
|
-
assistantId: "desk",
|
|
136
|
-
name: "Desk",
|
|
137
|
-
runtimeUrl: "http://127.0.0.1:7830",
|
|
138
|
-
cloud: "local",
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
process.argv = ["bun", "cadet", "unpair", "desk"];
|
|
142
|
-
const { exited, errors } = await runUnpair();
|
|
143
|
-
|
|
144
|
-
expect(exited).toBe(true);
|
|
145
|
-
expect(errors).toContain("cadet retire");
|
|
146
|
-
expect(findAssistantByName("desk")).not.toBeNull(); // untouched
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("errors on an unknown name", async () => {
|
|
150
|
-
process.argv = ["bun", "cadet", "unpair", "nope"];
|
|
151
|
-
const { exited } = await runUnpair();
|
|
152
|
-
expect(exited).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("deleteGuardianToken is a no-op when the token is absent", () => {
|
|
156
|
-
// No token seeded for this id; calling (twice) must not throw.
|
|
157
|
-
expect(() => deleteGuardianToken("ghost")).not.toThrow();
|
|
158
|
-
expect(() => deleteGuardianToken("ghost")).not.toThrow();
|
|
159
|
-
});
|
|
160
|
-
});
|
package/src/commands/rollback.ts
DELETED
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
import { isCloudHome } from "../lib/cloud-values.js";
|
|
2
|
-
import {
|
|
3
|
-
findAssistantByName,
|
|
4
|
-
getActiveAssistant,
|
|
5
|
-
loadAllAssistants,
|
|
6
|
-
saveAssistantEntry,
|
|
7
|
-
type AssistantEntry,
|
|
8
|
-
} from "../lib/assistant-config";
|
|
9
|
-
import {
|
|
10
|
-
GATEWAY_INTERNAL_PORT,
|
|
11
|
-
ASSISTANT_INTERNAL_PORT,
|
|
12
|
-
dockerResourceNames,
|
|
13
|
-
startContainers,
|
|
14
|
-
stopContainers,
|
|
15
|
-
} from "../lib/docker";
|
|
16
|
-
import type { ServiceName } from "../lib/docker";
|
|
17
|
-
import { emitCliError, categorizeUpgradeError } from "../lib/cli-error.js";
|
|
18
|
-
import {
|
|
19
|
-
readCloudToken,
|
|
20
|
-
rollbackCloudAssistant,
|
|
21
|
-
} from "../lib/cloud-client.js";
|
|
22
|
-
import {
|
|
23
|
-
broadcastUpgradeEvent,
|
|
24
|
-
buildCompleteEvent,
|
|
25
|
-
buildProgressEvent,
|
|
26
|
-
buildStartingEvent,
|
|
27
|
-
buildUpgradeCommitMessage,
|
|
28
|
-
captureReplayState,
|
|
29
|
-
commitWorkspaceViaGateway,
|
|
30
|
-
fetchCurrentVersion,
|
|
31
|
-
fetchPreviousVersion,
|
|
32
|
-
performDockerRollback,
|
|
33
|
-
rollbackMigrations,
|
|
34
|
-
UPGRADE_PROGRESS,
|
|
35
|
-
waitForReady,
|
|
36
|
-
} from "../lib/upgrade-lifecycle.js";
|
|
37
|
-
|
|
38
|
-
function parseArgs(): { name: string | null; version: string | null } {
|
|
39
|
-
const args = process.argv.slice(3);
|
|
40
|
-
let name: string | null = null;
|
|
41
|
-
let version: string | null = null;
|
|
42
|
-
|
|
43
|
-
for (let i = 0; i < args.length; i++) {
|
|
44
|
-
const arg = args[i];
|
|
45
|
-
if (arg === "--help" || arg === "-h") {
|
|
46
|
-
console.log("Usage: cadet rollback [<name>] [--version <version>]");
|
|
47
|
-
console.log("");
|
|
48
|
-
console.log(
|
|
49
|
-
"Roll back a Docker or cloud assistant to a previous version.",
|
|
50
|
-
);
|
|
51
|
-
console.log("");
|
|
52
|
-
console.log("Arguments:");
|
|
53
|
-
console.log(
|
|
54
|
-
" <name> Name of the assistant, defaults to active or only assistant",
|
|
55
|
-
);
|
|
56
|
-
console.log("");
|
|
57
|
-
console.log("Options:");
|
|
58
|
-
console.log(
|
|
59
|
-
" --version <version> Target version, optional for managed ā omit to roll back to previous",
|
|
60
|
-
);
|
|
61
|
-
console.log("");
|
|
62
|
-
console.log("Examples:");
|
|
63
|
-
console.log(
|
|
64
|
-
" cadet rollback my-assistant # Roll back to previous version (Docker or managed)",
|
|
65
|
-
);
|
|
66
|
-
console.log(
|
|
67
|
-
" cadet rollback my-assistant --version v1.2.3 # Roll back to a specific version",
|
|
68
|
-
);
|
|
69
|
-
process.exit(0);
|
|
70
|
-
} else if (arg === "--version") {
|
|
71
|
-
const next = args[i + 1];
|
|
72
|
-
if (!next || next.startsWith("-")) {
|
|
73
|
-
console.error("Error: --version requires a value");
|
|
74
|
-
emitCliError("UNKNOWN", "--version requires a value");
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
version = next;
|
|
78
|
-
i++;
|
|
79
|
-
} else if (!arg.startsWith("-")) {
|
|
80
|
-
name = arg;
|
|
81
|
-
} else {
|
|
82
|
-
console.error(`Error: Unknown option '${arg}'.`);
|
|
83
|
-
emitCliError("UNKNOWN", `Unknown option '${arg}'`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { name, version };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Resolve which assistant to target for the rollback command. Priority:
|
|
93
|
-
* 1. Explicit name argument
|
|
94
|
-
* 2. Active assistant set via `cadet use`
|
|
95
|
-
* 3. Sole assistant (when exactly one exists)
|
|
96
|
-
*/
|
|
97
|
-
function resolveTargetAssistant(nameArg: string | null): AssistantEntry {
|
|
98
|
-
if (nameArg) {
|
|
99
|
-
const entry = findAssistantByName(nameArg);
|
|
100
|
-
if (!entry) {
|
|
101
|
-
console.error(`No assistant found with name '${nameArg}'.`);
|
|
102
|
-
emitCliError(
|
|
103
|
-
"ASSISTANT_NOT_FOUND",
|
|
104
|
-
`No assistant found with name '${nameArg}'.`,
|
|
105
|
-
);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
return entry;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const active = getActiveAssistant();
|
|
112
|
-
if (active) {
|
|
113
|
-
const entry = findAssistantByName(active);
|
|
114
|
-
if (entry) return entry;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const all = loadAllAssistants();
|
|
118
|
-
if (all.length === 1) return all[0];
|
|
119
|
-
|
|
120
|
-
if (all.length === 0) {
|
|
121
|
-
const msg = "No assistants found. Run 'cadet hatch' first.";
|
|
122
|
-
console.error(msg);
|
|
123
|
-
emitCliError("ASSISTANT_NOT_FOUND", msg);
|
|
124
|
-
} else {
|
|
125
|
-
const msg =
|
|
126
|
-
"Multiple assistants found. Specify a name or set an active assistant with 'cadet use <name>'.";
|
|
127
|
-
console.error(msg);
|
|
128
|
-
emitCliError("ASSISTANT_NOT_FOUND", msg);
|
|
129
|
-
}
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function rollbackPlatformViaEndpoint(
|
|
134
|
-
entry: AssistantEntry,
|
|
135
|
-
version?: string,
|
|
136
|
-
): Promise<void> {
|
|
137
|
-
// Step 1 ā Authenticate
|
|
138
|
-
const token = readCloudToken();
|
|
139
|
-
if (!token) {
|
|
140
|
-
const msg =
|
|
141
|
-
"Error: Not logged in. Run `cadet login --token <token>` first.";
|
|
142
|
-
console.error(msg);
|
|
143
|
-
emitCliError("AUTH_FAILED", msg);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Fetch current version from health endpoint (best-effort)
|
|
148
|
-
const currentVersion = await fetchCurrentVersion(entry.runtimeUrl);
|
|
149
|
-
|
|
150
|
-
// Step 3 ā Call rollback endpoint
|
|
151
|
-
if (version) {
|
|
152
|
-
console.log(`Rolling back to ${version}...`);
|
|
153
|
-
} else {
|
|
154
|
-
console.log("Rolling back to previous version...");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let result: { detail: string; version: string | null };
|
|
158
|
-
try {
|
|
159
|
-
result = await rollbackCloudAssistant(
|
|
160
|
-
token,
|
|
161
|
-
entry.assistantId,
|
|
162
|
-
version,
|
|
163
|
-
entry.runtimeUrl,
|
|
164
|
-
);
|
|
165
|
-
} catch (err) {
|
|
166
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
167
|
-
|
|
168
|
-
// Map specific server error messages to actionable CLI output
|
|
169
|
-
if (detail.includes("No previous version")) {
|
|
170
|
-
console.error(
|
|
171
|
-
"No previous version available. A successful upgrade must have been performed first.",
|
|
172
|
-
);
|
|
173
|
-
} else if (detail.includes("not older")) {
|
|
174
|
-
console.error(
|
|
175
|
-
`Target version is not older than the current version. Use 'cadet upgrade --version' instead.`,
|
|
176
|
-
);
|
|
177
|
-
} else if (detail.includes("not found")) {
|
|
178
|
-
console.error(
|
|
179
|
-
version
|
|
180
|
-
? `Version ${version} not found.`
|
|
181
|
-
: `Rollback target not found.`,
|
|
182
|
-
);
|
|
183
|
-
} else if (
|
|
184
|
-
err instanceof TypeError ||
|
|
185
|
-
detail.includes("fetch failed") ||
|
|
186
|
-
detail.includes("ECONNREFUSED")
|
|
187
|
-
) {
|
|
188
|
-
console.error(
|
|
189
|
-
`Connection error: ${detail}\nIs Cadet Cloud reachable? Try 'cadet wake' if the assistant is asleep.`,
|
|
190
|
-
);
|
|
191
|
-
} else {
|
|
192
|
-
console.error(`Error: ${detail}`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
emitCliError("PLATFORM_API_ERROR", "Cadet Cloud rollback failed", detail);
|
|
196
|
-
await broadcastUpgradeEvent(
|
|
197
|
-
entry.runtimeUrl,
|
|
198
|
-
entry.assistantId,
|
|
199
|
-
buildCompleteEvent(currentVersion ?? version ?? "unknown", false),
|
|
200
|
-
);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const rolledBackVersion = result.version ?? version ?? "unknown";
|
|
205
|
-
|
|
206
|
-
// Step 4 ā Print success
|
|
207
|
-
console.log(`Rolled back to version ${rolledBackVersion}.`);
|
|
208
|
-
if (!version) {
|
|
209
|
-
console.log("Tip: Run 'cadet rollback' again to undo.");
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export const rollback = Object.assign(
|
|
214
|
-
async function rollback(): Promise<void> {
|
|
215
|
-
const { name, version } = parseArgs();
|
|
216
|
-
const entry = resolveTargetAssistant(name);
|
|
217
|
-
const cloud = entry.cloud;
|
|
218
|
-
|
|
219
|
-
if (cloud === "apple-container") {
|
|
220
|
-
console.error(
|
|
221
|
-
`Error: '${entry.assistantId}' uses the Apple Containers runtime. Rollback is not yet supported for this topology.`,
|
|
222
|
-
);
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ---------- Managed (Cadet Cloud) rollback ----------
|
|
227
|
-
if (isCloudHome(cloud)) {
|
|
228
|
-
await rollbackPlatformViaEndpoint(entry, version ?? undefined);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ---------- Unsupported topologies ----------
|
|
233
|
-
if (cloud !== "docker") {
|
|
234
|
-
const msg = "Rollback is only supported for Docker and cloud assistants.";
|
|
235
|
-
console.error(msg);
|
|
236
|
-
emitCliError("UNSUPPORTED_TOPOLOGY", msg);
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ---------- Docker: Targeted version rollback (--version specified) ----------
|
|
241
|
-
if (version) {
|
|
242
|
-
try {
|
|
243
|
-
await performDockerRollback(entry, { targetVersion: version });
|
|
244
|
-
} catch (err) {
|
|
245
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
246
|
-
console.error(`\nā Rollback failed: ${detail}`);
|
|
247
|
-
await broadcastUpgradeEvent(
|
|
248
|
-
entry.runtimeUrl,
|
|
249
|
-
entry.assistantId,
|
|
250
|
-
buildCompleteEvent(version ?? "unknown", false),
|
|
251
|
-
);
|
|
252
|
-
emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ---------- Docker: Saved-state rollback (no --version) ----------
|
|
259
|
-
|
|
260
|
-
// Fetch current + previous version from live APIs
|
|
261
|
-
const currentVersion = await fetchCurrentVersion(entry.runtimeUrl);
|
|
262
|
-
const previousVersion =
|
|
263
|
-
(await fetchPreviousVersion(currentVersion, entry.previousVersion)) ??
|
|
264
|
-
"unknown";
|
|
265
|
-
|
|
266
|
-
// Verify rollback state exists
|
|
267
|
-
if (!entry.previousContainerInfo) {
|
|
268
|
-
const msg =
|
|
269
|
-
"No rollback state available. Run `cadet upgrade` first to create a rollback point.";
|
|
270
|
-
console.error(msg);
|
|
271
|
-
emitCliError("ROLLBACK_NO_STATE", msg);
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Roll back to the previous images. Older entries recorded per-service
|
|
276
|
-
// digests (the image sha captured at upgrade time) ā those are gone from
|
|
277
|
-
// the contract now; the recorded image refs are the rollback target.
|
|
278
|
-
const prev = entry.previousContainerInfo;
|
|
279
|
-
if (!prev.assistantImage || !prev.gatewayImage || !prev.cesImage) {
|
|
280
|
-
const msg =
|
|
281
|
-
"Incomplete rollback state. Previous container image refs are missing.";
|
|
282
|
-
console.error(msg);
|
|
283
|
-
emitCliError("ROLLBACK_NO_STATE", msg);
|
|
284
|
-
process.exit(1);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Build image refs from the previous record
|
|
288
|
-
const previousImageRefs: Record<ServiceName, string> = {
|
|
289
|
-
assistant: prev.assistantImage,
|
|
290
|
-
"credential-executor": prev.cesImage,
|
|
291
|
-
gateway: prev.gatewayImage,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const instanceName = entry.assistantId;
|
|
295
|
-
const res = dockerResourceNames(instanceName);
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
// Record rollback start in workspace git history
|
|
299
|
-
await commitWorkspaceViaGateway(
|
|
300
|
-
entry.runtimeUrl,
|
|
301
|
-
entry.assistantId,
|
|
302
|
-
buildUpgradeCommitMessage({
|
|
303
|
-
action: "rollback",
|
|
304
|
-
phase: "starting",
|
|
305
|
-
from: currentVersion ?? "unknown",
|
|
306
|
-
to: previousVersion,
|
|
307
|
-
topology: "docker",
|
|
308
|
-
assistantId: entry.assistantId,
|
|
309
|
-
}),
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
console.log(
|
|
313
|
-
`š Rolling back Docker assistant '${instanceName}' to ${previousVersion}...\n`,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
const {
|
|
317
|
-
bootstrapSecret,
|
|
318
|
-
cesServiceToken,
|
|
319
|
-
signingKey,
|
|
320
|
-
extraAssistantEnv,
|
|
321
|
-
extraGatewayEnv,
|
|
322
|
-
} = await captureReplayState(res);
|
|
323
|
-
|
|
324
|
-
// Parse gateway port from entry's runtimeUrl, fall back to default
|
|
325
|
-
let gatewayPort = GATEWAY_INTERNAL_PORT;
|
|
326
|
-
try {
|
|
327
|
-
const parsed = new URL(entry.runtimeUrl);
|
|
328
|
-
const port = parseInt(parsed.port, 10);
|
|
329
|
-
if (!isNaN(port)) {
|
|
330
|
-
gatewayPort = port;
|
|
331
|
-
}
|
|
332
|
-
} catch {
|
|
333
|
-
// use default
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Recover the assistant host port from the entry, fall back to default.
|
|
337
|
-
const assistantPort =
|
|
338
|
-
entry.containerInfo?.assistantPort ?? ASSISTANT_INTERNAL_PORT;
|
|
339
|
-
|
|
340
|
-
// Notify connected clients that a rollback is about to begin (best-effort)
|
|
341
|
-
console.log("š¢ Notifying connected clients...");
|
|
342
|
-
await broadcastUpgradeEvent(
|
|
343
|
-
entry.runtimeUrl,
|
|
344
|
-
entry.assistantId,
|
|
345
|
-
buildStartingEvent(previousVersion),
|
|
346
|
-
);
|
|
347
|
-
// Brief pause to allow SSE delivery before containers stop.
|
|
348
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
349
|
-
|
|
350
|
-
// Roll back migrations to pre-upgrade state (must happen before containers stop)
|
|
351
|
-
if (
|
|
352
|
-
entry.previousDbMigrationVersion !== undefined ||
|
|
353
|
-
entry.previousWorkspaceMigrationId !== undefined
|
|
354
|
-
) {
|
|
355
|
-
console.log("š Reverting database changes...");
|
|
356
|
-
await broadcastUpgradeEvent(
|
|
357
|
-
entry.runtimeUrl,
|
|
358
|
-
entry.assistantId,
|
|
359
|
-
buildProgressEvent(UPGRADE_PROGRESS.REVERTING_MIGRATIONS),
|
|
360
|
-
);
|
|
361
|
-
await rollbackMigrations(
|
|
362
|
-
entry.runtimeUrl,
|
|
363
|
-
entry.assistantId,
|
|
364
|
-
entry.previousDbMigrationVersion,
|
|
365
|
-
entry.previousWorkspaceMigrationId,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Progress: switching version (must be sent BEFORE stopContainers)
|
|
370
|
-
await broadcastUpgradeEvent(
|
|
371
|
-
entry.runtimeUrl,
|
|
372
|
-
entry.assistantId,
|
|
373
|
-
buildProgressEvent(UPGRADE_PROGRESS.SWITCHING),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
console.log("š Stopping existing containers...");
|
|
377
|
-
await stopContainers(res);
|
|
378
|
-
console.log("ā
Containers stopped\n");
|
|
379
|
-
|
|
380
|
-
console.log("š Starting containers with previous version...");
|
|
381
|
-
await startContainers(
|
|
382
|
-
{
|
|
383
|
-
signingKey,
|
|
384
|
-
bootstrapSecret,
|
|
385
|
-
cesServiceToken,
|
|
386
|
-
extraAssistantEnv,
|
|
387
|
-
extraGatewayEnv,
|
|
388
|
-
gatewayPort,
|
|
389
|
-
assistantPort,
|
|
390
|
-
imageTags: previousImageRefs,
|
|
391
|
-
instanceName,
|
|
392
|
-
res,
|
|
393
|
-
},
|
|
394
|
-
(msg) => console.log(msg),
|
|
395
|
-
);
|
|
396
|
-
console.log("ā
Containers started\n");
|
|
397
|
-
|
|
398
|
-
console.log("Waiting for assistant to become ready...");
|
|
399
|
-
const ready = await waitForReady(entry.runtimeUrl);
|
|
400
|
-
|
|
401
|
-
if (ready) {
|
|
402
|
-
// Swap current/previous state to enable "rollback the rollback"
|
|
403
|
-
const updatedEntry: AssistantEntry = {
|
|
404
|
-
...entry,
|
|
405
|
-
containerInfo: {
|
|
406
|
-
assistantImage: prev.assistantImage ?? previousImageRefs.assistant,
|
|
407
|
-
gatewayImage: prev.gatewayImage ?? previousImageRefs.gateway,
|
|
408
|
-
cesImage: prev.cesImage ?? previousImageRefs["credential-executor"],
|
|
409
|
-
assistantPort,
|
|
410
|
-
},
|
|
411
|
-
previousContainerInfo: entry.containerInfo,
|
|
412
|
-
// Clear the backup path ā it belonged to the upgrade we just rolled back
|
|
413
|
-
preUpgradeBackupPath: undefined,
|
|
414
|
-
previousDbMigrationVersion: undefined,
|
|
415
|
-
previousWorkspaceMigrationId: undefined,
|
|
416
|
-
};
|
|
417
|
-
saveAssistantEntry(updatedEntry);
|
|
418
|
-
|
|
419
|
-
// Notify clients that the rollback succeeded
|
|
420
|
-
await broadcastUpgradeEvent(
|
|
421
|
-
entry.runtimeUrl,
|
|
422
|
-
entry.assistantId,
|
|
423
|
-
buildCompleteEvent(previousVersion, true),
|
|
424
|
-
);
|
|
425
|
-
|
|
426
|
-
// Record successful rollback in workspace git history
|
|
427
|
-
await commitWorkspaceViaGateway(
|
|
428
|
-
entry.runtimeUrl,
|
|
429
|
-
entry.assistantId,
|
|
430
|
-
buildUpgradeCommitMessage({
|
|
431
|
-
action: "rollback",
|
|
432
|
-
phase: "complete",
|
|
433
|
-
from: currentVersion ?? "unknown",
|
|
434
|
-
to: previousVersion,
|
|
435
|
-
topology: "docker",
|
|
436
|
-
assistantId: entry.assistantId,
|
|
437
|
-
result: "success",
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
|
|
441
|
-
console.log(
|
|
442
|
-
`\nā
Docker assistant '${instanceName}' rolled back to ${previousVersion}.`,
|
|
443
|
-
);
|
|
444
|
-
console.log(
|
|
445
|
-
"\nTip: To also restore data from before the upgrade, use `cadet restore --from <backup-path>`.",
|
|
446
|
-
);
|
|
447
|
-
} else {
|
|
448
|
-
console.error(
|
|
449
|
-
`\nā Containers failed to become ready within the timeout.`,
|
|
450
|
-
);
|
|
451
|
-
console.log(
|
|
452
|
-
` Check logs with: docker logs -f ${res.assistantContainer}`,
|
|
453
|
-
);
|
|
454
|
-
await broadcastUpgradeEvent(
|
|
455
|
-
entry.runtimeUrl,
|
|
456
|
-
entry.assistantId,
|
|
457
|
-
buildCompleteEvent(previousVersion, false),
|
|
458
|
-
);
|
|
459
|
-
emitCliError(
|
|
460
|
-
"READINESS_TIMEOUT",
|
|
461
|
-
"Rolled-back containers failed to become ready within the timeout.",
|
|
462
|
-
);
|
|
463
|
-
process.exit(1);
|
|
464
|
-
}
|
|
465
|
-
} catch (err) {
|
|
466
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
467
|
-
console.error(`\nā Rollback failed: ${detail}`);
|
|
468
|
-
await broadcastUpgradeEvent(
|
|
469
|
-
entry.runtimeUrl,
|
|
470
|
-
entry.assistantId,
|
|
471
|
-
buildCompleteEvent(previousVersion, false),
|
|
472
|
-
);
|
|
473
|
-
emitCliError(categorizeUpgradeError(err), "Rollback failed", detail);
|
|
474
|
-
process.exit(1);
|
|
475
|
-
}
|
|
476
|
-
},
|
|
477
|
-
{ description: "Roll back an assistant to a previous version" },
|
|
478
|
-
);
|
|
479
|
-
|