@mycadet/cli 0.12.11 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/ingress.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
- 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-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 +182 -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__/sleep.test.ts +11 -0
- package/src/__tests__/teleport.test.ts +68 -2996
- package/src/__tests__/tunnel.test.ts +1 -1
- 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 +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/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 +0 -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/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 +134 -18
- 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/upgrade-lifecycle.ts +63 -141
- package/src/lib/web-dist.ts +23 -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/restore.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
+
statSync,
|
|
6
7
|
renameSync,
|
|
7
8
|
unlinkSync,
|
|
8
9
|
} from "fs";
|
|
@@ -14,18 +15,6 @@ import {
|
|
|
14
15
|
saveAssistantEntry,
|
|
15
16
|
} from "../lib/assistant-config.js";
|
|
16
17
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
17
|
-
import {
|
|
18
|
-
bundleFileSizeBytes,
|
|
19
|
-
formatBundleSizeMb,
|
|
20
|
-
importStagedBundle,
|
|
21
|
-
preflightStagedBundle,
|
|
22
|
-
stageBundleForRestore,
|
|
23
|
-
stagingTargetFromEntry,
|
|
24
|
-
} from "../lib/bundle-staging.js";
|
|
25
|
-
import {
|
|
26
|
-
loadGuardianToken,
|
|
27
|
-
leaseGuardianToken,
|
|
28
|
-
} from "../lib/guardian-token.js";
|
|
29
18
|
import {
|
|
30
19
|
generateLocalSigningKey,
|
|
31
20
|
startCes,
|
|
@@ -36,57 +25,44 @@ import { getArchivePath, getMetadataPath } from "../lib/retire-archive.js";
|
|
|
36
25
|
import { exec } from "../lib/step-runner.js";
|
|
37
26
|
import {
|
|
38
27
|
readCloudToken,
|
|
39
|
-
rollbackCloudAssistant,
|
|
40
28
|
cloudRequestSignedUrl,
|
|
41
29
|
platformUploadToSignedUrl,
|
|
42
30
|
platformImportPreflightFromGcs,
|
|
43
31
|
platformImportBundleFromGcs,
|
|
44
32
|
platformPollJobStatus,
|
|
45
33
|
} from "../lib/cloud-client.js";
|
|
46
|
-
import {
|
|
47
|
-
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
34
|
+
import { restoreRootBackup, validateRootArchive } from "../lib/root-backup.js";
|
|
48
35
|
|
|
49
36
|
function printUsage(): void {
|
|
50
|
-
console.log(
|
|
51
|
-
"Usage: cadet restore <name> [--from <path>] [--version <version>] [--dry-run]",
|
|
52
|
-
);
|
|
37
|
+
console.log("Usage: cadet restore <name> [--from <path>] [--dry-run]");
|
|
53
38
|
console.log("");
|
|
54
|
-
console.log("Restore data from a .vbundle backup into an assistant.");
|
|
55
|
-
console.log(
|
|
56
|
-
"Without --from, recovers a retired assistant from its archive in",
|
|
57
|
-
);
|
|
58
39
|
console.log(
|
|
59
|
-
"
|
|
40
|
+
"Restore all persistent roots from a tar archive into an assistant.",
|
|
60
41
|
);
|
|
61
42
|
console.log(
|
|
62
|
-
"
|
|
43
|
+
"Without --from, recovers a retired assistant from its archive in",
|
|
63
44
|
);
|
|
45
|
+
console.log("$XDG_DATA_HOME/cadet/retired/ and starts it again.");
|
|
64
46
|
console.log("");
|
|
65
47
|
console.log("Arguments:");
|
|
66
48
|
console.log(" <name> Name of the assistant to restore into");
|
|
67
49
|
console.log("");
|
|
68
50
|
console.log("Options:");
|
|
69
|
-
console.log(" --from <path> Path to the .
|
|
70
|
-
console.log(
|
|
71
|
-
" --version <version> Roll back to this version before importing data",
|
|
72
|
-
);
|
|
51
|
+
console.log(" --from <path> Path to the .tar.gz file (required)");
|
|
73
52
|
console.log(
|
|
74
53
|
" --dry-run Show what would change without applying (data-only)",
|
|
75
54
|
);
|
|
76
55
|
console.log("");
|
|
77
56
|
console.log("Examples:");
|
|
78
57
|
console.log(" cadet restore my-retired-assistant");
|
|
79
|
-
console.log(" cadet restore my-assistant --from backup.
|
|
80
|
-
console.log(
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
console.log(" cadet restore my-assistant --from backup.vbundle --dry-run");
|
|
58
|
+
console.log(" cadet restore my-assistant --from backup.tar.gz");
|
|
59
|
+
console.log(" cadet restore my-cloud-assistant --from backup.tar.gz");
|
|
60
|
+
console.log(" cadet restore my-assistant --from backup.tar.gz --dry-run");
|
|
84
61
|
}
|
|
85
62
|
|
|
86
63
|
function parseArgs(argv: string[]): {
|
|
87
64
|
name: string | undefined;
|
|
88
65
|
fromPath: string | undefined;
|
|
89
|
-
version: string | undefined;
|
|
90
66
|
dryRun: boolean;
|
|
91
67
|
help: boolean;
|
|
92
68
|
} {
|
|
@@ -96,14 +72,12 @@ function parseArgs(argv: string[]): {
|
|
|
96
72
|
return {
|
|
97
73
|
name: undefined,
|
|
98
74
|
fromPath: undefined,
|
|
99
|
-
version: undefined,
|
|
100
75
|
dryRun: false,
|
|
101
76
|
help: true,
|
|
102
77
|
};
|
|
103
78
|
}
|
|
104
79
|
|
|
105
80
|
let fromPath: string | undefined;
|
|
106
|
-
let version: string | undefined;
|
|
107
81
|
const dryRun = args.includes("--dry-run");
|
|
108
82
|
const positionals: string[] = [];
|
|
109
83
|
|
|
@@ -111,54 +85,16 @@ function parseArgs(argv: string[]): {
|
|
|
111
85
|
if (args[i] === "--from" && args[i + 1]) {
|
|
112
86
|
fromPath = args[i + 1];
|
|
113
87
|
i++; // skip the value
|
|
114
|
-
} else if (args[i] === "--version") {
|
|
115
|
-
const next = args[i + 1];
|
|
116
|
-
if (!next || next.startsWith("-")) {
|
|
117
|
-
console.error("Error: --version requires a value");
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
version = next;
|
|
121
|
-
i++; // skip the value
|
|
122
88
|
} else if (args[i] === "--dry-run") {
|
|
123
89
|
// already handled above
|
|
124
|
-
} else if (
|
|
90
|
+
} else if (args[i].startsWith("-")) {
|
|
91
|
+
throw new Error(`Unknown restore option: ${args[i]}`);
|
|
92
|
+
} else {
|
|
125
93
|
positionals.push(args[i]);
|
|
126
94
|
}
|
|
127
95
|
}
|
|
128
96
|
|
|
129
|
-
return { name: positionals[0], fromPath,
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function getAccessToken(
|
|
133
|
-
runtimeUrl: string,
|
|
134
|
-
assistantId: string,
|
|
135
|
-
displayName: string,
|
|
136
|
-
bootstrapSecret?: string,
|
|
137
|
-
): Promise<string> {
|
|
138
|
-
const tokenData = loadGuardianToken(assistantId);
|
|
139
|
-
|
|
140
|
-
if (tokenData && new Date(tokenData.accessTokenExpiresAt) > new Date()) {
|
|
141
|
-
return tokenData.accessToken;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const freshToken = await leaseGuardianToken(
|
|
146
|
-
runtimeUrl,
|
|
147
|
-
assistantId,
|
|
148
|
-
bootstrapSecret,
|
|
149
|
-
);
|
|
150
|
-
return freshToken.accessToken;
|
|
151
|
-
} catch (err) {
|
|
152
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
153
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
154
|
-
console.error(
|
|
155
|
-
`Error: Could not connect to assistant '${displayName}'. Is it running?`,
|
|
156
|
-
);
|
|
157
|
-
console.error(`Try: cadet wake ${displayName}`);
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}
|
|
160
|
-
throw err;
|
|
161
|
-
}
|
|
97
|
+
return { name: positionals[0], fromPath, dryRun, help: false };
|
|
162
98
|
}
|
|
163
99
|
|
|
164
100
|
interface PreflightFileEntry {
|
|
@@ -204,14 +140,14 @@ interface ImportResponse {
|
|
|
204
140
|
}
|
|
205
141
|
|
|
206
142
|
// ---------------------------------------------------------------------------
|
|
207
|
-
//
|
|
143
|
+
// Cloud restore through the cloud API; the CLI never opens cloud storage.
|
|
208
144
|
// ---------------------------------------------------------------------------
|
|
209
145
|
|
|
210
146
|
async function restorePlatform(
|
|
211
147
|
entry: AssistantEntry,
|
|
212
148
|
name: string,
|
|
213
149
|
bundleData: Buffer,
|
|
214
|
-
opts: {
|
|
150
|
+
opts: { dryRun: boolean },
|
|
215
151
|
): Promise<void> {
|
|
216
152
|
// Step 1 — Authenticate
|
|
217
153
|
const token = readCloudToken();
|
|
@@ -220,15 +156,13 @@ async function restorePlatform(
|
|
|
220
156
|
process.exit(1);
|
|
221
157
|
}
|
|
222
158
|
|
|
223
|
-
//
|
|
224
|
-
// We deliberately omit min/max runtime version here: restore uploads an
|
|
225
|
-
// arbitrary .vbundle from disk (often produced by a different runtime
|
|
226
|
-
// than the one we'd query right now), and the bundle's own manifest is
|
|
227
|
-
// the authority on its compatibility band. The cloud skips the
|
|
228
|
-
// version gate when these fields are absent and re-derives compat from
|
|
229
|
-
// the manifest when it processes the import.
|
|
159
|
+
// Upload the root archive through the cloud API; it owns cloud restore.
|
|
230
160
|
const { url: uploadUrl, bundleKey } = await cloudRequestSignedUrl(
|
|
231
|
-
{
|
|
161
|
+
{
|
|
162
|
+
operation: "upload",
|
|
163
|
+
contentType: "application/gzip",
|
|
164
|
+
contentLength: bundleData.length,
|
|
165
|
+
},
|
|
232
166
|
token,
|
|
233
167
|
entry.runtimeUrl,
|
|
234
168
|
);
|
|
@@ -237,13 +171,6 @@ async function restorePlatform(
|
|
|
237
171
|
|
|
238
172
|
// Step 2 — Dry-run path
|
|
239
173
|
if (opts.dryRun) {
|
|
240
|
-
if (opts.version) {
|
|
241
|
-
console.error(
|
|
242
|
-
"Dry-run is not supported with --version. Use `cadet restore --from <path> --dry-run` for data-only preflight.",
|
|
243
|
-
);
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
174
|
console.log("Running preflight analysis...\n");
|
|
248
175
|
|
|
249
176
|
let preflightResult: { statusCode: number; body: Record<string, unknown> };
|
|
@@ -255,7 +182,7 @@ async function restorePlatform(
|
|
|
255
182
|
);
|
|
256
183
|
} catch (err) {
|
|
257
184
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
258
|
-
console.error("Error: Preflight request timed out after
|
|
185
|
+
console.error("Error: Preflight request timed out after 30 minutes.");
|
|
259
186
|
process.exit(1);
|
|
260
187
|
}
|
|
261
188
|
throw err;
|
|
@@ -354,34 +281,6 @@ async function restorePlatform(
|
|
|
354
281
|
return;
|
|
355
282
|
}
|
|
356
283
|
|
|
357
|
-
// Step 3 — Version rollback (if --version set)
|
|
358
|
-
if (opts.version) {
|
|
359
|
-
console.log(
|
|
360
|
-
`Rolling back to version ${opts.version} before restoring data...`,
|
|
361
|
-
);
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
await rollbackCloudAssistant(
|
|
365
|
-
token,
|
|
366
|
-
entry.assistantId,
|
|
367
|
-
opts.version,
|
|
368
|
-
entry.runtimeUrl,
|
|
369
|
-
);
|
|
370
|
-
} catch (err) {
|
|
371
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
372
|
-
if (msg.includes("401") || msg.includes("403")) {
|
|
373
|
-
console.error("Authentication failed. Run 'cadet login' to refresh.");
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
console.error(`Error: Rollback failed — ${msg}`);
|
|
377
|
-
process.exit(1);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
console.log(
|
|
381
|
-
`Rolled back to ${opts.version}. Proceeding with data restore...`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
284
|
// Step 4 — Data import
|
|
386
285
|
console.log("Importing backup data...");
|
|
387
286
|
|
|
@@ -394,7 +293,7 @@ async function restorePlatform(
|
|
|
394
293
|
);
|
|
395
294
|
} catch (err) {
|
|
396
295
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
397
|
-
console.error("Error: Import request timed out after
|
|
296
|
+
console.error("Error: Import request timed out after 30 minutes.");
|
|
398
297
|
process.exit(1);
|
|
399
298
|
}
|
|
400
299
|
throw err;
|
|
@@ -623,349 +522,83 @@ async function recoverFromArchive(name: string): Promise<void> {
|
|
|
623
522
|
|
|
624
523
|
export const restore = Object.assign(
|
|
625
524
|
async function restore(): Promise<void> {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (help) {
|
|
629
|
-
printUsage();
|
|
630
|
-
process.exit(0);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// --version requires --from
|
|
634
|
-
if (version && !fromPath) {
|
|
635
|
-
console.error(
|
|
636
|
-
"A backup file is required for restore. Use --from <path> to specify the .vbundle file.",
|
|
637
|
-
);
|
|
638
|
-
process.exit(1);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// --dry-run is not supported with --version
|
|
642
|
-
if (version && dryRun) {
|
|
643
|
-
console.error(
|
|
644
|
-
"Dry-run is not supported with --version. Use `cadet restore --from <path> --dry-run` for data-only preflight.",
|
|
645
|
-
);
|
|
646
|
-
process.exit(1);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (!name) {
|
|
650
|
-
console.error("Error: <name> is required.");
|
|
651
|
-
console.error("");
|
|
652
|
-
printUsage();
|
|
653
|
-
process.exit(1);
|
|
654
|
-
}
|
|
525
|
+
const { name, fromPath, dryRun, help } = parseArgs(process.argv);
|
|
655
526
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
console.error(
|
|
660
|
-
"Dry-run is only supported with --from. Recovery replays the retired assistant's archive directly.",
|
|
661
|
-
);
|
|
662
|
-
process.exit(1);
|
|
527
|
+
if (help) {
|
|
528
|
+
printUsage();
|
|
529
|
+
process.exit(0);
|
|
663
530
|
}
|
|
664
|
-
await recoverFromArchive(name);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Look up the instance
|
|
669
|
-
const entry = findAssistantByName(name);
|
|
670
|
-
if (!entry) {
|
|
671
|
-
console.error(`Error: No assistant found with name '${name}'.`);
|
|
672
|
-
console.error("Run 'cadet ps' to see available assistants.");
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Verify .vbundle file exists
|
|
677
|
-
if (!existsSync(fromPath)) {
|
|
678
|
-
console.error(`Error: File not found: ${fromPath}`);
|
|
679
|
-
process.exit(1);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const sizeMB = formatBundleSizeMb(bundleFileSizeBytes(fromPath));
|
|
683
|
-
console.log(`Reading ${fromPath} — ${sizeMB} MB...`);
|
|
684
|
-
|
|
685
|
-
// Detect topology and route platform assistants through Django import
|
|
686
|
-
const cloud =
|
|
687
|
-
entry.cloud || (entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
|
|
688
|
-
|
|
689
|
-
if (cloud === "apple-container") {
|
|
690
|
-
console.error(
|
|
691
|
-
`Error: '${name}' uses the Apple Containers runtime. Restore is not yet supported for this topology.`,
|
|
692
|
-
);
|
|
693
|
-
process.exit(1);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if (isCloudHome(cloud)) {
|
|
697
|
-
const bundleData = readFileSync(fromPath);
|
|
698
|
-
await restorePlatform(entry, name, bundleData, { version, dryRun });
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (version && cloud !== "docker") {
|
|
703
|
-
console.error(
|
|
704
|
-
"Restore with --version is only supported for Docker and cloud assistants.",
|
|
705
|
-
);
|
|
706
|
-
process.exit(1);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Obtain auth token (acquired before dry-run or before data import;
|
|
710
|
-
// re-acquired after version rollback since containers restart).
|
|
711
|
-
const accessToken = await getAccessToken(
|
|
712
|
-
entry.runtimeUrl,
|
|
713
|
-
entry.assistantId,
|
|
714
|
-
name,
|
|
715
|
-
entry.guardianBootstrapSecret,
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
const staging = stagingTargetFromEntry(entry);
|
|
719
|
-
let stagedRelativePath: string | undefined;
|
|
720
|
-
let bundleData: Buffer | undefined;
|
|
721
|
-
let cleanupStaged: (() => Promise<void>) | undefined;
|
|
722
|
-
if (staging) {
|
|
723
|
-
const staged = await stageBundleForRestore(staging, fromPath);
|
|
724
|
-
stagedRelativePath = staged.relativePath;
|
|
725
|
-
cleanupStaged = staged.cleanup;
|
|
726
|
-
} else {
|
|
727
|
-
bundleData = readFileSync(fromPath);
|
|
728
|
-
}
|
|
729
531
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
dryRun,
|
|
736
|
-
accessToken,
|
|
737
|
-
stagedRelativePath,
|
|
738
|
-
bundleData,
|
|
739
|
-
});
|
|
740
|
-
} finally {
|
|
741
|
-
if (cleanupStaged) {
|
|
742
|
-
await cleanupStaged();
|
|
532
|
+
if (!name) {
|
|
533
|
+
console.error("Error: <name> is required.");
|
|
534
|
+
console.error("");
|
|
535
|
+
printUsage();
|
|
536
|
+
process.exit(1);
|
|
743
537
|
}
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
|
-
{ description: "Restore a .vbundle backup, or recover a retired assistant" },
|
|
747
|
-
);
|
|
748
|
-
|
|
749
538
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
version: string | undefined;
|
|
754
|
-
dryRun: boolean;
|
|
755
|
-
accessToken: string;
|
|
756
|
-
stagedRelativePath: string | undefined;
|
|
757
|
-
bundleData: Buffer | undefined;
|
|
758
|
-
}): Promise<void> {
|
|
759
|
-
const { entry, name, version, dryRun, stagedRelativePath, bundleData } = opts;
|
|
760
|
-
let accessToken = opts.accessToken;
|
|
761
|
-
|
|
762
|
-
if (dryRun) {
|
|
763
|
-
// Preflight check
|
|
764
|
-
console.log("Running preflight analysis...\n");
|
|
765
|
-
|
|
766
|
-
let response: Response;
|
|
767
|
-
try {
|
|
768
|
-
response = stagedRelativePath
|
|
769
|
-
? await preflightStagedBundle(
|
|
770
|
-
entry.runtimeUrl,
|
|
771
|
-
accessToken,
|
|
772
|
-
stagedRelativePath,
|
|
773
|
-
)
|
|
774
|
-
: await loopbackSafeFetch(
|
|
775
|
-
`${entry.runtimeUrl}/v1/migrations/import-preflight`,
|
|
776
|
-
{
|
|
777
|
-
method: "POST",
|
|
778
|
-
headers: {
|
|
779
|
-
Authorization: `Bearer ${accessToken}`,
|
|
780
|
-
"Content-Type": "application/octet-stream",
|
|
781
|
-
},
|
|
782
|
-
body: bundleData ? new Uint8Array(bundleData) : undefined,
|
|
783
|
-
signal: AbortSignal.timeout(120_000),
|
|
784
|
-
},
|
|
785
|
-
);
|
|
786
|
-
} catch (err) {
|
|
787
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
788
|
-
console.error("Error: Preflight request timed out after 2 minutes.");
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
791
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
792
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
539
|
+
// Without --from: recover a retired assistant from its retire archive.
|
|
540
|
+
if (!fromPath) {
|
|
541
|
+
if (dryRun) {
|
|
793
542
|
console.error(
|
|
794
|
-
|
|
543
|
+
"Dry-run is only supported with --from. Recovery replays the retired assistant's archive directly.",
|
|
795
544
|
);
|
|
796
|
-
console.error(`Try: cadet wake ${name}`);
|
|
797
545
|
process.exit(1);
|
|
798
546
|
}
|
|
799
|
-
|
|
547
|
+
await recoverFromArchive(name);
|
|
548
|
+
return;
|
|
800
549
|
}
|
|
801
550
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
);
|
|
551
|
+
// Look up the instance
|
|
552
|
+
const entry = findAssistantByName(name);
|
|
553
|
+
if (!entry) {
|
|
554
|
+
console.error(`Error: No assistant found with name '${name}'.`);
|
|
555
|
+
console.error("Run 'cadet ps' to see available assistants.");
|
|
807
556
|
process.exit(1);
|
|
808
557
|
}
|
|
809
558
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (result.validation?.errors?.length) {
|
|
814
|
-
console.error("Import blocked by validation errors:");
|
|
815
|
-
for (const err of result.validation.errors) {
|
|
816
|
-
console.error(
|
|
817
|
-
` - ${err.message}${err.path ? ` (${err.path})` : ""}`,
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (result.conflicts?.length) {
|
|
822
|
-
console.error("Import blocked by conflicts:");
|
|
823
|
-
for (const conflict of result.conflicts) {
|
|
824
|
-
console.error(
|
|
825
|
-
` - ${conflict.message}${conflict.path ? ` (${conflict.path})` : ""}`,
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
559
|
+
// Verify .tar.gz file exists
|
|
560
|
+
if (!existsSync(fromPath)) {
|
|
561
|
+
console.error(`Error: File not found: ${fromPath}`);
|
|
829
562
|
process.exit(1);
|
|
830
563
|
}
|
|
831
564
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
files_to_create: 0,
|
|
835
|
-
files_to_overwrite: 0,
|
|
836
|
-
files_unchanged: 0,
|
|
837
|
-
total_files: 0,
|
|
838
|
-
};
|
|
839
|
-
console.log("Preflight analysis:");
|
|
840
|
-
console.log(` Files to create: ${summary.files_to_create}`);
|
|
841
|
-
console.log(` Files to overwrite: ${summary.files_to_overwrite}`);
|
|
842
|
-
console.log(` Files unchanged: ${summary.files_unchanged}`);
|
|
843
|
-
console.log(` Total: ${summary.total_files}`);
|
|
844
|
-
console.log("");
|
|
845
|
-
|
|
846
|
-
const conflicts = result.conflicts ?? [];
|
|
847
|
-
console.log(
|
|
848
|
-
`Conflicts: ${conflicts.length > 0 ? conflicts.map((c) => c.message).join(", ") : "none"}`,
|
|
849
|
-
);
|
|
850
|
-
|
|
851
|
-
// List individual files with their action
|
|
852
|
-
if (result.files && result.files.length > 0) {
|
|
853
|
-
console.log("");
|
|
854
|
-
console.log("Files:");
|
|
855
|
-
for (const file of result.files) {
|
|
856
|
-
console.log(` [${file.action}] ${file.path}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
} else {
|
|
860
|
-
// Version rollback (when --version is specified)
|
|
861
|
-
if (version) {
|
|
862
|
-
console.log(`Rolling back to version ${version}...`);
|
|
863
|
-
await performDockerRollback(entry, { targetVersion: version });
|
|
864
|
-
console.log("");
|
|
865
|
-
|
|
866
|
-
// Re-acquire auth token since containers were restarted during rollback
|
|
867
|
-
accessToken = await getAccessToken(
|
|
868
|
-
entry.runtimeUrl,
|
|
869
|
-
entry.assistantId,
|
|
870
|
-
name,
|
|
871
|
-
entry.guardianBootstrapSecret,
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Data import
|
|
876
|
-
console.log("Importing backup data...\n");
|
|
565
|
+
const sizeMB = (statSync(fromPath).size / (1024 * 1024)).toFixed(1);
|
|
566
|
+
console.log(`Reading ${fromPath} — ${sizeMB} MB...`);
|
|
877
567
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
entry.runtimeUrl,
|
|
883
|
-
accessToken,
|
|
884
|
-
stagedRelativePath,
|
|
885
|
-
)
|
|
886
|
-
: await loopbackSafeFetch(`${entry.runtimeUrl}/v1/migrations/import`, {
|
|
887
|
-
method: "POST",
|
|
888
|
-
headers: {
|
|
889
|
-
Authorization: `Bearer ${accessToken}`,
|
|
890
|
-
"Content-Type": "application/octet-stream",
|
|
891
|
-
},
|
|
892
|
-
body: bundleData ? new Uint8Array(bundleData) : undefined,
|
|
893
|
-
signal: AbortSignal.timeout(120_000),
|
|
894
|
-
});
|
|
895
|
-
if (!response.ok) {
|
|
896
|
-
const body = await response.text();
|
|
897
|
-
console.error(`Error: Import failed (${response.status}): ${body}`);
|
|
898
|
-
process.exit(1);
|
|
899
|
-
}
|
|
900
|
-
result = (await response.json()) as ImportResponse;
|
|
901
|
-
} catch (err) {
|
|
902
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
903
|
-
console.error("Error: Import request timed out after 2 minutes.");
|
|
904
|
-
process.exit(1);
|
|
905
|
-
}
|
|
906
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
907
|
-
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
908
|
-
console.error(
|
|
909
|
-
`Error: Could not connect to assistant '${name}'. Is it running?`,
|
|
910
|
-
);
|
|
911
|
-
console.error(`Try: cadet wake ${name}`);
|
|
912
|
-
process.exit(1);
|
|
913
|
-
}
|
|
914
|
-
throw err;
|
|
915
|
-
}
|
|
568
|
+
// Cloud storage and lifecycle belong to the cloud API.
|
|
569
|
+
const cloud =
|
|
570
|
+
entry.cloud ||
|
|
571
|
+
(entry.project ? "gcp" : entry.sshUser ? "custom" : "local");
|
|
916
572
|
|
|
917
|
-
if (
|
|
573
|
+
if (cloud === "apple-container") {
|
|
918
574
|
console.error(
|
|
919
|
-
`Error:
|
|
575
|
+
`Error: '${name}' uses the Apple Containers runtime. Restore is not yet supported for this topology.`,
|
|
920
576
|
);
|
|
921
|
-
for (const err of result.errors ?? []) {
|
|
922
|
-
console.error(` - ${err.message}${err.path ? ` (${err.path})` : ""}`);
|
|
923
|
-
}
|
|
924
577
|
process.exit(1);
|
|
925
578
|
}
|
|
926
579
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
files_skipped: 0,
|
|
933
|
-
backups_created: 0,
|
|
934
|
-
};
|
|
935
|
-
console.log("✅ Restore complete.");
|
|
936
|
-
console.log(` Files created: ${summary.files_created}`);
|
|
937
|
-
console.log(` Files overwritten: ${summary.files_overwritten}`);
|
|
938
|
-
console.log(` Files skipped: ${summary.files_skipped}`);
|
|
939
|
-
console.log(` Backups created: ${summary.backups_created}`);
|
|
940
|
-
|
|
941
|
-
// A daemon in the terminal failed-migrations state accepts this import
|
|
942
|
-
// (the migration-repair exemption), but its readiness latch only clears
|
|
943
|
-
// on restart — without one it keeps refusing DB-backed routes.
|
|
944
|
-
try {
|
|
945
|
-
const readyz = await loopbackSafeFetch(`${entry.runtimeUrl}/readyz`, {
|
|
946
|
-
signal: AbortSignal.timeout(3_000),
|
|
947
|
-
});
|
|
948
|
-
const readyzBody = (await readyz.json().catch(() => null)) as {
|
|
949
|
-
dbMigrations?: { state?: string };
|
|
950
|
-
} | null;
|
|
951
|
-
if (readyzBody?.dbMigrations?.state === "failed") {
|
|
952
|
-
console.warn(
|
|
953
|
-
"\n⚠️ The assistant is in a failed-migrations state; the restored data takes effect after a restart:",
|
|
954
|
-
);
|
|
955
|
-
console.warn(` cadet sleep ${name} && cadet wake ${name}`);
|
|
956
|
-
}
|
|
957
|
-
} catch {
|
|
958
|
-
// Best-effort advisory — never fail a completed restore over it.
|
|
580
|
+
if (isCloudHome(cloud)) {
|
|
581
|
+
await validateRootArchive(fromPath);
|
|
582
|
+
const bundleData = readFileSync(fromPath);
|
|
583
|
+
await restorePlatform(entry, name, bundleData, { dryRun });
|
|
584
|
+
return;
|
|
959
585
|
}
|
|
960
586
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (warnings.length > 0) {
|
|
964
|
-
console.log("");
|
|
965
|
-
console.log("Warnings:");
|
|
966
|
-
for (const warning of warnings) {
|
|
967
|
-
console.log(` ⚠️ ${warning}`);
|
|
968
|
-
}
|
|
587
|
+
if (cloud !== "local" && cloud !== "docker") {
|
|
588
|
+
throw new Error(`Restore for '${cloud}' must run on its host.`);
|
|
969
589
|
}
|
|
970
|
-
|
|
971
|
-
|
|
590
|
+
const recoveryPath = await restoreRootBackup(entry, fromPath, { dryRun });
|
|
591
|
+
if (dryRun)
|
|
592
|
+
console.log(
|
|
593
|
+
"Root archive is valid; restore would replace workspace, gateway security, CES security and CES data. No services were stopped.",
|
|
594
|
+
);
|
|
595
|
+
else {
|
|
596
|
+
console.log("Restore complete.");
|
|
597
|
+
console.log(`Previous state retained at ${recoveryPath}`);
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
description:
|
|
602
|
+
"Restore persistent roots from tar, or recover a retired assistant",
|
|
603
|
+
},
|
|
604
|
+
);
|
package/src/commands/retire.ts
CHANGED
|
@@ -332,10 +332,9 @@ async function retireInner(): Promise<void> {
|
|
|
332
332
|
if (cloud === "paired") {
|
|
333
333
|
// A remote assistant paired from another machine. Retiring tears the
|
|
334
334
|
// assistant down — that can only happen on its host machine, never from a
|
|
335
|
-
// paired machine, which holds nothing but a pairing record.
|
|
336
|
-
// local record is `cadet unpair`'s job, not retire's.)
|
|
335
|
+
// paired machine, which holds nothing but a pairing record.
|
|
337
336
|
console.error(
|
|
338
|
-
`Error: '${assistantId}' is a remote assistant paired from another machine — it can't be retired from here. Retiring tears down the assistant, which can only be done on its host machine. To
|
|
337
|
+
`Error: '${assistantId}' is a remote assistant paired from another machine — it can't be retired from here. Retiring tears down the assistant, which can only be done on its host machine. To revoke this device's access, run \`cadet devices\` on the host that runs the assistant.`,
|
|
339
338
|
);
|
|
340
339
|
process.exit(1);
|
|
341
340
|
}
|