@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,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
@@ -305,7 +307,6 @@ export interface HatchDockerParams {
305
307
  * also enables hot-reload.
306
308
  */
307
309
  sourcePath?: string | null;
308
- analyze?: boolean;
309
310
  /**
310
311
  * Name of an existing container whose network namespace the assistant,
311
312
  * gateway, and credential-executor join (`--network=container:<name>`),
@@ -813,6 +814,7 @@ export const SERVICE_START_ORDER: ServiceName[] = [
813
814
  /** Start all three containers in dependency order. */
814
815
  export async function startContainers(
815
816
  opts: {
817
+ createOnly?: boolean;
816
818
  signingKey?: string;
817
819
  bootstrapSecret?: string;
818
820
  cesServiceToken?: string;
@@ -833,8 +835,13 @@ export async function startContainers(
833
835
  avatarDevicePath: resolveAvatarDevicePath(),
834
836
  });
835
837
  for (const service of SERVICE_START_ORDER) {
836
- log(`🚀 Starting ${service} container...`);
837
- 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);
838
845
  }
839
846
  }
840
847
 
@@ -850,14 +857,23 @@ export async function stopContainers(
850
857
  /** Stop containers without removing them (preserves state for `docker start`). */
851
858
  export async function sleepContainers(
852
859
  res: ReturnType<typeof dockerResourceNames>,
860
+ options: { timeoutMs?: number } = {},
853
861
  ): Promise<void> {
854
862
  for (const container of [
855
- res.cesContainer,
856
- res.gatewayContainer,
857
863
  res.assistantContainer,
864
+ res.gatewayContainer,
865
+ res.cesContainer,
858
866
  ]) {
859
867
  try {
860
- 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
+ }
861
877
  } catch (err) {
862
878
  const msg =
863
879
  err instanceof Error ? err.message.toLowerCase() : String(err);
@@ -1631,7 +1647,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1631
1647
  }
1632
1648
 
1633
1649
  const cesServiceToken = randomBytes(32).toString("hex");
1634
- const signingKey = randomBytes(32).toString("hex");
1650
+ const signingKey = params.initialArchive
1651
+ ? undefined
1652
+ : randomBytes(32).toString("hex");
1635
1653
 
1636
1654
  // When launched by a remote hatch startup script, the env var
1637
1655
  // GUARDIAN_BOOTSTRAP_SECRET is already set with the laptop's secret.
@@ -1670,8 +1688,26 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1670
1688
  ...flagEnvVars,
1671
1689
  CADET_DEVICE_ID: hostDeviceId,
1672
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
+ }
1673
1708
  await startContainers(
1674
1709
  {
1710
+ createOnly: !!params.initialArchive,
1675
1711
  signingKey,
1676
1712
  bootstrapSecret,
1677
1713
  cesServiceToken,
@@ -1689,29 +1725,29 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1689
1725
  );
1690
1726
  const containersUpAt = Date.now();
1691
1727
 
1692
- const runtimeUrl = `http://localhost:${gatewayPort}`;
1693
- const dockerEntry: AssistantEntry = {
1694
- assistantId: instanceName,
1695
- runtimeUrl,
1696
- cloud: "docker",
1697
- hatchedAt: new Date().toISOString(),
1698
- guardianBootstrapSecret: ownSecret,
1699
- containerInfo: {
1700
- assistantImage: imageTags.assistant,
1701
- gatewayImage: imageTags.gateway,
1702
- cesImage: imageTags["credential-executor"],
1703
- assistantPort,
1704
- },
1705
- };
1706
1728
  emitProgress(5, 6, "Saving configuration...");
1707
1729
  saveAssistantEntry(dockerEntry);
1708
1730
  setActiveAssistant(instanceName);
1709
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
+
1710
1741
  emitProgress(6, 6, "Waiting for services...");
1711
1742
  // Hatch always detaches; only watch mode keeps the log stream attached.
1712
1743
  const waitDetached = !watch;
1713
- const { ready, guardianAccessToken, notReadyReason } =
1714
- await waitForGatewayAndLease({
1744
+ const { ready, guardianAccessToken, notReadyReason } = params.initialArchive
1745
+ ? {
1746
+ ready: true,
1747
+ guardianAccessToken: undefined,
1748
+ notReadyReason: undefined,
1749
+ }
1750
+ : await waitForGatewayAndLease({
1715
1751
  bootstrapSecret: ownSecret,
1716
1752
  containerName: res.assistantContainer,
1717
1753
  detached: waitDetached,
@@ -1719,7 +1755,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1719
1755
  logFd,
1720
1756
  runtimeUrl,
1721
1757
  containersUpAt,
1722
- analyze: params.analyze ?? false,
1723
1758
  });
1724
1759
 
1725
1760
  if (!ready && !(watch && repoRoot)) {
@@ -1828,7 +1863,6 @@ async function waitForGatewayAndLease(opts: {
1828
1863
  logFd: number | "ignore";
1829
1864
  runtimeUrl: string;
1830
1865
  containersUpAt: number;
1831
- analyze: boolean;
1832
1866
  }): Promise<{
1833
1867
  ready: boolean;
1834
1868
  guardianAccessToken?: string;
@@ -1844,7 +1878,6 @@ async function waitForGatewayAndLease(opts: {
1844
1878
  logFd,
1845
1879
  runtimeUrl,
1846
1880
  containersUpAt,
1847
- analyze,
1848
1881
  } = opts;
1849
1882
 
1850
1883
  const log = (msg: string): void => {
@@ -1964,14 +1997,12 @@ async function waitForGatewayAndLease(opts: {
1964
1997
  const containersUpToReadyMs = readyAt - start;
1965
1998
  const elapsedSec = (containersUpToReadyMs / 1000).toFixed(1);
1966
1999
  log(`Assistant ready after ${elapsedSec}s`);
1967
- if (analyze) {
1968
- console.info(
1969
- `[cadet-hatch-timing] ${JSON.stringify({
1970
- containers_up_to_ready_ms: containersUpToReadyMs,
1971
- instance: instanceName,
1972
- })}`,
1973
- );
1974
- }
2000
+ console.info(
2001
+ `[cadet-hatch-timing] ${JSON.stringify({
2002
+ containers_up_to_ready_ms: containersUpToReadyMs,
2003
+ instance: instanceName,
2004
+ })}`,
2005
+ );
1975
2006
 
1976
2007
  // Lease guardian token. The /readyz check confirms both gateway and
1977
2008
  // assistant are reachable. Retry with backoff in case there is a brief
@@ -31,6 +31,8 @@ export interface EnsureBinaryOptions {
31
31
  runInstall?: (argv: string[]) => boolean;
32
32
  /** Confirm the install. Default: promptLine "[Y/n]" on Enter-as-yes. */
33
33
  confirmInstall?: () => Promise<boolean>;
34
+ platform?: NodeJS.Platform;
35
+ confirmHomebrewInstall?: () => Promise<boolean>;
34
36
  streams?: {
35
37
  input?: NodeJS.ReadStream | Readable;
36
38
  output?: NodeJS.WriteStream | Writable;
@@ -69,9 +71,60 @@ export async function ensureBinaryInstalled(
69
71
  }
70
72
 
71
73
  const pmAvailable = opts.pmAvailable ?? probePackageManager;
72
- const argv = Object.entries(opts.packages).find(([pm]) =>
73
- pmAvailable(pm),
74
- )?.[1];
74
+ let argv = Object.entries(opts.packages).find(([pm]) => pmAvailable(pm))?.[1];
75
+ if (
76
+ !argv &&
77
+ (opts.platform ?? process.platform) === "darwin" &&
78
+ opts.packages.brew
79
+ ) {
80
+ const brew = ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"].find(
81
+ pmAvailable,
82
+ );
83
+ if (brew) {
84
+ const prefix = brew.slice(0, -"/bin/brew".length);
85
+ process.env.PATH = `${prefix}/bin:${prefix}/sbin:${process.env.PATH ?? ""}`;
86
+ argv = [brew, ...opts.packages.brew.slice(1)];
87
+ }
88
+ }
89
+ if (
90
+ !argv &&
91
+ (opts.platform ?? process.platform) === "darwin" &&
92
+ opts.packages.brew
93
+ ) {
94
+ output.write(
95
+ "Homebrew is required to install this dependency, but it isn't installed.\n",
96
+ );
97
+ const confirmHomebrew =
98
+ opts.confirmHomebrewInstall ?? defaultConfirm(opts, "Homebrew");
99
+ let approved = false;
100
+ try {
101
+ approved = await confirmHomebrew();
102
+ } catch {
103
+ /* No interactive approval. */
104
+ }
105
+ if (approved) {
106
+ const bootstrap = [
107
+ "/bin/bash",
108
+ "-c",
109
+ '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/7a3f48c7e498d7df3237fd41c30f4d492857edf4/install.sh)"',
110
+ ];
111
+ const installed = opts.runInstall
112
+ ? opts.runInstall(bootstrap)
113
+ : runInstallArgv(bootstrap);
114
+ const brew = installed
115
+ ? ["brew", "/opt/homebrew/bin/brew", "/usr/local/bin/brew"].find(
116
+ pmAvailable,
117
+ )
118
+ : undefined;
119
+ if (brew) {
120
+ const prefix =
121
+ brew === "brew" ? "" : brew.slice(0, -"/bin/brew".length);
122
+ if (prefix)
123
+ process.env.PATH = `${prefix}/bin:${prefix}/sbin:${process.env.PATH ?? ""}`;
124
+ argv = [brew, ...opts.packages.brew.slice(1)];
125
+ }
126
+ }
127
+ }
75
128
  if (!argv) {
76
129
  output.write(`⚠ No supported package manager found. ${opts.manualHint}\n`);
77
130
  return null;
@@ -90,7 +143,10 @@ export async function ensureBinaryInstalled(
90
143
  }
91
144
 
92
145
  /** Default confirm: visible prompt, Enter = yes, non-TTY throws. */
93
- function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
146
+ function defaultConfirm(
147
+ opts: EnsureBinaryOptions,
148
+ binary = opts.binary,
149
+ ): () => Promise<boolean> {
94
150
  return async () => {
95
151
  const input = opts.streams?.input ?? process.stdin;
96
152
  const output = opts.streams?.output ?? process.stdout;
@@ -99,7 +155,7 @@ function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
99
155
  throw new Error("Install confirm requires an interactive terminal.");
100
156
  }
101
157
  const answer = (
102
- await readLineText(`Install ${opts.binary} now? [Y/n] `, {
158
+ await readLineText(`Install ${binary} now? [Y/n] `, {
103
159
  input,
104
160
  output,
105
161
  })
@@ -114,7 +170,10 @@ function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
114
170
  function runInstallArgv(argv: string[]): boolean {
115
171
  const [command, ...args] = argv;
116
172
  if (!command) return false;
117
- const result = spawnSync(command, args, { stdio: "inherit" });
173
+ const result = spawnSync(command, args, {
174
+ stdio: "inherit",
175
+ windowsHide: true,
176
+ });
118
177
  return !result.error && result.status === 0;
119
178
  }
120
179
 
@@ -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,
@@ -52,7 +53,6 @@ import {
52
53
  import { logHatchNextSteps } from "./hatch-next-steps.js";
53
54
  import { printInstanceDetails } from "./instance-details.js";
54
55
  import { checkProviderApiKey } from "./api-key-check.js";
55
- import { loopbackSafeFetch } from "./loopback-fetch.js";
56
56
  import {
57
57
  describeLoginError,
58
58
  ensureCloudLoginInteractive,
@@ -156,13 +156,13 @@ export interface HatchLocalParams {
156
156
  name?: string | null;
157
157
  /** Build from a local source tree and hot-reload on change. */
158
158
  watch?: boolean;
159
- /** Stay attached after hatch and poll the gateway until it stops. */
160
- keepAlive?: boolean;
161
159
  /** Hatch-time config values (key → value). */
162
160
  configValues?: Record<string, string>;
163
161
  /** Extra env vars forwarded into the assistant process. */
164
162
  flagEnvVars?: Record<string, string>;
165
163
  setupProviderCredentials?: boolean;
164
+ /** Whole-installation state installed before the first service startup. */
165
+ initialArchive?: string;
166
166
  /**
167
167
  * Show the per-service startup logs (assistant, credential executor,
168
168
  * gateway) and the extended next steps. Off by default — the happy path
@@ -192,13 +192,7 @@ export interface HatchLocalResult {
192
192
  export async function hatchLocal(
193
193
  params: HatchLocalParams = {},
194
194
  ): Promise<HatchLocalResult> {
195
- const {
196
- name,
197
- watch = false,
198
- keepAlive = false,
199
- configValues = {},
200
- flagEnvVars = {},
201
- } = params;
195
+ const { name, watch = false, configValues = {}, flagEnvVars = {} } = params;
202
196
  const reporter = params.reporter ?? consoleLifecycleReporter;
203
197
  const verbose = params.verbose ?? false;
204
198
  const setupProviderCredentials = params.setupProviderCredentials !== false;
@@ -300,7 +294,20 @@ export async function hatchLocal(
300
294
  const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
301
295
 
302
296
  reporter.progress(3, 6, "Starting assistant...");
303
- 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);
304
311
  const bootstrapSecret = generateLocalSigningKey();
305
312
  let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
306
313
  try {
@@ -454,46 +461,5 @@ export async function hatchLocal(
454
461
  guardianAccessToken,
455
462
  };
456
463
 
457
- if (keepAlive) {
458
- const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
459
- const healthTarget = "Gateway";
460
- const POLL_INTERVAL_MS = 5000;
461
- const MAX_FAILURES = 3;
462
- let consecutiveFailures = 0;
463
-
464
- const shutdown = async (): Promise<void> => {
465
- reporter.log("\nShutting down local processes...");
466
- await stopLocalProcesses(resources);
467
- process.exit(0);
468
- };
469
-
470
- process.on("SIGTERM", () => void shutdown());
471
- process.on("SIGINT", () => void shutdown());
472
-
473
- // Poll the health endpoint until it stops responding.
474
- while (true) {
475
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
476
- try {
477
- const res = await loopbackSafeFetch(healthUrl, {
478
- signal: AbortSignal.timeout(3000),
479
- });
480
- if (res.ok) {
481
- consecutiveFailures = 0;
482
- } else {
483
- consecutiveFailures++;
484
- }
485
- } catch {
486
- consecutiveFailures++;
487
- }
488
- if (consecutiveFailures >= MAX_FAILURES) {
489
- reporter.log(
490
- `\n⚠️ ${healthTarget} stopped responding — shutting down.`,
491
- );
492
- await stopLocalProcesses(resources);
493
- process.exit(1);
494
- }
495
- }
496
- }
497
-
498
464
  return result;
499
465
  }
@@ -1,8 +1,21 @@
1
+ import { getCurrentEnvironment } from "./environments/resolve.js";
2
+
3
+ function cliCommand(): string {
4
+ // Production keeps the bare `cadet` binary; every non-production
5
+ // environment installs as `cadet-<env>` (the same rule as the config and
6
+ // state dirs — see environments/paths.ts).
7
+ const env = getCurrentEnvironment();
8
+ return env.name === "production" ? "cadet" : `cadet-${env.name}`;
9
+ }
10
+
1
11
  export function logHatchNextSteps(log: (message: string) => void): void {
12
+ const command = cliCommand();
2
13
  log("Next steps:");
3
- log(" - Run `cadet client` to interact with your cadet via the terminal");
4
14
  log(
5
- " - Run `cadet tunnel` to make your cadet publicly accessible and interact with it via the browser",
15
+ ` - Run \`${command} client\` to interact with your cadet via the terminal`,
16
+ );
17
+ log(
18
+ ` - Run \`${command} tunnel\` to make your cadet publicly accessible and interact with it via the browser`,
6
19
  );
7
20
  log("");
8
21
  }