@mycadet/cli 0.12.11 → 0.13.0-dev.202610171617.9ea363e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) 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 +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  4. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  6. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  7. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  9. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  10. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  11. package/package.json +8 -3
  12. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  13. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  14. package/src/__tests__/backup-ops.test.ts +105 -0
  15. package/src/__tests__/backup.test.ts +114 -673
  16. package/src/__tests__/cloud-releases.test.ts +82 -0
  17. package/src/__tests__/commands-mock.ts +101 -0
  18. package/src/__tests__/debug.test.ts +17 -91
  19. package/src/__tests__/devices.test.ts +2 -2
  20. package/src/__tests__/gateway-token.test.ts +267 -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__/nginx-ingress.test.ts +536 -23
  24. package/src/__tests__/orphan-detection.test.ts +1 -1
  25. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  26. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  27. package/src/__tests__/root-backup-docker.test.ts +294 -0
  28. package/src/__tests__/root-backup.test.ts +527 -0
  29. package/src/__tests__/root-restore-command.test.ts +147 -0
  30. package/src/__tests__/root-signing-key.test.ts +50 -0
  31. package/src/__tests__/setup.test.ts +3 -3
  32. package/src/__tests__/shell-mode.test.ts +83 -0
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel.test.ts +10 -2
  36. package/src/__tests__/upgrade-local.test.ts +21 -11
  37. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  38. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  39. package/src/__tests__/wake.test.ts +4 -1
  40. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  41. package/src/commands/backup.ts +44 -399
  42. package/src/commands/client.ts +3 -3
  43. package/src/commands/completions.ts +0 -2
  44. package/src/commands/debug.ts +75 -131
  45. package/src/commands/devices.ts +51 -17
  46. package/src/commands/gateway/token.ts +77 -23
  47. package/src/commands/pair.ts +2 -2
  48. package/src/commands/restore.ts +77 -444
  49. package/src/commands/retire.ts +2 -3
  50. package/src/commands/sleep.ts +15 -2
  51. package/src/commands/teleport.ts +47 -1559
  52. package/src/commands/tunnel.ts +48 -7
  53. package/src/commands/upgrade.ts +78 -156
  54. package/src/commands/wake.ts +1 -1
  55. package/src/index.ts +0 -4
  56. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  57. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  58. package/src/lib/__tests__/web-dist.test.ts +27 -1
  59. package/src/lib/assistant-client.ts +1 -1
  60. package/src/lib/assistant-config.ts +23 -18
  61. package/src/lib/backup-ops.ts +37 -251
  62. package/src/lib/cloud-client.ts +10 -13
  63. package/src/lib/cloud-login.ts +2 -1
  64. package/src/lib/cloud-releases.ts +54 -10
  65. package/src/lib/cloud-values.ts +1 -1
  66. package/src/lib/confirm-action.ts +2 -2
  67. package/src/lib/docker.ts +59 -22
  68. package/src/lib/github-repository.ts +2 -0
  69. package/src/lib/guardian-token.ts +1 -23
  70. package/src/lib/hatch-local.ts +17 -1
  71. package/src/lib/host-teleport.ts +506 -0
  72. package/src/lib/http-client.ts +25 -0
  73. package/src/lib/ingress-config.ts +31 -0
  74. package/src/lib/live-voice/tunnel.test.ts +1 -1
  75. package/src/lib/live-voice/tunnel.ts +1 -1
  76. package/src/lib/local.ts +4 -2
  77. package/src/lib/nginx-ingress.ts +454 -79
  78. package/src/lib/process.ts +13 -0
  79. package/src/lib/root-backup.ts +917 -0
  80. package/src/lib/root-signing-key.ts +31 -0
  81. package/src/lib/runtime-url.ts +0 -29
  82. package/src/lib/teleport-backup.ts +3 -16
  83. package/src/lib/tunnel.ts +1 -0
  84. package/src/lib/upgrade-lifecycle.ts +63 -141
  85. package/src/lib/web-dist.ts +79 -1
  86. package/src/__tests__/bundle-staging.test.ts +0 -268
  87. package/src/__tests__/unpair.test.ts +0 -160
  88. package/src/commands/rollback.ts +0 -479
  89. package/src/commands/unpair.ts +0 -122
  90. package/src/lib/__tests__/job-polling.test.ts +0 -278
  91. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  92. package/src/lib/bundle-staging.ts +0 -220
  93. package/src/lib/job-polling.ts +0 -195
  94. package/src/lib/local-runtime-client.ts +0 -374
@@ -1,374 +0,0 @@
1
- import { isCloudHome } from "./cloud-values.js";
2
- import type { AssistantEntry } from "./assistant-config.js";
3
- import {
4
- authHeaders,
5
- invalidateOrgIdCache,
6
- parseUnifiedJobStatus,
7
- type UnifiedJobStatus,
8
- } from "./cloud-client.js";
9
- import {
10
- resolveRuntimeMigrationUrl,
11
- resolveRuntimeUrl,
12
- } from "./runtime-url.js";
13
- import { loopbackSafeFetch } from "./loopback-fetch.js";
14
- import { z } from "zod";
15
-
16
- const RuntimeIdentitySchema = z.object({ version: z.string().min(1) });
17
-
18
- /**
19
- * Thrown when the local runtime returns 409 for an export/import request
20
- * because another migration of the same type is already in-flight. The
21
- * caller can inspect {@link existingJobId} and decide whether to poll the
22
- * existing job instead of retrying.
23
- */
24
- export class MigrationInProgressError extends Error {
25
- readonly existingJobId: string;
26
- readonly kind: "export_in_progress" | "import_in_progress";
27
-
28
- constructor(
29
- kind: "export_in_progress" | "import_in_progress",
30
- jobId: string,
31
- ) {
32
- super(
33
- `A migration is already in progress (${kind}); existing job_id=${jobId}`,
34
- );
35
- this.name = "MigrationInProgressError";
36
- this.kind = kind;
37
- this.existingJobId = jobId;
38
- }
39
- }
40
-
41
- function bearerHeaders(token: string): Record<string, string> {
42
- return {
43
- Authorization: `Bearer ${token}`,
44
- "Content-Type": "application/json",
45
- Accept: "application/json",
46
- };
47
- }
48
-
49
- /**
50
- * Build the auth + content headers for a runtime migration request.
51
- *
52
- * - For `isCloudHome(cloud)` we go through the cloud's wildcard runtime
53
- * proxy, which authenticates user-session / vak_ tokens via DRF's default
54
- * authentication classes — `authHeaders()` produces the right combination
55
- * (`X-Session-Token` + `Cadet-Organization-Id`, or `Authorization: Bearer
56
- * vak_...`).
57
- * - For local/docker the runtime endpoint expects a guardian-token bearer.
58
- */
59
- async function migrationRequestHeaders(
60
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl">,
61
- token: string,
62
- ): Promise<Record<string, string>> {
63
- if (isCloudHome(entry.cloud)) {
64
- return {
65
- ...(await authHeaders(token, entry.runtimeUrl)),
66
- Accept: "application/json",
67
- };
68
- }
69
- return bearerHeaders(token);
70
- }
71
-
72
- const Raw409BodySchema = z.object({
73
- detail: z.string().optional(),
74
- // The runtime's current 409 contract nests the payload under `error`:
75
- // { error: { code: "export_in_progress" | "import_in_progress", job_id } }
76
- // We also tolerate a legacy flat shape ({ code, job_id }) for resilience.
77
- error: z
78
- .union([
79
- z.string(),
80
- z.object({
81
- code: z.string().optional(),
82
- job_id: z.string().optional(),
83
- }),
84
- ])
85
- .optional(),
86
- code: z.string().optional(),
87
- job_id: z.string().optional(),
88
- });
89
-
90
- /** Common 409 → MigrationInProgressError parsing used by the two POST helpers. */
91
- async function throwIfInProgress(
92
- response: Response,
93
- defaultKind: "export_in_progress" | "import_in_progress",
94
- ): Promise<void> {
95
- if (response.status !== 409) return;
96
- const parsed = Raw409BodySchema.safeParse(
97
- await response.json().catch(() => ({})),
98
- );
99
- const body = parsed.success ? parsed.data : {};
100
- const nested = typeof body.error === "object" ? body.error : undefined;
101
- const jobId = nested?.job_id ?? body.job_id ?? "";
102
- const rawKind =
103
- nested?.code ??
104
- body.code ??
105
- (typeof body.error === "string" ? body.error : undefined) ??
106
- defaultKind;
107
- const kind: "export_in_progress" | "import_in_progress" =
108
- rawKind === "export_in_progress" || rawKind === "import_in_progress"
109
- ? rawKind
110
- : defaultKind;
111
- throw new MigrationInProgressError(kind, jobId);
112
- }
113
-
114
- /**
115
- * Kick off an async export-to-GCS job on the assistant's runtime.
116
- *
117
- * For local/docker assistants this POSTs to
118
- * `{runtimeUrl}/v1/migrations/export-to-gcs` with guardian-token bearer
119
- * auth. For cloud-managed (cloud="cadet") assistants the URL is rewritten
120
- * to the wildcard-runtime-proxy shape
121
- * `{cloudUrl}/v1/assistants/<assistantId>/migrations/export-to-gcs` and
122
- * authenticated via the cloud-token header set the cloud's DRF auth
123
- * accepts (session / vak_).
124
- *
125
- * Returns the 202-accepted `job_id`. On 409 (another export in flight)
126
- * throws {@link MigrationInProgressError} with the existing job_id.
127
- */
128
- export async function localRuntimeExportToGcs(
129
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
130
- token: string,
131
- params: {
132
- uploadUrl: string;
133
- description?: string;
134
- /**
135
- * `"debug"` builds a bundle for Cadet staff: no credentials, plus the
136
- * gateway's database and logs. Omitted means a normal teleport bundle.
137
- */
138
- profile?: "migration" | "debug";
139
- },
140
- ): Promise<{ jobId: string }> {
141
- const body: Record<string, unknown> = { upload_url: params.uploadUrl };
142
- if (params.description !== undefined) {
143
- body.description = params.description;
144
- }
145
- if (params.profile !== undefined) {
146
- body.profile = params.profile;
147
- }
148
-
149
- const response = await loopbackSafeFetch(
150
- resolveRuntimeMigrationUrl(entry, "export-to-gcs"),
151
- {
152
- method: "POST",
153
- headers: await migrationRequestHeaders(entry, token),
154
- body: JSON.stringify(body),
155
- },
156
- );
157
-
158
- await throwIfInProgress(response, "export_in_progress");
159
-
160
- if (response.status !== 202) {
161
- const errText = await response.text().catch(() => "");
162
- throw new Error(
163
- `Local runtime export-to-gcs failed (${response.status}): ${
164
- errText || response.statusText
165
- }`,
166
- );
167
- }
168
-
169
- const json = (await response.json()) as {
170
- job_id: string;
171
- status?: string;
172
- type?: string;
173
- };
174
- return { jobId: json.job_id };
175
- }
176
-
177
- /**
178
- * Kick off an async import-from-GCS job on the assistant's runtime.
179
- *
180
- * For local/docker assistants this POSTs to
181
- * `{runtimeUrl}/v1/migrations/import-from-gcs` with guardian-token bearer
182
- * auth. For cloud-managed (cloud="cadet") assistants the URL is rewritten
183
- * to the wildcard-runtime-proxy shape
184
- * `{cloudUrl}/v1/assistants/<assistantId>/migrations/import-from-gcs` and
185
- * authenticated via the cloud token. On 409 throws
186
- * {@link MigrationInProgressError}.
187
- */
188
- export async function localRuntimeImportFromGcs(
189
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
190
- token: string,
191
- params: { bundleUrl: string },
192
- ): Promise<{ jobId: string }> {
193
- const response = await loopbackSafeFetch(
194
- resolveRuntimeMigrationUrl(entry, "import-from-gcs"),
195
- {
196
- method: "POST",
197
- headers: await migrationRequestHeaders(entry, token),
198
- body: JSON.stringify({ bundle_url: params.bundleUrl }),
199
- },
200
- );
201
-
202
- await throwIfInProgress(response, "import_in_progress");
203
-
204
- if (response.status !== 202) {
205
- const errText = await response.text().catch(() => "");
206
- throw new Error(
207
- `Local runtime import-from-gcs failed (${response.status}): ${
208
- errText || response.statusText
209
- }`,
210
- );
211
- }
212
-
213
- const json = (await response.json()) as {
214
- job_id: string;
215
- status?: string;
216
- type?: string;
217
- };
218
- return { jobId: json.job_id };
219
- }
220
-
221
- /**
222
- * Run a dry-run preflight analysis on a .vbundle archive stored at a signed
223
- * GCS download URL.
224
- *
225
- * For local/docker assistants this POSTs to
226
- * `{runtimeUrl}/v1/migrations/preflight-from-gcs` with guardian-token bearer
227
- * auth. The daemon fetches the bundle from GCS, validates it, and returns a
228
- * report of what would change on import — without writing anything to disk.
229
- *
230
- * Returns the raw preflight response body. Callers should cast to their
231
- * local `PreflightResponse` shape (mirrors the `/import-preflight` shape).
232
- */
233
- export async function localRuntimePreflightFromGcs(
234
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
235
- token: string,
236
- params: { bundleUrl: string },
237
- ): Promise<Record<string, unknown>> {
238
- const response = await fetch(
239
- resolveRuntimeMigrationUrl(entry, "preflight-from-gcs"),
240
- {
241
- method: "POST",
242
- headers: await migrationRequestHeaders(entry, token),
243
- body: JSON.stringify({ bundle_url: params.bundleUrl }),
244
- },
245
- );
246
-
247
- if (response.status !== 200) {
248
- const errText = await response.text().catch(() => "");
249
- throw new Error(
250
- `Local runtime preflight-from-gcs failed (${response.status}): ${
251
- errText || response.statusText
252
- }`,
253
- );
254
- }
255
-
256
- return (await response.json()) as Record<string, unknown>;
257
- }
258
-
259
- /**
260
- * Poll the runtime's unified job-status endpoint.
261
- *
262
- * For local/docker assistants this GETs
263
- * `{runtimeUrl}/v1/migrations/jobs/{jobId}` directly (guardian-token
264
- * bearer). For cloud assistants it routes through the wildcard
265
- * runtime proxy at
266
- * `{cloudUrl}/v1/assistants/<assistantId>/migrations/jobs/{jobId}` with
267
- * platform-token auth — important: the cloud's dedicated
268
- * `/v1/migrations/jobs/{id}/` endpoint queries platform-side ImportJob
269
- * records and would 404 on runtime-created job IDs.
270
- */
271
- export async function localRuntimePollJobStatus(
272
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
273
- token: string,
274
- jobId: string,
275
- ): Promise<UnifiedJobStatus> {
276
- const response = await loopbackSafeFetch(
277
- resolveRuntimeMigrationUrl(entry, `jobs/${jobId}`),
278
- {
279
- headers: await migrationRequestHeaders(entry, token),
280
- },
281
- );
282
-
283
- if (response.status === 404) {
284
- throw new Error("Migration job not found");
285
- }
286
-
287
- if (!response.ok) {
288
- throw new Error(
289
- `Local job status check failed: ${response.status} ${response.statusText}`,
290
- );
291
- }
292
-
293
- const raw = (await response.json()) as Parameters<
294
- typeof parseUnifiedJobStatus
295
- >[0];
296
- return parseUnifiedJobStatus(raw);
297
- }
298
-
299
- /**
300
- * The subset of `/v1/health` we care about. The runtime's full response
301
- * includes additional fields (status, disk, memory, cpu, migrations, etc.)
302
- * — we only model `version` here because that's all the CLI consumes today.
303
- */
304
- export interface RuntimeIdentity {
305
- version: string;
306
- }
307
-
308
- /**
309
- * Fetch the target runtime's APP_VERSION via `/v1/health`. Used by
310
- * `cadet teleport` and `cadet backup` to stamp the exported bundle's
311
- * `min_runtime_version` with the version of the runtime that actually
312
- * produced it — which can diverge from the orchestrating CLI's version when
313
- * the target was upgraded independently.
314
- *
315
- * GETs `/v1/health` (not `/v1/identity`) so the call works on freshly-
316
- * hatched runtimes that haven't completed onboarding. The `/v1/identity`
317
- * handler reads `IDENTITY.md` from the workspace and 404s if it's missing
318
- * — and `IDENTITY.md` is only written during onboarding, not hatch. The
319
- * `/v1/health` handler returns the same `version` field unconditionally
320
- * (no filesystem reads), so it's safe to call against any running runtime.
321
- *
322
- * For local/docker assistants this GETs `{runtimeUrl}/v1/health` with
323
- * guardian-token bearer auth. For cloud-managed (cloud="cadet")
324
- * assistants the URL is rewritten to the wildcard runtime proxy shape
325
- * `{cloudUrl}/v1/assistants/<assistantId>/health` and authenticated via
326
- * the cloud token.
327
- *
328
- * For the cadet target this is the FIRST network call in the
329
- * teleport/backup export flow, so a stale `Cadet-Organization-Id` cache
330
- * entry would surface as a hard abort before any retry-friendly call (like
331
- * `cloudRequestSignedUrl`) gets a chance to recover. Mirror that helper's
332
- * one-shot 401-retry: invalidate the org-ID cache and retry once. Local /
333
- * docker entries do not use the org-ID cache and are wrapped in
334
- * `callRuntimeWithAuthRetry` by callers for guardian-token refresh, so the
335
- * retry is intentionally cadet-only.
336
- *
337
- * The function name is intentionally retained ("identity-ish info about the
338
- * runtime") even though the implementation now hits `/v1/health` — renaming
339
- * would force changes in 4+ callsites for no behavioral benefit.
340
- *
341
- * Throws on non-2xx so callers can surface the failure (we never silently
342
- * fall back — see teleport.ts call site).
343
- */
344
- export async function localRuntimeIdentity(
345
- entry: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId">,
346
- token: string,
347
- ): Promise<RuntimeIdentity> {
348
- const url = resolveRuntimeUrl(entry, "health");
349
- const doRequest = async (): Promise<Response> =>
350
- loopbackSafeFetch(url, {
351
- method: "GET",
352
- headers: await migrationRequestHeaders(entry, token),
353
- });
354
-
355
- let response = await doRequest();
356
- if (response.status === 401 && isCloudHome(entry.cloud)) {
357
- // `entry.runtimeUrl` is the cloud host for cadet-cloud entries
358
- // (the wildcard runtime proxy lives there). Pass it as the cache key
359
- // cloudUrl so we invalidate the same entry that authHeaders cached.
360
- invalidateOrgIdCache(token, entry.runtimeUrl);
361
- response = await doRequest();
362
- }
363
-
364
- if (!response.ok) {
365
- throw new Error(
366
- `Failed to fetch runtime identity: ${response.status} ${response.statusText}`,
367
- );
368
- }
369
- const parsed = RuntimeIdentitySchema.safeParse(await response.json());
370
- if (!parsed.success) {
371
- throw new Error("Runtime identity response missing version");
372
- }
373
- return { version: parsed.data.version };
374
- }