@mycadet/cli 0.12.9 → 0.12.11

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 (55) hide show
  1. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  5. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  6. package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  7. package/package.json +4 -3
  8. package/src/__tests__/client-web-config.test.ts +0 -1
  9. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  10. package/src/__tests__/devices.test.ts +62 -0
  11. package/src/__tests__/ensure-binary.test.ts +52 -0
  12. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  13. package/src/__tests__/multi-local.test.ts +3 -3
  14. package/src/__tests__/nginx-ingress.test.ts +58 -26
  15. package/src/__tests__/pair.test.ts +149 -78
  16. package/src/__tests__/ps-cloud-status.test.ts +6 -4
  17. package/src/__tests__/setup.test.ts +14 -4
  18. package/src/__tests__/teleport.test.ts +0 -15
  19. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  20. package/src/__tests__/tunnel-stop.test.ts +226 -0
  21. package/src/__tests__/tunnel.test.ts +265 -70
  22. package/src/__tests__/uninstall.test.ts +140 -8
  23. package/src/commands/client.ts +4 -6
  24. package/src/commands/devices.ts +66 -30
  25. package/src/commands/hatch.ts +14 -53
  26. package/src/commands/pair.ts +147 -38
  27. package/src/commands/ps.ts +10 -20
  28. package/src/commands/teleport.ts +2 -21
  29. package/src/commands/tunnel.ts +67 -28
  30. package/src/commands/uninstall.ts +106 -71
  31. package/src/commands/upgrade.ts +6 -1
  32. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  33. package/src/lib/assistant-config.ts +20 -5
  34. package/src/lib/cloud-client.ts +0 -3
  35. package/src/lib/cloud-login-poll.ts +75 -0
  36. package/src/lib/cloud-login.ts +3 -2
  37. package/src/lib/color.ts +4 -0
  38. package/src/lib/docker.ts +46 -70
  39. package/src/lib/ensure-binary.ts +65 -6
  40. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  41. package/src/lib/environments/paths.ts +16 -8
  42. package/src/lib/environments/resolve.ts +2 -4
  43. package/src/lib/hatch-local.ts +16 -59
  44. package/src/lib/hatch-next-steps.ts +18 -9
  45. package/src/lib/live-voice/tunnel.ts +2 -2
  46. package/src/lib/local.ts +42 -9
  47. package/src/lib/nginx-ingress.ts +124 -24
  48. package/src/lib/step-runner.ts +6 -1
  49. package/src/lib/tunnel.ts +86 -34
  50. package/src/lib/tunnels/cadet.ts +12 -1
  51. package/src/lib/tunnels/cloudflare.ts +36 -1
  52. package/src/lib/tunnels/ngrok.ts +16 -2
  53. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  54. package/src/lib/tunnels/tailscale.ts +79 -1
  55. package/src/lib/xdg-log.ts +11 -0
package/src/lib/docker.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  ASSISTANT_INTERNAL_PORT,
28
28
  getDefaultPorts,
29
29
  } from "./environments/paths.js";
30
+ import { green, yellow } from "./color.js";
30
31
  import { getCurrentEnvironment } from "./environments/resolve.js";
31
32
  import { leaseGuardianToken } from "./guardian-token";
32
33
  import { logHatchNextSteps } from "./hatch-next-steps.js";
@@ -42,7 +43,6 @@ import { isDevVersion } from "./version-compat.js";
42
43
  import {
43
44
  collectProviderApiKey,
44
45
  configureHatchProviderApiKey,
45
- formatProviderName,
46
46
  promptProviderChoice,
47
47
  resolveHatchProvider,
48
48
  shouldPromptForHatchProvider,
@@ -305,7 +305,6 @@ export interface HatchDockerParams {
305
305
  * also enables hot-reload.
306
306
  */
307
307
  sourcePath?: string | null;
308
- analyze?: boolean;
309
308
  /**
310
309
  * Name of an existing container whose network namespace the assistant,
311
310
  * gateway, and credential-executor join (`--network=container:<name>`),
@@ -1544,7 +1543,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1544
1543
  }
1545
1544
  }
1546
1545
  }
1547
- log("✅ Docker images acquired");
1546
+ log(green("✅ Docker images acquired", process.stdout));
1548
1547
  }
1549
1548
 
1550
1549
  const res = dockerResourceNames(instanceName);
@@ -1719,7 +1718,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1719
1718
  logFd,
1720
1719
  runtimeUrl,
1721
1720
  containersUpAt,
1722
- analyze: params.analyze ?? false,
1723
1721
  });
1724
1722
 
1725
1723
  if (!ready && !(watch && repoRoot)) {
@@ -1745,18 +1743,16 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1745
1743
  );
1746
1744
  } else if (providerSetupAction === "missing-token" && provider !== null) {
1747
1745
  log(
1748
- `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
1749
- ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
1746
+ yellow(
1747
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
1748
+ ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
1749
+ process.stdout,
1750
+ ),
1750
1751
  );
1751
1752
  } else if (
1752
1753
  providerSetupAction === "configure" &&
1753
1754
  provider !== undefined
1754
1755
  ) {
1755
- log(
1756
- provider === null
1757
- ? "Checking provider credentials..."
1758
- : `Checking ${formatProviderName(provider)} credentials...`,
1759
- );
1760
1756
  await configureHatchProviderApiKey({
1761
1757
  gatewayUrl: runtimeUrl,
1762
1758
  provider,
@@ -1830,7 +1826,6 @@ async function waitForGatewayAndLease(opts: {
1830
1826
  logFd: number | "ignore";
1831
1827
  runtimeUrl: string;
1832
1828
  containersUpAt: number;
1833
- analyze: boolean;
1834
1829
  }): Promise<{
1835
1830
  ready: boolean;
1836
1831
  guardianAccessToken?: string;
@@ -1846,7 +1841,6 @@ async function waitForGatewayAndLease(opts: {
1846
1841
  logFd,
1847
1842
  runtimeUrl,
1848
1843
  containersUpAt,
1849
- analyze,
1850
1844
  } = opts;
1851
1845
 
1852
1846
  const log = (msg: string): void => {
@@ -1855,25 +1849,12 @@ async function waitForGatewayAndLease(opts: {
1855
1849
  };
1856
1850
 
1857
1851
  if (detached) {
1858
- log("\n✅ Docker assistant hatched!\n");
1859
- log("Instance details:");
1860
- printInstanceDetails(
1861
- {
1862
- name: instanceName,
1863
- runtime: runtimeUrl,
1864
- container: containerName,
1865
- },
1866
- log,
1867
- );
1868
- log("");
1869
- log(`Stop with: cadet retire ${instanceName}`);
1870
-
1871
- // Lease a guardian token even in detached mode so that `cadet ps`,
1872
- // `cadet exec`, and other CLI commands can authenticate to the
1873
- // gateway. Skip the /readyz readiness poll (the caller asked to detach
1874
- // and not block) but retry the lease itself since the gateway may need
1875
- // a moment to accept connections after the container starts.
1876
- const leaseStart = Date.now();
1852
+ // Lease a guardian token BEFORE declaring the assistant hatched so the
1853
+ // banner means `cadet ps`, `cadet exec`, and other CLI commands can
1854
+ // already authenticate. Skip the /readyz readiness poll (the caller
1855
+ // asked to detach and not block) but retry the lease silently since
1856
+ // the gateway may need a moment to accept connections after the
1857
+ // container starts; only a final failure surfaces to the user.
1877
1858
  const leaseDeadline = containersUpAt + DOCKER_READY_TIMEOUT_MS;
1878
1859
  let guardianAccessToken: string | undefined;
1879
1860
  while (Date.now() < leaseDeadline) {
@@ -1884,27 +1865,33 @@ async function waitForGatewayAndLease(opts: {
1884
1865
  bootstrapSecret,
1885
1866
  );
1886
1867
  guardianAccessToken = tokenData.accessToken;
1887
- const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
1888
- log(
1889
- `Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId})`,
1890
- );
1891
1868
  break;
1892
- } catch (err) {
1893
- const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
1894
- const msg = err instanceof Error ? err.message : String(err);
1895
- log(
1896
- `Guardian token lease: attempt failed after ${elapsed}s (${msg.split("\n")[0]}), retrying...`,
1897
- );
1869
+ } catch {
1898
1870
  await new Promise((r) => setTimeout(r, 2000));
1899
1871
  }
1900
1872
  }
1901
1873
  if (!guardianAccessToken) {
1902
1874
  log(
1903
- `⚠️ Guardian token lease failed after ${DOCKER_READY_TIMEOUT_MS / 1000}s.\n` +
1904
- ` The assistant is running but CLI commands (cadet ps, cadet exec) will not authenticate.\n` +
1905
- ` Re-hatch or run \`cadet setup\` to recover.`,
1875
+ yellow(
1876
+ `⚠️ Guardian token lease failed after ${DOCKER_READY_TIMEOUT_MS / 1000}s.\n` +
1877
+ ` The assistant is running but CLI commands (cadet ps, cadet exec) will not authenticate.\n` +
1878
+ ` Re-hatch or run \`cadet setup\` to recover.`,
1879
+ process.stdout,
1880
+ ),
1906
1881
  );
1907
1882
  }
1883
+
1884
+ log(`\n${green("✅ Docker assistant hatched!", process.stdout)}\n`);
1885
+ log("Instance details:");
1886
+ printInstanceDetails(
1887
+ {
1888
+ name: instanceName,
1889
+ runtime: runtimeUrl,
1890
+ container: containerName,
1891
+ },
1892
+ log,
1893
+ );
1894
+ log("");
1908
1895
  return { ready: true, guardianAccessToken };
1909
1896
  }
1910
1897
 
@@ -1973,19 +1960,16 @@ async function waitForGatewayAndLease(opts: {
1973
1960
  const containersUpToReadyMs = readyAt - start;
1974
1961
  const elapsedSec = (containersUpToReadyMs / 1000).toFixed(1);
1975
1962
  log(`Assistant ready after ${elapsedSec}s`);
1976
- if (analyze) {
1977
- console.info(
1978
- `[cadet-hatch-timing] ${JSON.stringify({
1979
- containers_up_to_ready_ms: containersUpToReadyMs,
1980
- instance: instanceName,
1981
- })}`,
1982
- );
1983
- }
1963
+ console.info(
1964
+ `[cadet-hatch-timing] ${JSON.stringify({
1965
+ containers_up_to_ready_ms: containersUpToReadyMs,
1966
+ instance: instanceName,
1967
+ })}`,
1968
+ );
1984
1969
 
1985
1970
  // Lease guardian token. The /readyz check confirms both gateway and
1986
1971
  // assistant are reachable. Retry with backoff in case there is a brief
1987
1972
  // window where readiness passes but the guardian endpoint is not yet ready.
1988
- log(`Guardian token lease: starting for ${instanceName} at ${runtimeUrl}`);
1989
1973
  const leaseStart = Date.now();
1990
1974
  const leaseDeadline = start + DOCKER_READY_TIMEOUT_MS;
1991
1975
  let leaseSuccess = false;
@@ -1999,38 +1983,30 @@ async function waitForGatewayAndLease(opts: {
1999
1983
  instanceName,
2000
1984
  bootstrapSecret,
2001
1985
  );
2002
- const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
2003
- log(
2004
- `Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
2005
- );
2006
1986
  guardianAccessToken = tokenData.accessToken;
2007
1987
  leaseSuccess = true;
2008
1988
  break;
2009
1989
  } catch (err) {
2010
1990
  lastLeaseError =
2011
1991
  err instanceof Error ? (err.stack ?? err.message) : String(err);
2012
- // Log periodically so the user knows we're still trying
2013
- const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
2014
- log(
2015
- `Guardian token lease: attempt failed after ${elapsed}s (${
2016
- lastLeaseError.split("\n")[0]
2017
- }), retrying...`,
2018
- );
2019
1992
  }
2020
1993
  await new Promise((r) => setTimeout(r, 2000));
2021
1994
  }
2022
1995
 
2023
1996
  if (!leaseSuccess) {
2024
1997
  log(
2025
- `\u26a0\ufe0f Guardian token lease: FAILED after ${(
2026
- (Date.now() - leaseStart) /
2027
- 1000
2028
- ).toFixed(1)}s — ${lastLeaseError ?? "unknown error"}`,
1998
+ yellow(
1999
+ `\u26a0\ufe0f Guardian token lease: FAILED after ${(
2000
+ (Date.now() - leaseStart) /
2001
+ 1000
2002
+ ).toFixed(1)}s — ${lastLeaseError ?? "unknown error"}`,
2003
+ process.stdout,
2004
+ ),
2029
2005
  );
2030
2006
  }
2031
2007
 
2032
2008
  log("");
2033
- log(`\u2705 Docker containers are up and running!`);
2009
+ log(green(`\u2705 Docker containers are up and running!`, process.stdout));
2034
2010
  log(` Name: ${instanceName}`);
2035
2011
  log(` Runtime: ${runtimeUrl}`);
2036
2012
  log("");
@@ -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
 
@@ -39,9 +39,11 @@ const {
39
39
  getConfigDir,
40
40
  getConfigDirs,
41
41
  getDefaultPorts,
42
+ getInputHistoryPath,
42
43
  getLockfilePath,
43
44
  getLockfilePaths,
44
45
  getMultiInstanceDir,
46
+ getSharedStateDir,
45
47
  } = await import("../paths.js");
46
48
  type EnvironmentDefinition =
47
49
  import("@mycadet/environments").EnvironmentDefinition;
@@ -58,7 +60,12 @@ const dev: EnvironmentDefinition = {
58
60
  webUrl: "https://dev.mycadet.ai",
59
61
  };
60
62
 
61
- const XDG_ENV_VARS = ["XDG_DATA_HOME", "XDG_CONFIG_HOME", "APPDATA"] as const;
63
+ const XDG_ENV_VARS = [
64
+ "XDG_DATA_HOME",
65
+ "XDG_CONFIG_HOME",
66
+ "XDG_STATE_HOME",
67
+ "APPDATA",
68
+ ] as const;
62
69
 
63
70
  describe("path helpers", () => {
64
71
  let savedEnv: Record<string, string | undefined>;
@@ -224,6 +231,25 @@ describe("path helpers", () => {
224
231
  });
225
232
  });
226
233
 
234
+ describe("getSharedStateDir", () => {
235
+ test("is not environment-scoped: ~/.local/state/cadet for every env", () => {
236
+ expect(getSharedStateDir()).toBe(
237
+ join(TEST_HOME, ".local", "state", "cadet"),
238
+ );
239
+ });
240
+
241
+ test("respects XDG_STATE_HOME", () => {
242
+ process.env.XDG_STATE_HOME = "/custom/state";
243
+ expect(getSharedStateDir()).toBe("/custom/state/cadet");
244
+ });
245
+
246
+ test("holds the shared terminal input history file", () => {
247
+ expect(getInputHistoryPath()).toBe(
248
+ join(getSharedStateDir(), "input-history"),
249
+ );
250
+ });
251
+ });
252
+
227
253
  describe("getDefaultPorts", () => {
228
254
  test("returns production defaults for production", () => {
229
255
  const ports = getDefaultPorts(prod);
@@ -82,18 +82,26 @@ export function getLogDir(env: EnvironmentDefinition): string {
82
82
  }
83
83
 
84
84
  /**
85
- * Path to the interactive CLI's input history file.
85
+ * The interactive CLI's shared state directory — `$XDG_STATE_HOME/cadet`.
86
86
  *
87
- * Follows the XDG Base Directory spec: history files are state data
88
- * (persistent across runs but not portable / user-owned content), so they
89
- * belong under `$XDG_STATE_HOME`, mirroring `bash`, `zsh`, `psql`, and `gh`.
90
- * Defaults to `~/.local/state/cadet/input-history`.
87
+ * Follows the XDG Base Directory spec: session state is data that persists
88
+ * across runs but is not portable user-owned content, mirroring `bash`,
89
+ * `zsh`, `psql`, and `gh`. Defaults to `~/.local/state/cadet`.
91
90
  *
92
- * Not environment-scoped: terminal input history is per-user, not per-assistant,
93
- * so dev and prod CLIs share the same history file.
91
+ * Not environment-scoped: it holds per-user state (the terminal input
92
+ * history, below) that dev and prod CLIs share.
93
+ */
94
+ export function getSharedStateDir(): string {
95
+ return join(xdgStateHome(), "cadet");
96
+ }
97
+
98
+ /**
99
+ * Path to the interactive CLI's input history file, inside
100
+ * {@link getSharedStateDir}. Defaults to
101
+ * `~/.local/state/cadet/input-history`.
94
102
  */
95
103
  export function getInputHistoryPath(): string {
96
- return join(xdgStateHome(), "cadet", "input-history");
104
+ return join(getSharedStateDir(), "input-history");
97
105
  }
98
106
 
99
107
  /**
@@ -67,10 +67,8 @@ export function getCurrentEnvironment(
67
67
  const seed = SEEDS[name];
68
68
  if (!seed) {
69
69
  if (name !== DEFAULT_ENVIRONMENT_NAME) {
70
- // Warn on stderr instead of throwing, to match the silent-fallback
71
- // behavior in assistant/src/util/platform.ts:getXdgCadetConfigDirName,
72
- // which silently falls back to production; the CLI agrees so neither
73
- // writer ends up in a disjoint state on a typo.
70
+ // Warn on stderr instead of throwing: an unknown environment falls back
71
+ // to production, so a typo never splits writes between two states.
74
72
  process.stderr.write(
75
73
  `warning: unknown environment "${name}"; falling back to "${DEFAULT_ENVIRONMENT_NAME}". ` +
76
74
  `Add it to packages/environments/src/seeds.ts and rebuild if this was intentional.\n`,
@@ -21,6 +21,7 @@ import {
21
21
  setActiveAssistant,
22
22
  } from "./assistant-config.js";
23
23
  import type { AssistantEntry } from "./assistant-config.js";
24
+ import { green, yellow } from "./color.js";
24
25
  import { buildHatchConfigValues, writeInitialConfig } from "./config-utils.js";
25
26
  import {
26
27
  generateLocalSigningKey,
@@ -51,7 +52,6 @@ import {
51
52
  import { logHatchNextSteps } from "./hatch-next-steps.js";
52
53
  import { printInstanceDetails } from "./instance-details.js";
53
54
  import { checkProviderApiKey } from "./api-key-check.js";
54
- import { loopbackSafeFetch } from "./loopback-fetch.js";
55
55
  import {
56
56
  describeLoginError,
57
57
  ensureCloudLoginInteractive,
@@ -155,8 +155,6 @@ export interface HatchLocalParams {
155
155
  name?: string | null;
156
156
  /** Build from a local source tree and hot-reload on change. */
157
157
  watch?: boolean;
158
- /** Stay attached after hatch and poll the gateway until it stops. */
159
- keepAlive?: boolean;
160
158
  /** Hatch-time config values (key → value). */
161
159
  configValues?: Record<string, string>;
162
160
  /** Extra env vars forwarded into the assistant process. */
@@ -191,13 +189,7 @@ export interface HatchLocalResult {
191
189
  export async function hatchLocal(
192
190
  params: HatchLocalParams = {},
193
191
  ): Promise<HatchLocalResult> {
194
- const {
195
- name,
196
- watch = false,
197
- keepAlive = false,
198
- configValues = {},
199
- flagEnvVars = {},
200
- } = params;
192
+ const { name, watch = false, configValues = {}, flagEnvVars = {} } = params;
201
193
  const reporter = params.reporter ?? consoleLifecycleReporter;
202
194
  const verbose = params.verbose ?? false;
203
195
  const setupProviderCredentials = params.setupProviderCredentials !== false;
@@ -379,14 +371,18 @@ export async function hatchLocal(
379
371
  ) {
380
372
  const delayMs = 2000 * 2 ** (attempt - 1);
381
373
  reporter.error(
382
- `⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
374
+ yellow(
375
+ `⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
376
+ ),
383
377
  );
384
378
  await new Promise((r) => setTimeout(r, delayMs));
385
379
  } else {
386
380
  reporter.error(
387
- `⚠️ Guardian token lease failed${err instanceof GuardianLeasePermanentError ? ", not retrying — a malformed 200 will not fix itself" : ` after ${maxLeaseAttempts} attempts`}: ${err}\n` +
388
- ` The assistant is running but guardian-token.json was not written.\n` +
389
- ` Run \`cadet restart ${JSON.stringify(instanceName)} --repair-guardian\` to re-lease a token.`,
381
+ yellow(
382
+ `⚠️ Guardian token lease failed${err instanceof GuardianLeasePermanentError ? ", not retrying — a malformed 200 will not fix itself" : ` after ${maxLeaseAttempts} attempts`}: ${err}\n` +
383
+ ` The assistant is running but guardian-token.json was not written.\n` +
384
+ ` Run \`cadet restart ${JSON.stringify(instanceName)} --repair-guardian\` to re-lease a token.`,
385
+ ),
390
386
  );
391
387
  }
392
388
  }
@@ -398,8 +394,10 @@ export async function hatchLocal(
398
394
 
399
395
  if (provider !== undefined && provider !== null && !guardianAccessToken) {
400
396
  reporter.error(
401
- `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
402
- ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
397
+ yellow(
398
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
399
+ ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
400
+ ),
403
401
  );
404
402
  } else if (provider !== undefined) {
405
403
  await configureHatchProviderApiKey({
@@ -429,7 +427,7 @@ export async function hatchLocal(
429
427
  }
430
428
 
431
429
  reporter.log("");
432
- reporter.log(`✅ Local assistant hatched!`);
430
+ reporter.log(green(`✅ Local assistant hatched!`, process.stdout));
433
431
  printInstanceDetails(
434
432
  {
435
433
  name: instanceName,
@@ -438,7 +436,7 @@ export async function hatchLocal(
438
436
  },
439
437
  (message) => reporter.log(message),
440
438
  );
441
- logHatchNextSteps((message) => reporter.log(message), verbose);
439
+ logHatchNextSteps((message) => reporter.log(message));
442
440
 
443
441
  const result: HatchLocalResult = {
444
442
  assistantId: instanceName,
@@ -447,46 +445,5 @@ export async function hatchLocal(
447
445
  guardianAccessToken,
448
446
  };
449
447
 
450
- if (keepAlive) {
451
- const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
452
- const healthTarget = "Gateway";
453
- const POLL_INTERVAL_MS = 5000;
454
- const MAX_FAILURES = 3;
455
- let consecutiveFailures = 0;
456
-
457
- const shutdown = async (): Promise<void> => {
458
- reporter.log("\nShutting down local processes...");
459
- await stopLocalProcesses(resources);
460
- process.exit(0);
461
- };
462
-
463
- process.on("SIGTERM", () => void shutdown());
464
- process.on("SIGINT", () => void shutdown());
465
-
466
- // Poll the health endpoint until it stops responding.
467
- while (true) {
468
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
469
- try {
470
- const res = await loopbackSafeFetch(healthUrl, {
471
- signal: AbortSignal.timeout(3000),
472
- });
473
- if (res.ok) {
474
- consecutiveFailures = 0;
475
- } else {
476
- consecutiveFailures++;
477
- }
478
- } catch {
479
- consecutiveFailures++;
480
- }
481
- if (consecutiveFailures >= MAX_FAILURES) {
482
- reporter.log(
483
- `\n⚠️ ${healthTarget} stopped responding — shutting down.`,
484
- );
485
- await stopLocalProcesses(resources);
486
- process.exit(1);
487
- }
488
- }
489
- }
490
-
491
448
  return result;
492
449
  }
@@ -1,12 +1,21 @@
1
- export function logHatchNextSteps(
2
- log: (message: string) => void,
3
- verbose = false,
4
- ): void {
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
+
11
+ export function logHatchNextSteps(log: (message: string) => void): void {
12
+ const command = cliCommand();
5
13
  log("Next steps:");
6
- log(" cadet client");
7
- if (verbose) {
8
- log(' cadet message "hello"');
9
- log(" cadet events");
10
- }
14
+ log(
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`,
19
+ );
11
20
  log("");
12
21
  }
@@ -28,7 +28,7 @@
28
28
  * mint again rather than reuse what resolution returned.
29
29
  */
30
30
 
31
- import { tunnelHostForPlatformHost } from "@mycadet/service-contracts/ingress";
31
+ import { tunnelHostForCloudHost } from "@mycadet/service-contracts/ingress";
32
32
 
33
33
  import { authHeaders } from "../cloud-client.js";
34
34
 
@@ -66,7 +66,7 @@ export function tunnelHostForCloudUrl(cloudUrl: string): string {
66
66
  }
67
67
  try {
68
68
  return (
69
- tunnelHostForPlatformHost(new URL(cloudUrl).host) ??
69
+ tunnelHostForCloudHost(new URL(cloudUrl).host) ??
70
70
  DEFAULT_TUNNEL_HOST
71
71
  );
72
72
  } catch {