@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.
Files changed (105) 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 +10 -10
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  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-login-poll.test.ts +69 -0
  14. package/src/__tests__/cloud-releases.test.ts +82 -0
  15. package/src/__tests__/commands-mock.ts +101 -0
  16. package/src/__tests__/debug.test.ts +17 -91
  17. package/src/__tests__/devices.test.ts +64 -2
  18. package/src/__tests__/ensure-binary.test.ts +52 -0
  19. package/src/__tests__/gateway-token.test.ts +267 -0
  20. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  21. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  22. package/src/__tests__/host-teleport.test.ts +405 -0
  23. package/src/__tests__/multi-local.test.ts +3 -3
  24. package/src/__tests__/nginx-ingress.test.ts +233 -36
  25. package/src/__tests__/orphan-detection.test.ts +1 -1
  26. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  27. package/src/__tests__/ps-cloud-status.test.ts +8 -7
  28. package/src/__tests__/root-backup-docker.test.ts +294 -0
  29. package/src/__tests__/root-backup.test.ts +527 -0
  30. package/src/__tests__/root-restore-command.test.ts +147 -0
  31. package/src/__tests__/root-signing-key.test.ts +50 -0
  32. package/src/__tests__/setup.test.ts +17 -7
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  36. package/src/__tests__/tunnel-stop.test.ts +226 -0
  37. package/src/__tests__/tunnel.test.ts +13 -3
  38. package/src/__tests__/upgrade-local.test.ts +21 -11
  39. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  40. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  41. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  42. package/src/commands/backup.ts +44 -399
  43. package/src/commands/client.ts +3 -3
  44. package/src/commands/completions.ts +0 -2
  45. package/src/commands/debug.ts +75 -131
  46. package/src/commands/devices.ts +117 -47
  47. package/src/commands/gateway/token.ts +77 -23
  48. package/src/commands/hatch.ts +10 -52
  49. package/src/commands/pair.ts +9 -3
  50. package/src/commands/ps.ts +4 -2
  51. package/src/commands/restore.ts +77 -444
  52. package/src/commands/retire.ts +2 -3
  53. package/src/commands/sleep.ts +15 -2
  54. package/src/commands/teleport.ts +47 -1559
  55. package/src/commands/tunnel.ts +43 -4
  56. package/src/commands/upgrade.ts +84 -157
  57. package/src/commands/wake.ts +1 -1
  58. package/src/index.ts +0 -4
  59. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  60. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  61. package/src/lib/__tests__/web-dist.test.ts +27 -1
  62. package/src/lib/assistant-client.ts +1 -1
  63. package/src/lib/assistant-config.ts +20 -23
  64. package/src/lib/backup-ops.ts +37 -251
  65. package/src/lib/cloud-client.ts +10 -13
  66. package/src/lib/cloud-login-poll.ts +75 -0
  67. package/src/lib/cloud-login.ts +5 -2
  68. package/src/lib/cloud-releases.ts +54 -10
  69. package/src/lib/cloud-values.ts +1 -1
  70. package/src/lib/confirm-action.ts +2 -2
  71. package/src/lib/docker.ts +65 -34
  72. package/src/lib/ensure-binary.ts +65 -6
  73. package/src/lib/github-repository.ts +2 -0
  74. package/src/lib/guardian-token.ts +1 -23
  75. package/src/lib/hatch-local.ts +18 -52
  76. package/src/lib/hatch-next-steps.ts +15 -2
  77. package/src/lib/host-teleport.ts +506 -0
  78. package/src/lib/http-client.ts +25 -0
  79. package/src/lib/live-voice/tunnel.test.ts +1 -1
  80. package/src/lib/live-voice/tunnel.ts +3 -3
  81. package/src/lib/local.ts +10 -8
  82. package/src/lib/nginx-ingress.ts +254 -36
  83. package/src/lib/process.ts +13 -0
  84. package/src/lib/root-backup.ts +917 -0
  85. package/src/lib/root-signing-key.ts +31 -0
  86. package/src/lib/runtime-url.ts +0 -29
  87. package/src/lib/teleport-backup.ts +3 -16
  88. package/src/lib/tunnel.ts +86 -34
  89. package/src/lib/tunnels/cadet.ts +12 -1
  90. package/src/lib/tunnels/cloudflare.ts +36 -1
  91. package/src/lib/tunnels/ngrok.ts +16 -2
  92. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  93. package/src/lib/tunnels/tailscale.ts +79 -1
  94. package/src/lib/upgrade-lifecycle.ts +63 -141
  95. package/src/lib/web-dist.ts +23 -1
  96. package/src/lib/xdg-log.ts +11 -0
  97. package/src/__tests__/bundle-staging.test.ts +0 -268
  98. package/src/__tests__/unpair.test.ts +0 -160
  99. package/src/commands/rollback.ts +0 -479
  100. package/src/commands/unpair.ts +0 -122
  101. package/src/lib/__tests__/job-polling.test.ts +0 -278
  102. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  103. package/src/lib/bundle-staging.ts +0 -220
  104. package/src/lib/job-polling.ts +0 -195
  105. package/src/lib/local-runtime-client.ts +0 -374
@@ -1,409 +1,54 @@
1
- import { isCloudHome } from "../lib/cloud-values.js";
2
- import { mkdirSync, writeFileSync } from "fs";
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 { getBackupsDir, formatSize } from "../lib/backup-ops.js";
8
- import { loadGuardianToken, leaseGuardianToken } from "../lib/guardian-token";
9
- import { pollJobUntilDone } from "../lib/job-polling.js";
10
- import {
11
- MigrationInProgressError,
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
- const args = process.argv.slice(3);
29
-
30
- if (args.includes("--help") || args.includes("-h")) {
31
- console.log(
32
- "Usage: cadet backup <name> [--output <path>] [--export-timeout <seconds>]",
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
- accessToken = freshToken.accessToken;
123
- } catch (err) {
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
- process.exit(1);
21
+ console.log("Example: cadet backup my-assistant");
22
+ return;
188
23
  }
189
- const msg = err instanceof Error ? err.message : String(err);
190
- if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
191
- console.error(
192
- `Error: Could not connect to assistant '${name}'. Is it running?`,
193
- );
194
- console.error(`Try: cadet wake ${name}`);
195
- process.exit(1);
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
- throw err;
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
- throw err;
319
- }
320
-
321
- console.log(`Export started (job ${jobId})...`);
322
-
323
- // Step 3 — Poll the job through the wildcard proxy. The dedicated
324
- // `/v1/migrations/jobs/{id}/` endpoint queries platform-side ImportJob
325
- // records and would 404 on runtime-created job IDs.
326
- const terminal = await pollJobUntilDone({
327
- label: "platform export",
328
- poll: () => localRuntimePollJobStatus(entry, exportPlatformToken, jobId),
329
- refreshOn401: async () => {
330
- const refreshed = readCloudToken();
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
+ );
@@ -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), the
835
- // write counterpart of unpair. The exchange runs here rather than in the
836
- // browser, so the device code and the credentials it buys never reach it.
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) ||
@@ -12,12 +12,10 @@ const ASSISTANT_NAME_COMMANDS = new Set([
12
12
  "restart",
13
13
  "restore",
14
14
  "retire",
15
- "rollback",
16
15
  "sleep",
17
16
  "teleport",
18
17
  "terminal",
19
18
  "tunnel",
20
- "unpair",
21
19
  "upgrade",
22
20
  "use",
23
21
  "wake",