@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
package/src/commands/backup.ts
CHANGED
|
@@ -1,409 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "path";
|
|
4
|
-
|
|
5
|
-
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
6
3
|
import { findAssistantByName } from "../lib/assistant-config.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
localRuntimeExportToGcs,
|
|
13
|
-
localRuntimeIdentity,
|
|
14
|
-
localRuntimePollJobStatus,
|
|
15
|
-
} from "../lib/local-runtime-client.js";
|
|
16
|
-
import {
|
|
17
|
-
cloudRequestSignedUrl,
|
|
18
|
-
readCloudToken,
|
|
19
|
-
} from "../lib/cloud-client.js";
|
|
20
|
-
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
21
|
-
|
|
22
|
-
// Default timeout for the runtime-direct export request. Overridable via
|
|
23
|
-
// --export-timeout.
|
|
24
|
-
const DEFAULT_EXPORT_TIMEOUT_MS = 300_000;
|
|
4
|
+
import { formatSize, getBackupsDir } from "../lib/backup-ops.js";
|
|
5
|
+
import { readCloudToken } from "../lib/cloud-client.js";
|
|
6
|
+
import { isCloudHome } from "../lib/cloud-values.js";
|
|
7
|
+
import { createRootBackup } from "../lib/root-backup.js";
|
|
8
|
+
import { createPlatformBackup } from "../lib/teleport-backup.js";
|
|
25
9
|
|
|
26
10
|
export const backup = Object.assign(
|
|
27
11
|
async function backup(): Promise<void> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
34
|
-
console.log("");
|
|
35
|
-
console.log(
|
|
36
|
-
"Export a backup of a running assistant as a .vbundle archive.",
|
|
37
|
-
);
|
|
38
|
-
console.log("");
|
|
39
|
-
console.log("Arguments:");
|
|
40
|
-
console.log(" <name> Name of the assistant to back up");
|
|
41
|
-
console.log("");
|
|
42
|
-
console.log("Options:");
|
|
43
|
-
console.log(" --output <path> Path to save the .vbundle file");
|
|
44
|
-
console.log(
|
|
45
|
-
" defaults to ~/.local/share/cadet/backups/<name>-<timestamp>.vbundle",
|
|
46
|
-
);
|
|
47
|
-
console.log(
|
|
48
|
-
` --export-timeout <secs> Export request timeout in seconds, defaults to ${DEFAULT_EXPORT_TIMEOUT_MS / 1000}`,
|
|
49
|
-
);
|
|
50
|
-
console.log("");
|
|
51
|
-
console.log("Examples:");
|
|
52
|
-
console.log(" cadet backup my-assistant");
|
|
53
|
-
console.log(
|
|
54
|
-
" cadet backup my-assistant --output ~/Desktop/backup.vbundle",
|
|
55
|
-
);
|
|
56
|
-
console.log(" cadet backup my-assistant --export-timeout 600");
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const name = args[0];
|
|
61
|
-
if (!name || name.startsWith("-")) {
|
|
62
|
-
console.error(
|
|
63
|
-
"Usage: cadet backup <name> [--output <path>] [--export-timeout <seconds>]",
|
|
64
|
-
);
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Parse flags
|
|
69
|
-
let outputArg: string | undefined;
|
|
70
|
-
let exportTimeoutMs = DEFAULT_EXPORT_TIMEOUT_MS;
|
|
71
|
-
for (let i = 1; i < args.length; i++) {
|
|
72
|
-
if (args[i] === "--output" && args[i + 1]) {
|
|
73
|
-
outputArg = args[i + 1];
|
|
74
|
-
} else if (args[i] === "--export-timeout" && args[i + 1]) {
|
|
75
|
-
const seconds = Number(args[i + 1]);
|
|
76
|
-
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
77
|
-
console.error(
|
|
78
|
-
`Error: --export-timeout must be a positive number of seconds (got '${args[i + 1]}').`,
|
|
79
|
-
);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
exportTimeoutMs = seconds * 1000;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Look up the instance
|
|
87
|
-
const entry = findAssistantByName(name);
|
|
88
|
-
if (!entry) {
|
|
89
|
-
console.error(`No assistant found with name '${name}'.`);
|
|
90
|
-
console.error("Run 'cadet hatch' first, or check the instance name.");
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Detect topology and route platform assistants through Django export
|
|
95
|
-
const cloud =
|
|
96
|
-
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
|
|
97
|
-
|
|
98
|
-
if (cloud === "apple-container") {
|
|
99
|
-
console.error(
|
|
100
|
-
`Error: '${name}' uses the Apple Containers runtime. Backup is not yet supported for this topology.`,
|
|
101
|
-
);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (isCloudHome(cloud)) {
|
|
106
|
-
await backupPlatform(entry, name, outputArg);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Obtain an auth token
|
|
111
|
-
let accessToken: string;
|
|
112
|
-
const tokenData = loadGuardianToken(entry.assistantId);
|
|
113
|
-
if (tokenData && new Date(tokenData.accessTokenExpiresAt) > new Date()) {
|
|
114
|
-
accessToken = tokenData.accessToken;
|
|
115
|
-
} else {
|
|
116
|
-
try {
|
|
117
|
-
const freshToken = await leaseGuardianToken(
|
|
118
|
-
entry.runtimeUrl,
|
|
119
|
-
entry.assistantId,
|
|
120
|
-
entry.guardianBootstrapSecret,
|
|
12
|
+
const args = process.argv.slice(3);
|
|
13
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
14
|
+
console.log("Usage: cadet backup <name>");
|
|
15
|
+
console.log(
|
|
16
|
+
"Local/Docker: gracefully stop services, capture all persistent roots as tar.gz, then resume.",
|
|
121
17
|
);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
126
|
-
console.error(
|
|
127
|
-
`Error: Could not connect to assistant '${name}'. Is it running?`,
|
|
128
|
-
);
|
|
129
|
-
console.error(`Try: cadet wake ${name}`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Call the export endpoint
|
|
137
|
-
let response: Response;
|
|
138
|
-
try {
|
|
139
|
-
response = await loopbackSafeFetch(
|
|
140
|
-
`${entry.runtimeUrl}/v1/migrations/export`,
|
|
141
|
-
{
|
|
142
|
-
method: "POST",
|
|
143
|
-
headers: {
|
|
144
|
-
Authorization: `Bearer ${accessToken}`,
|
|
145
|
-
"Content-Type": "application/json",
|
|
146
|
-
},
|
|
147
|
-
body: JSON.stringify({ description: "CLI backup" }),
|
|
148
|
-
signal: AbortSignal.timeout(exportTimeoutMs),
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// Retry once with a fresh token on 401 — the cached token may be stale
|
|
153
|
-
// after a container restart that generated a new gateway signing key.
|
|
154
|
-
if (response.status === 401) {
|
|
155
|
-
let refreshedToken: string | null = null;
|
|
156
|
-
try {
|
|
157
|
-
const freshToken = await leaseGuardianToken(
|
|
158
|
-
entry.runtimeUrl,
|
|
159
|
-
entry.assistantId,
|
|
160
|
-
entry.guardianBootstrapSecret,
|
|
161
|
-
);
|
|
162
|
-
refreshedToken = freshToken.accessToken;
|
|
163
|
-
} catch {
|
|
164
|
-
// If token refresh fails, fall through to the !response.ok handler below
|
|
165
|
-
}
|
|
166
|
-
if (refreshedToken) {
|
|
167
|
-
accessToken = refreshedToken;
|
|
168
|
-
response = await loopbackSafeFetch(
|
|
169
|
-
`${entry.runtimeUrl}/v1/migrations/export`,
|
|
170
|
-
{
|
|
171
|
-
method: "POST",
|
|
172
|
-
headers: {
|
|
173
|
-
Authorization: `Bearer ${accessToken}`,
|
|
174
|
-
"Content-Type": "application/json",
|
|
175
|
-
},
|
|
176
|
-
body: JSON.stringify({ description: "CLI backup" }),
|
|
177
|
-
signal: AbortSignal.timeout(exportTimeoutMs),
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
184
|
-
console.error(
|
|
185
|
-
`Error: Export request timed out after ${exportTimeoutMs / 1000} seconds.`,
|
|
18
|
+
console.log(
|
|
19
|
+
"Cloud: create a backup through the cloud API using your cloud session.",
|
|
186
20
|
);
|
|
187
|
-
|
|
21
|
+
console.log("Example: cadet backup my-assistant");
|
|
22
|
+
return;
|
|
188
23
|
}
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
24
|
+
const name = args[0];
|
|
25
|
+
if (!name || name.startsWith("-"))
|
|
26
|
+
throw new Error("Usage: cadet backup <name>");
|
|
27
|
+
if (args.length > 1) throw new Error(`Unknown backup option: ${args[1]}`);
|
|
28
|
+
const entry = findAssistantByName(name);
|
|
29
|
+
if (!entry) throw new Error(`No assistant found with name '${name}'.`);
|
|
30
|
+
if (isCloudHome(entry.cloud)) {
|
|
31
|
+
const token = readCloudToken();
|
|
32
|
+
if (!token) throw new Error("Not logged in. Run 'cadet login' first.");
|
|
33
|
+
await createPlatformBackup(entry, token);
|
|
34
|
+
console.log("Cloud backup is ready.");
|
|
35
|
+
return;
|
|
196
36
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
const body = await response.text();
|
|
202
|
-
console.error(`Error: Export failed (${response.status}): ${body}`);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Read the response body
|
|
207
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
208
|
-
const data = new Uint8Array(arrayBuffer);
|
|
209
|
-
|
|
210
|
-
// Determine output path
|
|
211
|
-
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
212
|
-
const outputPath =
|
|
213
|
-
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
214
|
-
|
|
215
|
-
// Ensure parent directory exists
|
|
216
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
217
|
-
|
|
218
|
-
// Write the archive to disk
|
|
219
|
-
writeFileSync(outputPath, data);
|
|
220
|
-
|
|
221
|
-
// Print success
|
|
222
|
-
const manifestSha = response.headers.get("X-Vbundle-Manifest-Sha256");
|
|
223
|
-
console.log(`Backup saved to ${outputPath}`);
|
|
224
|
-
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
225
|
-
if (manifestSha) {
|
|
226
|
-
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
{ description: "Export a backup of a running assistant" },
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
// Platform-managed (cloud="cadet") backup over GCS.
|
|
235
|
-
//
|
|
236
|
-
// The runtime exports the bundle straight to a cloud-issued signed GCS
|
|
237
|
-
// URL; the CLI then downloads from GCS to local disk. Bytes never flow
|
|
238
|
-
// through Django. Same architectural shape as the cloud-source half of
|
|
239
|
-
// `cadet teleport`. Output format and success log lines match mode 1
|
|
240
|
-
// (runtime-direct local backup) so users see one consistent UX.
|
|
241
|
-
//
|
|
242
|
-
// Lifecycle: the GCS bucket has a 1-day TTL on `uploads/<org>/*` objects
|
|
243
|
-
// (see `mycadet-platform/django/app/assistant/migration/views.py`
|
|
244
|
-
// and `migration/services.py`). Backup is single-shot with no import to
|
|
245
|
-
// trigger best-effort cleanup, so the bundle sits in GCS up to 24h before
|
|
246
|
-
// TTL deletion. No explicit cleanup endpoint exists; relying on TTL is
|
|
247
|
-
// intentional.
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
async function backupPlatform(
|
|
250
|
-
entry: AssistantEntry,
|
|
251
|
-
name: string,
|
|
252
|
-
outputArg?: string,
|
|
253
|
-
): Promise<void> {
|
|
254
|
-
const cloudToken = readCloudToken();
|
|
255
|
-
if (!cloudToken) {
|
|
256
|
-
console.error(
|
|
257
|
-
"Not logged in. Run 'cadet login' first.",
|
|
258
|
-
);
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
261
|
-
// Pin upload, download, and runtime requests to the same platform instance
|
|
262
|
-
// the assistant lives on. Using `getCloudUrl()` instead would target
|
|
263
|
-
// whatever the lockfile / env-var resolves to, which may differ from
|
|
264
|
-
// `entry.runtimeUrl` for staging/dev assistants and end up signing URLs
|
|
265
|
-
// for the wrong GCS bucket. Mirrors the teleport bundleCloudUrl
|
|
266
|
-
// threading at `cli/src/commands/teleport.ts:1311-1312`.
|
|
267
|
-
const cloudUrl = entry.runtimeUrl;
|
|
268
|
-
// Track the working platform token across kickoff/poll/download so a
|
|
269
|
-
// 401-driven refresh during polling stays consistent through the final
|
|
270
|
-
// signed-download request.
|
|
271
|
-
let exportPlatformToken = cloudToken;
|
|
272
|
-
|
|
273
|
-
// Step 0 — Ask the source runtime which version it's running. The bundle
|
|
274
|
-
// is produced by the daemon (not the CLI), and the CLI version can drift
|
|
275
|
-
// from the daemon version, so the daemon's version is the authoritative
|
|
276
|
-
// value to record as the bundle's `min_runtime_version`. Stamping with
|
|
277
|
-
// `cliPkg.version` here would record an inaccurate compatibility band on
|
|
278
|
-
// the signed-URL request.
|
|
279
|
-
let runtimeIdentity: { version: string };
|
|
280
|
-
try {
|
|
281
|
-
runtimeIdentity = await localRuntimeIdentity(entry, exportPlatformToken);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
284
|
-
console.error(
|
|
285
|
-
`Error: Could not fetch runtime identity from '${name}': ${msg}`,
|
|
286
|
-
);
|
|
287
|
-
process.exit(1);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Step 1 — Request a signed upload URL.
|
|
291
|
-
const { url: uploadUrl, bundleKey } = await cloudRequestSignedUrl(
|
|
292
|
-
{
|
|
293
|
-
operation: "upload",
|
|
294
|
-
minRuntimeVersion: runtimeIdentity.version,
|
|
295
|
-
maxRuntimeVersion: null,
|
|
296
|
-
},
|
|
297
|
-
exportPlatformToken,
|
|
298
|
-
cloudUrl,
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
// Step 2 — Kick off runtime export-to-GCS through the cloud's
|
|
302
|
-
// wildcard runtime proxy. `localRuntimeExportToGcs` builds the
|
|
303
|
-
// `/v1/assistants/<id>/migrations/export-to-gcs` URL for cloud="cadet"
|
|
304
|
-
// and uses platform-token auth (no guardian-token bootstrap).
|
|
305
|
-
let jobId: string;
|
|
306
|
-
try {
|
|
307
|
-
({ jobId } = await localRuntimeExportToGcs(entry, exportPlatformToken, {
|
|
308
|
-
uploadUrl,
|
|
309
|
-
description: "CLI backup",
|
|
310
|
-
}));
|
|
311
|
-
} catch (err) {
|
|
312
|
-
if (err instanceof MigrationInProgressError) {
|
|
313
|
-
console.error(
|
|
314
|
-
`Error: Another backup or teleport export is already in progress on '${entry.assistantId}' (job ${err.existingJobId}). Wait for it to finish, then re-run.`,
|
|
37
|
+
if (entry.cloud && !["local", "docker"].includes(entry.cloud)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Backup for '${entry.cloud}' must run on its host; only local, Docker and cloud backups are supported.`,
|
|
315
40
|
);
|
|
316
|
-
process.exit(1);
|
|
317
41
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (!refreshed) {
|
|
332
|
-
throw new Error(
|
|
333
|
-
"Platform auth expired during export and no credential was found on disk. Run 'cadet login' and retry.",
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
exportPlatformToken = refreshed;
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
if (terminal.status === "failed") {
|
|
341
|
-
console.error(`Error: Export failed: ${terminal.error}`);
|
|
342
|
-
process.exit(1);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Step 4 — Request a signed download URL for the same bundle and fetch
|
|
346
|
-
// it from GCS directly. No auth on signed URLs.
|
|
347
|
-
// Use `exportPlatformToken` (not the original `cloudToken`) so a
|
|
348
|
-
// poll-loop 401 refresh doesn't get clobbered here — otherwise a long
|
|
349
|
-
// export that recovered mid-poll via re-auth would still 401 on the
|
|
350
|
-
// download-URL request and abort an otherwise successful run.
|
|
351
|
-
//
|
|
352
|
-
// We deliberately do NOT send `targetRuntimeVersion` here. This flow
|
|
353
|
-
// saves the bundle to disk for offline storage; there is no target
|
|
354
|
-
// runtime to gate against, and the user can later restore the file
|
|
355
|
-
// into any compatible runtime. Sending the CLI's version would
|
|
356
|
-
// incorrectly block older CLIs from backing up newer assistants.
|
|
357
|
-
// The cloud treats `target_runtime_version` as optional and skips
|
|
358
|
-
// the version check when it's omitted.
|
|
359
|
-
const { url: bundleUrl } = await cloudRequestSignedUrl(
|
|
360
|
-
{
|
|
361
|
-
operation: "download",
|
|
362
|
-
bundleKey,
|
|
363
|
-
},
|
|
364
|
-
exportPlatformToken,
|
|
365
|
-
cloudUrl,
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
let downloadResponse: Response;
|
|
369
|
-
try {
|
|
370
|
-
downloadResponse = await fetch(bundleUrl);
|
|
371
|
-
} catch (err) {
|
|
372
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
-
console.error(`Error: Failed to fetch bundle from GCS: ${msg}`);
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
if (!downloadResponse.ok) {
|
|
377
|
-
const body = await downloadResponse.text().catch(() => "");
|
|
378
|
-
console.error(
|
|
379
|
-
`Error: Failed to fetch bundle from GCS (${downloadResponse.status}): ${body}`,
|
|
380
|
-
);
|
|
381
|
-
process.exit(1);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const arrayBuffer = await downloadResponse.arrayBuffer();
|
|
385
|
-
const data = new Uint8Array(arrayBuffer);
|
|
386
|
-
|
|
387
|
-
// Step 5 — Write to disk using the same path resolution mode 1 uses.
|
|
388
|
-
const isoTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
389
|
-
const outputPath =
|
|
390
|
-
outputArg || join(getBackupsDir(), `${name}-${isoTimestamp}.vbundle`);
|
|
391
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
392
|
-
writeFileSync(outputPath, data);
|
|
393
|
-
|
|
394
|
-
// Step 6 — Print success. Manifest SHA is included only if the runtime
|
|
395
|
-
// surfaced it via the unified job result; the export-to-gcs runtime
|
|
396
|
-
// route does not set the legacy `X-Vbundle-Manifest-Sha256` response
|
|
397
|
-
// header.
|
|
398
|
-
console.log(`Backup saved to ${outputPath}`);
|
|
399
|
-
console.log(`Size: ${formatSize(data.byteLength)}`);
|
|
400
|
-
const manifestSha =
|
|
401
|
-
terminal.status === "complete" &&
|
|
402
|
-
terminal.result &&
|
|
403
|
-
typeof terminal.result === "object"
|
|
404
|
-
? (terminal.result as Record<string, unknown>).manifest_sha256
|
|
405
|
-
: undefined;
|
|
406
|
-
if (typeof manifestSha === "string") {
|
|
407
|
-
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
42
|
+
const path = join(
|
|
43
|
+
getBackupsDir(),
|
|
44
|
+
`${entry.assistantId}-${new Date().toISOString().replace(/[:.]/g, "-")}.tar.gz`,
|
|
45
|
+
);
|
|
46
|
+
await createRootBackup(entry, path);
|
|
47
|
+
console.log(`Backup saved to ${path}`);
|
|
48
|
+
console.log(`Size: ${formatSize((await stat(path)).size)}`);
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
description:
|
|
52
|
+
"Back up persistent local/Docker roots or create a cloud backup",
|
|
53
|
+
},
|
|
54
|
+
);
|
package/src/commands/client.ts
CHANGED
|
@@ -831,9 +831,9 @@ async function handleLocalEndpoints(
|
|
|
831
831
|
}
|
|
832
832
|
|
|
833
833
|
// Pairing: the three-step device-code exchange that registers an assistant
|
|
834
|
-
// paired on another machine (guardian token + paired lockfile entry)
|
|
835
|
-
//
|
|
836
|
-
//
|
|
834
|
+
// paired on another machine (guardian token + paired lockfile entry). The
|
|
835
|
+
// exchange runs here rather than in the browser, so the device code and the
|
|
836
|
+
// credentials it buys never reach it.
|
|
837
837
|
if (
|
|
838
838
|
PAIRING_START_PATTERN.test(pathname) ||
|
|
839
839
|
PAIRING_POLL_PATTERN.test(pathname) ||
|