@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64

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 (185) hide show
  1. package/README.md +2 -2
  2. package/knip.json +1 -1
  3. package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
  4. package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
  5. package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
  6. package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
  7. package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
  8. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
  9. package/node_modules/@mycadet/assistant-api/index.ts +11 -11
  10. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
  11. package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  12. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  13. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  14. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  15. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  16. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  17. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  18. package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
  19. package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
  20. package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  21. package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  22. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  23. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  24. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  25. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  26. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  27. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  28. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  29. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
  30. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
  31. package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  32. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  33. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  34. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  35. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  36. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  37. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  38. package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
  39. package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
  40. package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
  41. package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
  42. package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
  43. package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
  44. package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
  45. package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
  46. package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
  47. package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
  48. package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
  49. package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
  50. package/node_modules/@mycadet/service-contracts/package.json +2 -2
  51. package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
  52. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  53. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  54. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  55. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  56. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  57. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  58. package/package.json +6 -4
  59. package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
  60. package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
  61. package/src/__tests__/assistant-client-refresh.test.ts +2 -2
  62. package/src/__tests__/backup.test.ts +86 -91
  63. package/src/__tests__/client-token.test.ts +1 -1
  64. package/src/__tests__/client-tui-refresh.test.ts +1 -1
  65. package/src/__tests__/client-web-config.test.ts +1 -2
  66. package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
  67. package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
  68. package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
  69. package/src/__tests__/connect-import.test.ts +55 -39
  70. package/src/__tests__/debug.test.ts +38 -38
  71. package/src/__tests__/ensure-binary.test.ts +182 -0
  72. package/src/__tests__/guardian-token.test.ts +149 -56
  73. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  74. package/src/__tests__/helpers/guardian-token.ts +22 -0
  75. package/src/__tests__/helpers/http.ts +10 -0
  76. package/src/__tests__/nginx-ingress.test.ts +15 -20
  77. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  78. package/src/__tests__/orphan-detection.test.ts +1 -1
  79. package/src/__tests__/pair.test.ts +149 -85
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
  81. package/src/__tests__/ps-table.test.ts +79 -0
  82. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  83. package/src/__tests__/retire.test.ts +58 -199
  84. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  85. package/src/__tests__/sleep.test.ts +19 -19
  86. package/src/__tests__/statefulset.test.ts +8 -8
  87. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
  88. package/src/__tests__/teleport-backup.test.ts +3 -3
  89. package/src/__tests__/teleport.test.ts +180 -211
  90. package/src/__tests__/tunnel.test.ts +490 -270
  91. package/src/__tests__/uninstall.test.ts +150 -9
  92. package/src/__tests__/upgrade-local.test.ts +3 -3
  93. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  94. package/src/__tests__/wake.test.ts +260 -43
  95. package/src/__tests__/workos-pkce.test.ts +2 -2
  96. package/src/commands/backup.ts +24 -24
  97. package/src/commands/client.ts +76 -78
  98. package/src/commands/completions.test.ts +104 -0
  99. package/src/commands/completions.ts +87 -0
  100. package/src/commands/debug.ts +20 -20
  101. package/src/commands/exec.ts +12 -12
  102. package/src/commands/flags.ts +2 -2
  103. package/src/commands/hatch.ts +27 -24
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/pair.ts +140 -37
  107. package/src/commands/ps.ts +232 -86
  108. package/src/commands/restart.ts +30 -0
  109. package/src/commands/restore.ts +140 -22
  110. package/src/commands/retire.ts +48 -150
  111. package/src/commands/rollback.ts +20 -26
  112. package/src/commands/setup.ts +2 -2
  113. package/src/commands/sleep.ts +2 -2
  114. package/src/commands/teleport.ts +110 -129
  115. package/src/commands/terminal.ts +158 -18
  116. package/src/commands/tunnel.ts +156 -175
  117. package/src/commands/uninstall.ts +137 -54
  118. package/src/commands/unpair.ts +3 -3
  119. package/src/commands/upgrade.ts +30 -54
  120. package/src/commands/use.ts +1 -1
  121. package/src/commands/wake.ts +166 -50
  122. package/src/index.ts +18 -9
  123. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  124. package/src/lib/__tests__/docker.test.ts +168 -20
  125. package/src/lib/__tests__/job-polling.test.ts +2 -2
  126. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  127. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  128. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  129. package/src/lib/assistant-api-key-resolution.ts +5 -5
  130. package/src/lib/assistant-client.ts +8 -8
  131. package/src/lib/assistant-config.ts +58 -12
  132. package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
  133. package/src/lib/cloud-login.ts +23 -24
  134. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  135. package/src/lib/cloud-values.ts +3 -0
  136. package/src/lib/color.ts +4 -0
  137. package/src/lib/config-utils.ts +1 -1
  138. package/src/lib/detached-process.ts +1 -1
  139. package/src/lib/docker.ts +300 -111
  140. package/src/lib/ensure-binary.ts +171 -0
  141. package/src/lib/environments/__tests__/paths.test.ts +29 -3
  142. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  143. package/src/lib/environments/paths.ts +16 -8
  144. package/src/lib/environments/resolve.ts +10 -12
  145. package/src/lib/flag-args.test.ts +1 -1
  146. package/src/lib/flag-args.ts +1 -1
  147. package/src/lib/guardian-token.ts +37 -5
  148. package/src/lib/hatch-local.ts +32 -29
  149. package/src/lib/hatch-next-steps.ts +5 -9
  150. package/src/lib/health-check.ts +6 -6
  151. package/src/lib/ingress-config.ts +8 -35
  152. package/src/lib/job-polling.ts +2 -2
  153. package/src/lib/live-voice/connection.ts +12 -12
  154. package/src/lib/live-voice/tunnel.test.ts +11 -11
  155. package/src/lib/live-voice/tunnel.ts +15 -15
  156. package/src/lib/local-runtime-client.ts +20 -20
  157. package/src/lib/local.ts +46 -34
  158. package/src/lib/nginx-ingress.ts +40 -15
  159. package/src/lib/pair.ts +1 -1
  160. package/src/lib/process.ts +1 -1
  161. package/src/lib/provider-secrets.ts +3 -3
  162. package/src/lib/retire-apple-container.ts +1 -1
  163. package/src/lib/runtime-url.ts +8 -8
  164. package/src/lib/statefulset.ts +2 -2
  165. package/src/lib/step-runner.ts +6 -1
  166. package/src/lib/sync-cloud-assistants.ts +19 -19
  167. package/src/lib/teleport-backup.ts +4 -4
  168. package/src/lib/terminal-client.ts +17 -17
  169. package/src/lib/terminal-session.ts +24 -24
  170. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  171. package/src/lib/tunnels/cadet.ts +29 -0
  172. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  173. package/src/lib/tunnels/gateway-notify.ts +61 -0
  174. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  175. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  176. package/src/lib/upgrade-lifecycle.ts +8 -21
  177. package/src/lib/upgrade-preflight.ts +2 -2
  178. package/src/lib/workos-pkce.ts +5 -5
  179. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  180. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  181. package/src/commands/recover.ts +0 -131
  182. package/src/commands/roadmap.ts +0 -459
  183. package/src/commands/ssh.ts +0 -144
  184. package/src/commands/workflows.ts +0 -305
  185. package/src/lib/managed-cloud.ts +0 -3
@@ -0,0 +1,171 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import type { Readable, Writable } from "node:stream";
3
+
4
+ /**
5
+ * Consolidated missing-dependency flow for interactive commands: probe for a
6
+ * binary, and when it is absent, offer to install it — naming no commands —
7
+ * then run the right package manager for the platform and verify. Callers
8
+ * supply the recipes; everything else (prompt, package-manager discovery,
9
+ * fallback copy) lives here so every command behaves the same.
10
+ */
11
+
12
+ export interface EnsureBinaryOptions {
13
+ /** The binary as invoked on PATH, e.g. "nginx". */
14
+ binary: string;
15
+ /** What the running command needs it for, e.g. "the tunnel edge". */
16
+ purpose: string;
17
+ /**
18
+ * Package-manager name → full install argv (sudo included where needed),
19
+ * e.g. `{ brew: ["brew", "install", "nginx"] }`. The first entry whose
20
+ * package manager is available wins. Leave out a platform rather than
21
+ * guessing at an unreliable package ID — the manual hint covers it.
22
+ */
23
+ packages: Record<string, string[]>;
24
+ /** Shown when the user declines or auto-install fails. */
25
+ manualHint: string;
26
+ /** Probe: a version-ish string when the binary is usable, null otherwise. */
27
+ probe: () => string | null;
28
+ /** Check that a package manager exists. Default: `<pm> --version` probe. */
29
+ pmAvailable?: (pm: string) => boolean;
30
+ /** Run an install argv; true on success. Default: inherit-stdio spawnSync. */
31
+ runInstall?: (argv: string[]) => boolean;
32
+ /** Confirm the install. Default: promptLine "[Y/n]" on Enter-as-yes. */
33
+ confirmInstall?: () => Promise<boolean>;
34
+ streams?: {
35
+ input?: NodeJS.ReadStream | Readable;
36
+ output?: NodeJS.WriteStream | Writable;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Prompt-then-install a missing binary. Returns the probe result — the
42
+ * existing version when the binary was already present, the fresh one after a
43
+ * successful install — or null when the binary is missing and the user
44
+ * declines, no package manager matches, or the install fails. Non-interactive
45
+ * callers (no TTY) never hang: they get the manual hint and null.
46
+ */
47
+ export async function ensureBinaryInstalled(
48
+ opts: EnsureBinaryOptions,
49
+ ): Promise<string | null> {
50
+ const output = opts.streams?.output ?? process.stdout;
51
+
52
+ const existing = opts.probe();
53
+ if (existing) return existing;
54
+
55
+ output.write(
56
+ `\n${opts.binary} is required for ${opts.purpose}, but it isn't installed.\n`,
57
+ );
58
+
59
+ const confirm = opts.confirmInstall ?? defaultConfirm(opts);
60
+ let install = false;
61
+ try {
62
+ install = await confirm();
63
+ } catch {
64
+ install = false;
65
+ }
66
+ if (!install) {
67
+ output.write(`Skipping install. ${opts.manualHint}\n`);
68
+ return null;
69
+ }
70
+
71
+ const pmAvailable = opts.pmAvailable ?? probePackageManager;
72
+ const argv = Object.entries(opts.packages).find(([pm]) =>
73
+ pmAvailable(pm),
74
+ )?.[1];
75
+ if (!argv) {
76
+ output.write(`⚠ No supported package manager found. ${opts.manualHint}\n`);
77
+ return null;
78
+ }
79
+
80
+ const ok = opts.runInstall ? opts.runInstall(argv) : runInstallArgv(argv);
81
+ const version = ok ? opts.probe() : null;
82
+ if (!version) {
83
+ output.write(
84
+ `⚠ Couldn't install ${opts.binary} automatically. ${opts.manualHint}\n`,
85
+ );
86
+ return null;
87
+ }
88
+ output.write(`✅ Installed ${opts.binary} (${version}).\n`);
89
+ return version;
90
+ }
91
+
92
+ /** Default confirm: visible prompt, Enter = yes, non-TTY throws. */
93
+ function defaultConfirm(opts: EnsureBinaryOptions): () => Promise<boolean> {
94
+ return async () => {
95
+ const input = opts.streams?.input ?? process.stdin;
96
+ const output = opts.streams?.output ?? process.stdout;
97
+ // Readable has no isTTY; only a real terminal answers the prompt.
98
+ if (!("isTTY" in input) || !input.isTTY) {
99
+ throw new Error("Install confirm requires an interactive terminal.");
100
+ }
101
+ const answer = (
102
+ await readLineText(`Install ${opts.binary} now? [Y/n] `, {
103
+ input,
104
+ output,
105
+ })
106
+ )
107
+ .trim()
108
+ .toLowerCase();
109
+ return answer === "" || answer === "y" || answer === "yes";
110
+ };
111
+ }
112
+
113
+ /** Default installer: blocking spawn with inherited stdio (passwords flow). */
114
+ function runInstallArgv(argv: string[]): boolean {
115
+ const [command, ...args] = argv;
116
+ if (!command) return false;
117
+ const result = spawnSync(command, args, { stdio: "inherit" });
118
+ return !result.error && result.status === 0;
119
+ }
120
+
121
+ /** Default package-manager check: the manager answers `--version`. */
122
+ function probePackageManager(pm: string): boolean {
123
+ const result = spawnSync(pm, ["--version"], {
124
+ stdio: "ignore",
125
+ shell: process.platform === "win32",
126
+ windowsHide: true,
127
+ });
128
+ return !result.error && result.status === 0;
129
+ }
130
+
131
+ /**
132
+ * Read one visible line from stdin (Enter submits). Shared by the binary
133
+ * install confirm and the ngrok authtoken paste-in, so both degrade the same
134
+ * way on a non-interactive terminal (the caller decides what empty means).
135
+ */
136
+ export async function readLineText(
137
+ prompt: string,
138
+ streams: { input?: Readable; output?: Writable } = {},
139
+ ): Promise<string> {
140
+ const input = streams.input ?? process.stdin;
141
+ const output = streams.output ?? process.stdout;
142
+
143
+ output.write(prompt);
144
+
145
+ return new Promise((resolve, reject) => {
146
+ let line = "";
147
+ const onData = (chunk: Buffer | string): void => {
148
+ line += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
149
+ const newline = line.indexOf("\n");
150
+ if (newline !== -1) {
151
+ done(line.slice(0, newline).replace(/\r$/, ""));
152
+ }
153
+ };
154
+ const cleanup = (): void => {
155
+ input.removeListener("data", onData);
156
+ input.removeListener("error", onError);
157
+ input.pause();
158
+ };
159
+ const done = (value: string): void => {
160
+ cleanup();
161
+ resolve(value);
162
+ };
163
+ const onError = (err: Error): void => {
164
+ cleanup();
165
+ reject(err);
166
+ };
167
+ input.resume();
168
+ input.on("data", onData);
169
+ input.on("error", onError);
170
+ });
171
+ }
@@ -39,26 +39,33 @@ 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;
48
50
 
49
51
  const prod: EnvironmentDefinition = {
50
52
  name: "production",
51
- platformUrl: "https://cloud.mycadet.ai",
53
+ cloudUrl: "https://cloud.mycadet.ai",
52
54
  webUrl: "https://mycadet.ai",
53
55
  };
54
56
 
55
57
  const dev: EnvironmentDefinition = {
56
58
  name: "dev",
57
- platformUrl: "https://dev-cloud.mycadet.ai",
59
+ cloudUrl: "https://dev-cloud.mycadet.ai",
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);
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  const ENV_VARS_TO_SAVE = [
10
10
  "CADET_ENVIRONMENT",
11
- "CADET_PLATFORM_URL",
11
+ "CADET_CLOUD_URL",
12
12
  "CADET_ASSISTANT_PLATFORM_URL",
13
13
  "CADET_LOCKFILE_DIR",
14
14
  ] as const;
@@ -37,33 +37,33 @@ describe("getCurrentEnvironment", () => {
37
37
  test("returns production seed when no override, no env var", () => {
38
38
  const env = getCurrentEnvironment();
39
39
  expect(env.name).toBe("production");
40
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
40
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
41
41
  });
42
42
 
43
43
  test("returns dev seed when CADET_ENVIRONMENT=dev", () => {
44
44
  process.env.CADET_ENVIRONMENT = "dev";
45
45
  const env = getCurrentEnvironment();
46
46
  expect(env.name).toBe("dev");
47
- expect(env.platformUrl).toBe("https://dev-cloud.mycadet.ai");
47
+ expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
48
48
  });
49
49
 
50
50
  test("returns staging seed when CADET_ENVIRONMENT=staging", () => {
51
51
  process.env.CADET_ENVIRONMENT = "staging";
52
- expect(getCurrentEnvironment().platformUrl).toBe(
52
+ expect(getCurrentEnvironment().cloudUrl).toBe(
53
53
  "https://staging-cloud.mycadet.ai",
54
54
  );
55
55
  });
56
56
 
57
57
  test("returns local seed with localhost URL", () => {
58
58
  process.env.CADET_ENVIRONMENT = "local";
59
- expect(getCurrentEnvironment().platformUrl).toBe("http://localhost:8000");
59
+ expect(getCurrentEnvironment().cloudUrl).toBe("http://localhost:8000");
60
60
  });
61
61
 
62
62
  test("override argument takes priority over CADET_ENVIRONMENT env var", () => {
63
63
  process.env.CADET_ENVIRONMENT = "staging";
64
64
  const env = getCurrentEnvironment("dev");
65
65
  expect(env.name).toBe("dev");
66
- expect(env.platformUrl).toBe("https://dev-cloud.mycadet.ai");
66
+ expect(env.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
67
67
  });
68
68
 
69
69
  test("empty override argument falls through to env var", () => {
@@ -85,7 +85,7 @@ describe("getCurrentEnvironment", () => {
85
85
  try {
86
86
  const env = getCurrentEnvironment("no-such-env");
87
87
  expect(env.name).toBe("production");
88
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
88
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
89
89
  expect(stderr).toHaveBeenCalledWith(
90
90
  expect.stringContaining('unknown environment "no-such-env"'),
91
91
  );
@@ -105,7 +105,7 @@ describe("getCurrentEnvironment", () => {
105
105
  try {
106
106
  const env = getCurrentEnvironment();
107
107
  expect(env.name).toBe("production");
108
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
108
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
109
109
  expect(stderr).toHaveBeenCalledWith(
110
110
  expect.stringContaining('unknown environment "nope"'),
111
111
  );
@@ -128,49 +128,49 @@ describe("getCurrentEnvironment", () => {
128
128
  }
129
129
  });
130
130
 
131
- test("CADET_PLATFORM_URL overrides platformUrl on the resolved definition", () => {
131
+ test("CADET_CLOUD_URL overrides cloudUrl on the resolved definition", () => {
132
132
  process.env.CADET_ENVIRONMENT = "dev";
133
- process.env.CADET_PLATFORM_URL = "https://custom.example.com";
133
+ process.env.CADET_CLOUD_URL = "https://custom.example.com";
134
134
  const env = getCurrentEnvironment();
135
135
  expect(env.name).toBe("dev");
136
- expect(env.platformUrl).toBe("https://custom.example.com");
136
+ expect(env.cloudUrl).toBe("https://custom.example.com");
137
137
  });
138
138
 
139
- test("CADET_PLATFORM_URL override does not affect the seed table", () => {
140
- process.env.CADET_PLATFORM_URL = "https://custom.example.com";
139
+ test("CADET_CLOUD_URL override does not affect the seed table", () => {
140
+ process.env.CADET_CLOUD_URL = "https://custom.example.com";
141
141
  getCurrentEnvironment();
142
- delete process.env.CADET_PLATFORM_URL;
142
+ delete process.env.CADET_CLOUD_URL;
143
143
  const env = getCurrentEnvironment();
144
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
144
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
145
145
  });
146
146
 
147
- test("CADET_ASSISTANT_PLATFORM_URL overrides assistantPlatformUrl", () => {
147
+ test("CADET_ASSISTANT_PLATFORM_URL overrides assistantCloudUrl", () => {
148
148
  process.env.CADET_ASSISTANT_PLATFORM_URL =
149
149
  "http://host.docker.internal:8000";
150
150
  const env = getCurrentEnvironment();
151
- expect(env.assistantPlatformUrl).toBe("http://host.docker.internal:8000");
151
+ expect(env.assistantCloudUrl).toBe("http://host.docker.internal:8000");
152
152
  });
153
153
 
154
- test("CADET_ASSISTANT_PLATFORM_URL does not shadow platformUrl", () => {
154
+ test("CADET_ASSISTANT_PLATFORM_URL does not shadow cloudUrl", () => {
155
155
  process.env.CADET_ASSISTANT_PLATFORM_URL = "http://override";
156
156
  const env = getCurrentEnvironment();
157
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
157
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
158
158
  });
159
159
 
160
- test("does not auto-materialize a new environment from CADET_PLATFORM_URL alone", () => {
160
+ test("does not auto-materialize a new environment from CADET_CLOUD_URL alone", () => {
161
161
  // Unknown env names fall back to production (parity with daemon + Swift).
162
- // The per-field CADET_PLATFORM_URL override is intentionally dropped on
162
+ // The per-field CADET_CLOUD_URL override is intentionally dropped on
163
163
  // the fallback path — fallback returns a pristine production seed so a
164
164
  // typo'd env var can't accidentally stitch together a new environment.
165
165
  process.env.CADET_ENVIRONMENT = "my-custom";
166
- process.env.CADET_PLATFORM_URL = "https://my-custom.example.com";
166
+ process.env.CADET_CLOUD_URL = "https://my-custom.example.com";
167
167
  const stderr = spyOn(process.stderr, "write").mockImplementation(
168
168
  () => true,
169
169
  );
170
170
  try {
171
171
  const env = getCurrentEnvironment();
172
172
  expect(env.name).toBe("production");
173
- expect(env.platformUrl).toBe("https://cloud.mycadet.ai");
173
+ expect(env.cloudUrl).toBe("https://cloud.mycadet.ai");
174
174
  expect(stderr).toHaveBeenCalledWith(
175
175
  expect.stringContaining('unknown environment "my-custom"'),
176
176
  );
@@ -232,7 +232,7 @@ describe("getSeed", () => {
232
232
  const seed = getSeed("dev");
233
233
  expect(seed).toBeDefined();
234
234
  expect(seed?.name).toBe("dev");
235
- expect(seed?.platformUrl).toBe("https://dev-cloud.mycadet.ai");
235
+ expect(seed?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
236
236
  });
237
237
 
238
238
  test("returns undefined for an unknown name", () => {
@@ -242,10 +242,10 @@ describe("getSeed", () => {
242
242
  test("returned seed is a copy — mutations do not affect the table", () => {
243
243
  const seed = getSeed("dev");
244
244
  if (seed) {
245
- seed.platformUrl = "mutated";
245
+ seed.cloudUrl = "mutated";
246
246
  }
247
247
  const second = getSeed("dev");
248
- expect(second?.platformUrl).toBe("https://dev-cloud.mycadet.ai");
248
+ expect(second?.cloudUrl).toBe("https://dev-cloud.mycadet.ai");
249
249
  });
250
250
 
251
251
  test("all five canonical seeds exist", () => {
@@ -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
  /**
@@ -43,9 +43,9 @@ export function environmentFromVersion(version: string): string {
43
43
  *
44
44
  * Per-field env-var overrides are honored on the resolved definition as
45
45
  * ad-hoc escape hatches (they do not materialize new environments):
46
- * - `CADET_PLATFORM_URL` overrides `platformUrl`
46
+ * - `CADET_CLOUD_URL` overrides `cloudUrl`
47
47
  * - `CADET_WEB_URL` overrides `webUrl`
48
- * - `CADET_ASSISTANT_PLATFORM_URL` overrides `assistantPlatformUrl`
48
+ * - `CADET_ASSISTANT_PLATFORM_URL` overrides `assistantCloudUrl`
49
49
  * - `CADET_LOCKFILE_DIR` overrides `lockfileDirOverride` (legacy e2e
50
50
  * test hook)
51
51
  *
@@ -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`,
@@ -87,9 +85,9 @@ export function getCurrentEnvironment(
87
85
 
88
86
  const resolved: EnvironmentDefinition = { ...seed };
89
87
 
90
- const platformUrlOverride = process.env.CADET_PLATFORM_URL?.trim();
91
- if (platformUrlOverride) {
92
- resolved.platformUrl = platformUrlOverride;
88
+ const cloudUrlOverride = process.env.CADET_CLOUD_URL?.trim();
89
+ if (cloudUrlOverride) {
90
+ resolved.cloudUrl = cloudUrlOverride;
93
91
  }
94
92
 
95
93
  const webUrlOverride = process.env.CADET_WEB_URL?.trim();
@@ -97,10 +95,10 @@ export function getCurrentEnvironment(
97
95
  resolved.webUrl = webUrlOverride;
98
96
  }
99
97
 
100
- const assistantPlatformUrlOverride =
98
+ const assistantCloudUrlOverride =
101
99
  process.env.CADET_ASSISTANT_PLATFORM_URL?.trim();
102
- if (assistantPlatformUrlOverride) {
103
- resolved.assistantPlatformUrl = assistantPlatformUrlOverride;
100
+ if (assistantCloudUrlOverride) {
101
+ resolved.assistantCloudUrl = assistantCloudUrlOverride;
104
102
  }
105
103
 
106
104
  const lockfileDirOverride = process.env.CADET_LOCKFILE_DIR?.trim();
@@ -75,7 +75,7 @@ describe("parseFeatureFlagArgs", () => {
75
75
  "process.exit",
76
76
  );
77
77
  expect(errorSpy).toHaveBeenCalledWith(
78
- 'Error: invalid flag key "UPPER". Keys must be kebab-case (e.g. "browser")',
78
+ 'Error: invalid flag key "UPPER". Keys must be kebab-case, e.g. "browser"',
79
79
  );
80
80
 
81
81
  exitSpy.mockRestore();
@@ -38,7 +38,7 @@ export function parseFeatureFlagArgs(args: string[]): {
38
38
 
39
39
  if (!ALLOWED_KEY_RE.test(key)) {
40
40
  console.error(
41
- `Error: invalid flag key "${key}". Keys must be kebab-case (e.g. "browser")`,
41
+ `Error: invalid flag key "${key}". Keys must be kebab-case, e.g. "browser"`,
42
42
  );
43
43
  process.exit(1);
44
44
  }
@@ -49,7 +49,11 @@ const GuardianTokenFieldsSchema = z
49
49
  .union([z.string(), z.number()])
50
50
  .optional()
51
51
  .catch(undefined),
52
- refreshAfter: z.string().optional().catch(undefined),
52
+ refreshAfter: z
53
+ .union([z.string(), z.number()])
54
+ .transform((v) => (typeof v === "number" ? new Date(v).toISOString() : v))
55
+ .optional()
56
+ .catch(undefined),
53
57
  isNew: z.boolean().optional().catch(undefined),
54
58
  deviceId: z.string().optional().catch(undefined),
55
59
  })
@@ -425,7 +429,7 @@ export async function refreshGuardianTokenResult(
425
429
  console.warn(
426
430
  `Refusing to refresh the guardian token over an insecure URL (${gatewayUrl}). ` +
427
431
  "The refresh token is only sent over https or a loopback address: " +
428
- "use an https URL (e.g. a tunnel) or connect over loopback.",
432
+ "use an https URL, e.g. a tunnel, or connect over loopback.",
429
433
  );
430
434
  return {
431
435
  ok: false,
@@ -590,6 +594,20 @@ async function waitForGatewayReady(gatewayUrl: string): Promise<void> {
590
594
  }
591
595
  }
592
596
 
597
+ export class GuardianLeasePermanentError extends Error {
598
+ constructor(message: string) {
599
+ super(message);
600
+ this.name = "GuardianLeasePermanentError";
601
+ }
602
+ }
603
+
604
+ function describeJsonType(value: unknown): string {
605
+ if (value === undefined) return "absent";
606
+ if (value === null) return "null";
607
+ if (Array.isArray(value)) return "array";
608
+ return typeof value;
609
+ }
610
+
593
611
  export async function leaseGuardianToken(
594
612
  gatewayUrl: string,
595
613
  assistantId: string,
@@ -621,7 +639,8 @@ export async function leaseGuardianToken(
621
639
  throw new Error(`guardian/init failed (${response.status}): ${body}`);
622
640
  }
623
641
 
624
- const parsed = parseGuardianTokenFields(await response.json());
642
+ const rawBody: unknown = await response.json();
643
+ const parsed = parseGuardianTokenFields(rawBody);
625
644
  if (
626
645
  parsed.guardianPrincipalId == null ||
627
646
  parsed.accessToken == null ||
@@ -631,8 +650,21 @@ export async function leaseGuardianToken(
631
650
  parsed.refreshAfter == null ||
632
651
  parsed.isNew == null
633
652
  ) {
634
- throw new Error(
635
- `guardian/init returned a malformed token response (${response.status})`,
653
+ const raw = z.record(z.string(), z.unknown()).catch({}).parse(rawBody);
654
+ const requiredFields: (keyof typeof parsed)[] = [
655
+ "guardianPrincipalId",
656
+ "accessToken",
657
+ "accessTokenExpiresAt",
658
+ "refreshToken",
659
+ "refreshTokenExpiresAt",
660
+ "refreshAfter",
661
+ "isNew",
662
+ ];
663
+ const unusable = requiredFields
664
+ .filter((field) => parsed[field] == null)
665
+ .map((field) => `${field} arrived as ${describeJsonType(raw[field])}`);
666
+ throw new GuardianLeasePermanentError(
667
+ `guardian/init returned a malformed token response (${response.status}) — ${unusable.join(", ")}`,
636
668
  );
637
669
  }
638
670
  const tokenData: GuardianTokenData = {