@mycadet/cli 0.12.11 → 0.13.0-dev.202610171626.d97b8b8

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,3 +1,6 @@
1
+ import { z } from "zod";
2
+
3
+ import { CADET_GITHUB_REPOSITORY } from "./github-repository.js";
1
4
  import { getCloudUrl } from "./cloud-client.js";
2
5
  import { dockerhubImages } from "./docker.js";
3
6
  import type { ServiceName } from "./docker.js";
@@ -22,12 +25,40 @@ function releasesOptsFor(channel: ReleaseChannel): { channel?: "preview" } {
22
25
  return channel === "preview" ? { channel: "preview" } : {};
23
26
  }
24
27
 
25
- export interface ReleaseListItem {
26
- version: string;
27
- is_stable?: boolean;
28
- assistant_image_ref?: string | null;
29
- gateway_image_ref?: string | null;
30
- credential_executor_image_ref?: string | null;
28
+ const ReleaseListItemSchema = z.object({
29
+ version: z.string().min(1),
30
+ is_stable: z.boolean().optional(),
31
+ assistant_image_ref: z.string().nullable().optional(),
32
+ gateway_image_ref: z.string().nullable().optional(),
33
+ credential_executor_image_ref: z.string().nullable().optional(),
34
+ });
35
+
36
+ export type ReleaseListItem = z.infer<typeof ReleaseListItemSchema>;
37
+
38
+ const STABLE_VERSION = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
39
+ const GitHubReleaseSchema = z.object({
40
+ tag_name: z.string().regex(STABLE_VERSION),
41
+ draft: z.literal(false),
42
+ prerelease: z.literal(false),
43
+ });
44
+
45
+ async function fetchLatestGitHubStableVersion(): Promise<string | null> {
46
+ try {
47
+ const response = await loopbackSafeFetch(
48
+ `https://api.github.com/repos/${CADET_GITHUB_REPOSITORY}/releases/latest`,
49
+ {
50
+ headers: { Accept: "application/vnd.github+json" },
51
+ signal: AbortSignal.timeout(10_000),
52
+ },
53
+ );
54
+ if (!response.ok) {
55
+ return null;
56
+ }
57
+ const parsed = GitHubReleaseSchema.safeParse(await response.json());
58
+ return parsed.success ? parsed.data.tag_name.replace(/^v/, "") : null;
59
+ } catch {
60
+ return null;
61
+ }
31
62
  }
32
63
 
33
64
  /**
@@ -56,15 +87,21 @@ export async function fetchReleases(opts?: {
56
87
  `${cloudUrl}/v1/releases/?${filter}&limit=${RELEASES_FETCH_LIMIT}`,
57
88
  { signal: AbortSignal.timeout(10_000) },
58
89
  );
59
- if (!response.ok) return null;
60
- return (await response.json()) as ReleaseListItem[];
90
+ if (!response.ok) {
91
+ return null;
92
+ }
93
+ const parsed = z
94
+ .array(ReleaseListItemSchema)
95
+ .safeParse(await response.json());
96
+ return parsed.success ? parsed.data : null;
61
97
  } catch {
62
98
  return null;
63
99
  }
64
100
  }
65
101
 
66
102
  /**
67
- * Fetch the latest release version for a channel from the Cadet Cloud API.
103
+ * Fetch the latest release version from Cadet Cloud, falling back to GitHub
104
+ * for stable releases when the cloud has no usable result.
68
105
  * Returns the version string (e.g. "0.7.0") or null if unavailable.
69
106
  * The releases endpoint returns entries ordered newest-first, so the first
70
107
  * entry is the latest for the requested channel.
@@ -73,7 +110,14 @@ export async function fetchLatestVersion(
73
110
  channel: ReleaseChannel = "stable",
74
111
  ): Promise<string | null> {
75
112
  const releases = await fetchReleases(releasesOptsFor(channel));
76
- return releases?.[0]?.version ?? null;
113
+ if (channel === "preview") {
114
+ return releases?.[0]?.version ?? null;
115
+ }
116
+ const latest = releases?.find(
117
+ (release) =>
118
+ release.is_stable !== false && STABLE_VERSION.test(release.version),
119
+ );
120
+ return latest?.version ?? fetchLatestGitHubStableVersion();
77
121
  }
78
122
 
79
123
  /**
@@ -1,3 +1,3 @@
1
1
  export function isCloudHome(cloud: unknown): boolean {
2
- return cloud === "cloud" || cloud === "cadet";
2
+ return cloud === "cloud";
3
3
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Shared interactive confirmation for destructive CLI commands (retire, unpair,
3
- * …). Per cli/AGENTS.md, a command that removes assistant state must print the
2
+ * Shared interactive confirmation for destructive CLI commands (retire,
3
+ * devices revoke, …). Per cli/AGENTS.md, a command that removes assistant state must print the
4
4
  * resolved identity and require confirmation, with a `--yes` bypass for
5
5
  * automation.
6
6
  */
package/src/lib/docker.ts CHANGED
@@ -296,6 +296,8 @@ export interface HatchDockerParams {
296
296
  /** Extra env vars forwarded into the assistant container. */
297
297
  flagEnvVars?: Record<string, string>;
298
298
  setupProviderCredentials?: boolean;
299
+ /** Whole-installation state installed into stopped containers before startup. */
300
+ initialArchive?: string;
299
301
  /**
300
302
  * Path to a local source tree to build images from before hatching. When
301
303
  * provided, this path is used directly as the repo root and no file
@@ -812,6 +814,7 @@ export const SERVICE_START_ORDER: ServiceName[] = [
812
814
  /** Start all three containers in dependency order. */
813
815
  export async function startContainers(
814
816
  opts: {
817
+ createOnly?: boolean;
815
818
  signingKey?: string;
816
819
  bootstrapSecret?: string;
817
820
  cesServiceToken?: string;
@@ -832,8 +835,13 @@ export async function startContainers(
832
835
  avatarDevicePath: resolveAvatarDevicePath(),
833
836
  });
834
837
  for (const service of SERVICE_START_ORDER) {
835
- log(`🚀 Starting ${service} container...`);
836
- await exec("docker", runArgs[service]());
838
+ const args = runArgs[service]();
839
+ if (opts.createOnly) {
840
+ args[0] = "create";
841
+ args.splice(args.indexOf("-d"), 1);
842
+ }
843
+ log(`${opts.createOnly ? "Creating" : "Starting"} ${service} container...`);
844
+ await exec("docker", args);
837
845
  }
838
846
  }
839
847
 
@@ -849,14 +857,23 @@ export async function stopContainers(
849
857
  /** Stop containers without removing them (preserves state for `docker start`). */
850
858
  export async function sleepContainers(
851
859
  res: ReturnType<typeof dockerResourceNames>,
860
+ options: { timeoutMs?: number } = {},
852
861
  ): Promise<void> {
853
862
  for (const container of [
854
- res.cesContainer,
855
- res.gatewayContainer,
856
863
  res.assistantContainer,
864
+ res.gatewayContainer,
865
+ res.cesContainer,
857
866
  ]) {
858
867
  try {
859
- await exec("docker", ["stop", container]);
868
+ const status = await execOutput("docker", ["inspect", "--format", "{{.State.Status}}", container]);
869
+ if (status === "exited" || status === "created") continue;
870
+ if (status !== "running") throw new Error(`${container} is ${status}; cannot stop gracefully.`);
871
+ await exec("docker", ["kill", "--signal=SIGTERM", container]);
872
+ const deadline = Date.now() + (options.timeoutMs ?? 60_000);
873
+ while (await execOutput("docker", ["inspect", "--format", "{{.State.Running}}", container]) === "true") {
874
+ if (Date.now() >= deadline) throw new Error(`${container} did not stop gracefully.`);
875
+ await new Promise((resolve) => setTimeout(resolve, 250));
876
+ }
860
877
  } catch (err) {
861
878
  const msg =
862
879
  err instanceof Error ? err.message.toLowerCase() : String(err);
@@ -1630,7 +1647,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1630
1647
  }
1631
1648
 
1632
1649
  const cesServiceToken = randomBytes(32).toString("hex");
1633
- const signingKey = randomBytes(32).toString("hex");
1650
+ const signingKey = params.initialArchive
1651
+ ? undefined
1652
+ : randomBytes(32).toString("hex");
1634
1653
 
1635
1654
  // When launched by a remote hatch startup script, the env var
1636
1655
  // GUARDIAN_BOOTSTRAP_SECRET is already set with the laptop's secret.
@@ -1669,8 +1688,26 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1669
1688
  ...flagEnvVars,
1670
1689
  CADET_DEVICE_ID: hostDeviceId,
1671
1690
  };
1691
+ const runtimeUrl = `http://localhost:${gatewayPort}`;
1692
+ const dockerEntry: AssistantEntry = {
1693
+ assistantId: instanceName,
1694
+ runtimeUrl,
1695
+ cloud: "docker",
1696
+ hatchedAt: new Date().toISOString(),
1697
+ guardianBootstrapSecret: ownSecret,
1698
+ containerInfo: {
1699
+ assistantImage: imageTags.assistant,
1700
+ gatewayImage: imageTags.gateway,
1701
+ cesImage: imageTags["credential-executor"],
1702
+ assistantPort,
1703
+ },
1704
+ };
1705
+ if (params.initialArchive) {
1706
+ saveAssistantEntry(dockerEntry);
1707
+ }
1672
1708
  await startContainers(
1673
1709
  {
1710
+ createOnly: !!params.initialArchive,
1674
1711
  signingKey,
1675
1712
  bootstrapSecret,
1676
1713
  cesServiceToken,
@@ -1688,29 +1725,29 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1688
1725
  );
1689
1726
  const containersUpAt = Date.now();
1690
1727
 
1691
- const runtimeUrl = `http://localhost:${gatewayPort}`;
1692
- const dockerEntry: AssistantEntry = {
1693
- assistantId: instanceName,
1694
- runtimeUrl,
1695
- cloud: "docker",
1696
- hatchedAt: new Date().toISOString(),
1697
- guardianBootstrapSecret: ownSecret,
1698
- containerInfo: {
1699
- assistantImage: imageTags.assistant,
1700
- gatewayImage: imageTags.gateway,
1701
- cesImage: imageTags["credential-executor"],
1702
- assistantPort,
1703
- },
1704
- };
1705
1728
  emitProgress(5, 6, "Saving configuration...");
1706
1729
  saveAssistantEntry(dockerEntry);
1707
1730
  setActiveAssistant(instanceName);
1708
1731
 
1732
+ if (params.initialArchive) {
1733
+ const { initializeRootArchive, DockerRootStorage } =
1734
+ await import("./root-backup.js");
1735
+ await initializeRootArchive(dockerEntry, params.initialArchive);
1736
+ const storage = new DockerRootStorage(dockerEntry);
1737
+ storage.initialRunning = new Set(Object.values(storage.containers));
1738
+ await storage.resume();
1739
+ }
1740
+
1709
1741
  emitProgress(6, 6, "Waiting for services...");
1710
1742
  // Hatch always detaches; only watch mode keeps the log stream attached.
1711
1743
  const waitDetached = !watch;
1712
- const { ready, guardianAccessToken, notReadyReason } =
1713
- await waitForGatewayAndLease({
1744
+ const { ready, guardianAccessToken, notReadyReason } = params.initialArchive
1745
+ ? {
1746
+ ready: true,
1747
+ guardianAccessToken: undefined,
1748
+ notReadyReason: undefined,
1749
+ }
1750
+ : await waitForGatewayAndLease({
1714
1751
  bootstrapSecret: ownSecret,
1715
1752
  containerName: res.assistantContainer,
1716
1753
  detached: waitDetached,
@@ -0,0 +1,2 @@
1
+ /** Source repository for published Cadet releases. */
2
+ export const CADET_GITHUB_REPOSITORY = "vargasjr-dev/mycadet-temp";
@@ -7,7 +7,6 @@ import {
7
7
  mkdirSync,
8
8
  openSync,
9
9
  readFileSync,
10
- rmdirSync,
11
10
  statSync,
12
11
  unlinkSync,
13
12
  writeFileSync,
@@ -79,27 +78,6 @@ function getGuardianTokenPath(assistantId: string): string {
79
78
  return guardianTokenPath(resolveConfigDir(process.env), assistantId);
80
79
  }
81
80
 
82
- /**
83
- * Best-effort removal of an assistant's stored guardian token (used by
84
- * `cadet unpair` to forget a paired connection). Never throws if the token
85
- * file or its per-assistant directory is already absent.
86
- */
87
- export function deleteGuardianToken(assistantId: string): void {
88
- const tokenPath = getGuardianTokenPath(assistantId);
89
- try {
90
- unlinkSync(tokenPath);
91
- } catch {
92
- /* already gone */
93
- }
94
- // Clean up the now-empty per-assistant directory; rmdir throws if it still
95
- // holds other files, in which case we leave it.
96
- try {
97
- rmdirSync(dirname(tokenPath));
98
- } catch {
99
- /* not empty or absent */
100
- }
101
- }
102
-
103
81
  function getPersistedDeviceIdPath(): string {
104
82
  return join(getConfigDir(getCurrentEnvironment()), "device-id");
105
83
  }
@@ -454,7 +432,7 @@ export async function refreshGuardianTokenResult(
454
432
  return {
455
433
  ok: false,
456
434
  status: 401,
457
- error: "Guardian token expired. Re-run `cadet hatch` or `cadet wake`.",
435
+ error: "Guardian refresh token expired",
458
436
  };
459
437
  }
460
438
 
@@ -1,3 +1,4 @@
1
+ import { persistRootSigningKey } from "./root-signing-key.js";
1
2
  import {
2
3
  existsSync,
3
4
  lstatSync,
@@ -160,6 +161,8 @@ export interface HatchLocalParams {
160
161
  /** Extra env vars forwarded into the assistant process. */
161
162
  flagEnvVars?: Record<string, string>;
162
163
  setupProviderCredentials?: boolean;
164
+ /** Whole-installation state installed before the first service startup. */
165
+ initialArchive?: string;
163
166
  /**
164
167
  * Show the per-service startup logs (assistant, credential executor,
165
168
  * gateway) and the extended next steps. Off by default — the happy path
@@ -291,7 +294,20 @@ export async function hatchLocal(
291
294
  const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
292
295
 
293
296
  reporter.progress(3, 6, "Starting assistant...");
294
- const signingKey = generateLocalSigningKey();
297
+ const signingKey = params.initialArchive
298
+ ? await (
299
+ await import("./root-backup.js")
300
+ ).initializeRootArchive(
301
+ {
302
+ assistantId: instanceName,
303
+ cloud: "local",
304
+ runtimeUrl: "",
305
+ resources,
306
+ },
307
+ params.initialArchive,
308
+ )
309
+ : generateLocalSigningKey();
310
+ persistRootSigningKey(resources.instanceDir, signingKey);
295
311
  const bootstrapSecret = generateLocalSigningKey();
296
312
  let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
297
313
  try {