@mycadet/cli 0.12.7 → 0.12.9

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 (179) 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 +3 -2
  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 +2 -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 +21 -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 +1 -8
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -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 +181 -197
  90. package/src/__tests__/tunnel.test.ts +257 -222
  91. package/src/__tests__/uninstall.test.ts +13 -4
  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 -76
  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 +23 -23
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/ps.ts +244 -86
  107. package/src/commands/restart.ts +30 -0
  108. package/src/commands/restore.ts +140 -22
  109. package/src/commands/retire.ts +48 -150
  110. package/src/commands/rollback.ts +20 -26
  111. package/src/commands/setup.ts +2 -2
  112. package/src/commands/sleep.ts +2 -2
  113. package/src/commands/teleport.ts +109 -109
  114. package/src/commands/terminal.ts +158 -18
  115. package/src/commands/tunnel.ts +138 -157
  116. package/src/commands/uninstall.ts +57 -9
  117. package/src/commands/unpair.ts +3 -3
  118. package/src/commands/upgrade.ts +30 -54
  119. package/src/commands/use.ts +1 -1
  120. package/src/commands/wake.ts +166 -50
  121. package/src/index.ts +18 -9
  122. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  123. package/src/lib/__tests__/docker.test.ts +168 -20
  124. package/src/lib/__tests__/job-polling.test.ts +2 -2
  125. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  126. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  127. package/src/lib/assistant-api-key-resolution.ts +5 -5
  128. package/src/lib/assistant-client.ts +8 -8
  129. package/src/lib/assistant-config.ts +58 -12
  130. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  131. package/src/lib/cloud-login.ts +24 -24
  132. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  133. package/src/lib/cloud-values.ts +3 -0
  134. package/src/lib/config-utils.ts +1 -1
  135. package/src/lib/detached-process.ts +1 -1
  136. package/src/lib/docker.ts +260 -53
  137. package/src/lib/ensure-binary.ts +171 -0
  138. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  139. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  140. package/src/lib/environments/resolve.ts +8 -8
  141. package/src/lib/flag-args.test.ts +1 -1
  142. package/src/lib/flag-args.ts +1 -1
  143. package/src/lib/guardian-token.ts +37 -5
  144. package/src/lib/hatch-local.ts +19 -23
  145. package/src/lib/health-check.ts +6 -6
  146. package/src/lib/ingress-config.ts +8 -35
  147. package/src/lib/job-polling.ts +2 -2
  148. package/src/lib/live-voice/connection.ts +12 -12
  149. package/src/lib/live-voice/tunnel.test.ts +11 -11
  150. package/src/lib/live-voice/tunnel.ts +15 -15
  151. package/src/lib/local-runtime-client.ts +20 -20
  152. package/src/lib/local.ts +10 -31
  153. package/src/lib/nginx-ingress.ts +42 -15
  154. package/src/lib/pair.ts +1 -1
  155. package/src/lib/process.ts +1 -1
  156. package/src/lib/provider-secrets.ts +3 -3
  157. package/src/lib/retire-apple-container.ts +1 -1
  158. package/src/lib/runtime-url.ts +8 -8
  159. package/src/lib/statefulset.ts +2 -2
  160. package/src/lib/sync-cloud-assistants.ts +19 -19
  161. package/src/lib/teleport-backup.ts +4 -4
  162. package/src/lib/terminal-client.ts +17 -17
  163. package/src/lib/terminal-session.ts +24 -24
  164. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  165. package/src/lib/tunnels/cadet.ts +29 -0
  166. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  167. package/src/lib/tunnels/gateway-notify.ts +61 -0
  168. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  169. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  170. package/src/lib/upgrade-lifecycle.ts +8 -21
  171. package/src/lib/upgrade-preflight.ts +2 -2
  172. package/src/lib/workos-pkce.ts +5 -5
  173. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  174. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  175. package/src/commands/recover.ts +0 -131
  176. package/src/commands/roadmap.ts +0 -459
  177. package/src/commands/ssh.ts +0 -144
  178. package/src/commands/workflows.ts +0 -305
  179. package/src/lib/managed-cloud.ts +0 -3
@@ -12,15 +12,15 @@ import { join } from "node:path";
12
12
  import { mock } from "bun:test";
13
13
 
14
14
  import {
15
- clearPlatformToken,
15
+ clearCloudToken,
16
16
  fetchAssistantDetail,
17
17
  fetchUpgradeInProgress,
18
- getPlatformUrl,
19
- readPlatformToken,
20
- savePlatformToken,
21
- } from "../lib/platform-client.js";
18
+ getCloudUrl,
19
+ readCloudToken,
20
+ saveCloudToken,
21
+ } from "../lib/cloud-client.js";
22
22
 
23
- describe("platform-client token path is env-scoped", () => {
23
+ describe("cloud-client token path is env-scoped", () => {
24
24
  let tempHome: string;
25
25
  let savedXdg: string | undefined;
26
26
  let savedEnv: string | undefined;
@@ -28,7 +28,7 @@ describe("platform-client token path is env-scoped", () => {
28
28
  beforeEach(() => {
29
29
  savedXdg = process.env.XDG_CONFIG_HOME;
30
30
  savedEnv = process.env.CADET_ENVIRONMENT;
31
- tempHome = mkdtempSync(join(tmpdir(), "cli-platform-client-test-"));
31
+ tempHome = mkdtempSync(join(tmpdir(), "cli-cloud-client-test-"));
32
32
  process.env.XDG_CONFIG_HOME = tempHome;
33
33
  delete process.env.CADET_ENVIRONMENT;
34
34
  });
@@ -47,93 +47,93 @@ describe("platform-client token path is env-scoped", () => {
47
47
  rmSync(tempHome, { recursive: true, force: true });
48
48
  });
49
49
 
50
- test("prod (CADET_ENVIRONMENT unset) writes to $XDG_CONFIG_HOME/cadet/platform-token", () => {
50
+ test("prod (CADET_ENVIRONMENT unset) writes to $XDG_CONFIG_HOME/cadet/cloud-token", () => {
51
51
  const token = "vak_prod_token_123";
52
- savePlatformToken(token);
52
+ saveCloudToken(token);
53
53
 
54
- const prodPath = join(tempHome, "cadet", "platform-token");
54
+ const prodPath = join(tempHome, "cadet", "cloud-token");
55
55
  expect(existsSync(prodPath)).toBe(true);
56
56
  expect(readFileSync(prodPath, "utf-8").trim()).toBe(token);
57
- expect(readPlatformToken()).toBe(token);
57
+ expect(readCloudToken()).toBe(token);
58
58
  });
59
59
 
60
- test("dev (CADET_ENVIRONMENT=dev) writes to $XDG_CONFIG_HOME/cadet-dev/platform-token", () => {
60
+ test("dev (CADET_ENVIRONMENT=dev) writes to $XDG_CONFIG_HOME/cadet-dev/cloud-token", () => {
61
61
  process.env.CADET_ENVIRONMENT = "dev";
62
62
  const token = "vak_dev_token_456";
63
- savePlatformToken(token);
63
+ saveCloudToken(token);
64
64
 
65
- const devPath = join(tempHome, "cadet-dev", "platform-token");
65
+ const devPath = join(tempHome, "cadet-dev", "cloud-token");
66
66
  expect(existsSync(devPath)).toBe(true);
67
67
  expect(readFileSync(devPath, "utf-8").trim()).toBe(token);
68
68
 
69
- const prodPath = join(tempHome, "cadet", "platform-token");
69
+ const prodPath = join(tempHome, "cadet", "cloud-token");
70
70
  expect(existsSync(prodPath)).toBe(false);
71
71
 
72
- expect(readPlatformToken()).toBe(token);
72
+ expect(readCloudToken()).toBe(token);
73
73
  });
74
74
 
75
75
  test("prod and dev tokens are isolated on disk", () => {
76
76
  // Save prod token
77
77
  delete process.env.CADET_ENVIRONMENT;
78
- savePlatformToken("prod-token");
78
+ saveCloudToken("prod-token");
79
79
 
80
80
  // Switch to dev and save a different token
81
81
  process.env.CADET_ENVIRONMENT = "dev";
82
- savePlatformToken("dev-token");
82
+ saveCloudToken("dev-token");
83
83
 
84
84
  // Dev read returns dev
85
- expect(readPlatformToken()).toBe("dev-token");
85
+ expect(readCloudToken()).toBe("dev-token");
86
86
 
87
87
  // Switch back to prod — prod value is unchanged
88
88
  delete process.env.CADET_ENVIRONMENT;
89
- expect(readPlatformToken()).toBe("prod-token");
89
+ expect(readCloudToken()).toBe("prod-token");
90
90
 
91
91
  // Files live at distinct paths
92
92
  expect(
93
- readFileSync(join(tempHome, "cadet", "platform-token"), "utf-8").trim(),
93
+ readFileSync(join(tempHome, "cadet", "cloud-token"), "utf-8").trim(),
94
94
  ).toBe("prod-token");
95
95
  expect(
96
96
  readFileSync(
97
- join(tempHome, "cadet-dev", "platform-token"),
97
+ join(tempHome, "cadet-dev", "cloud-token"),
98
98
  "utf-8",
99
99
  ).trim(),
100
100
  ).toBe("dev-token");
101
101
  });
102
102
 
103
- test("clearPlatformToken removes only the env-scoped token", () => {
103
+ test("clearCloudToken removes only the env-scoped token", () => {
104
104
  // Prod token
105
105
  delete process.env.CADET_ENVIRONMENT;
106
- savePlatformToken("prod-token");
106
+ saveCloudToken("prod-token");
107
107
 
108
108
  // Dev token
109
109
  process.env.CADET_ENVIRONMENT = "dev";
110
- savePlatformToken("dev-token");
110
+ saveCloudToken("dev-token");
111
111
 
112
112
  // Clear dev
113
- clearPlatformToken();
114
- expect(existsSync(join(tempHome, "cadet-dev", "platform-token"))).toBe(
113
+ clearCloudToken();
114
+ expect(existsSync(join(tempHome, "cadet-dev", "cloud-token"))).toBe(
115
115
  false,
116
116
  );
117
117
 
118
118
  // Prod still there
119
- expect(existsSync(join(tempHome, "cadet", "platform-token"))).toBe(true);
119
+ expect(existsSync(join(tempHome, "cadet", "cloud-token"))).toBe(true);
120
120
  });
121
121
  });
122
122
 
123
- describe("getPlatformUrl resolution order", () => {
123
+ describe("getCloudUrl resolution order", () => {
124
124
  let tempLockDir: string;
125
125
  let savedLockDir: string | undefined;
126
126
  let savedEnv: string | undefined;
127
- let savedPlatformUrl: string | undefined;
127
+ let savedCloudUrl: string | undefined;
128
128
 
129
129
  beforeEach(() => {
130
130
  savedLockDir = process.env.CADET_LOCKFILE_DIR;
131
131
  savedEnv = process.env.CADET_ENVIRONMENT;
132
- savedPlatformUrl = process.env.CADET_PLATFORM_URL;
133
- tempLockDir = mkdtempSync(join(tmpdir(), "cli-platform-url-test-"));
132
+ savedCloudUrl = process.env.CADET_CLOUD_URL;
133
+ tempLockDir = mkdtempSync(join(tmpdir(), "cli-cloud-url-test-"));
134
134
  process.env.CADET_LOCKFILE_DIR = tempLockDir;
135
135
  delete process.env.CADET_ENVIRONMENT;
136
- delete process.env.CADET_PLATFORM_URL;
136
+ delete process.env.CADET_CLOUD_URL;
137
137
  });
138
138
 
139
139
  afterEach(() => {
@@ -147,10 +147,10 @@ describe("getPlatformUrl resolution order", () => {
147
147
  } else {
148
148
  process.env.CADET_ENVIRONMENT = savedEnv;
149
149
  }
150
- if (savedPlatformUrl === undefined) {
151
- delete process.env.CADET_PLATFORM_URL;
150
+ if (savedCloudUrl === undefined) {
151
+ delete process.env.CADET_CLOUD_URL;
152
152
  } else {
153
- process.env.CADET_PLATFORM_URL = savedPlatformUrl;
153
+ process.env.CADET_CLOUD_URL = savedCloudUrl;
154
154
  }
155
155
  rmSync(tempLockDir, { recursive: true, force: true });
156
156
  });
@@ -163,47 +163,47 @@ describe("getPlatformUrl resolution order", () => {
163
163
  );
164
164
  }
165
165
 
166
- test("returns lockfile platformBaseUrl when set", () => {
167
- writeLockfile({ platformBaseUrl: "https://staging.mycadet.ai" });
168
- expect(getPlatformUrl()).toBe("https://staging.mycadet.ai");
166
+ test("returns lockfile cloudBaseUrl when set", () => {
167
+ writeLockfile({ cloudBaseUrl: "https://staging.mycadet.ai" });
168
+ expect(getCloudUrl()).toBe("https://staging.mycadet.ai");
169
169
  });
170
170
 
171
- test("lockfile platformBaseUrl takes priority over CADET_PLATFORM_URL", () => {
172
- writeLockfile({ platformBaseUrl: "https://lockfile.mycadet.ai" });
173
- process.env.CADET_PLATFORM_URL = "https://env.mycadet.ai";
174
- expect(getPlatformUrl()).toBe("https://lockfile.mycadet.ai");
171
+ test("lockfile cloudBaseUrl takes priority over CADET_CLOUD_URL", () => {
172
+ writeLockfile({ cloudBaseUrl: "https://lockfile.mycadet.ai" });
173
+ process.env.CADET_CLOUD_URL = "https://env.mycadet.ai";
174
+ expect(getCloudUrl()).toBe("https://lockfile.mycadet.ai");
175
175
  });
176
176
 
177
- test("falls back to CADET_PLATFORM_URL when lockfile is missing", () => {
178
- process.env.CADET_PLATFORM_URL = "https://env-only.mycadet.ai";
179
- expect(getPlatformUrl()).toBe("https://env-only.mycadet.ai");
177
+ test("falls back to CADET_CLOUD_URL when lockfile is missing", () => {
178
+ process.env.CADET_CLOUD_URL = "https://env-only.mycadet.ai";
179
+ expect(getCloudUrl()).toBe("https://env-only.mycadet.ai");
180
180
  });
181
181
 
182
- test("falls back to CADET_PLATFORM_URL when lockfile has no platformBaseUrl", () => {
182
+ test("falls back to CADET_CLOUD_URL when lockfile has no cloudBaseUrl", () => {
183
183
  writeLockfile({ assistants: [] });
184
- process.env.CADET_PLATFORM_URL = "https://env-fallback.mycadet.ai";
185
- expect(getPlatformUrl()).toBe("https://env-fallback.mycadet.ai");
184
+ process.env.CADET_CLOUD_URL = "https://env-fallback.mycadet.ai";
185
+ expect(getCloudUrl()).toBe("https://env-fallback.mycadet.ai");
186
186
  });
187
187
 
188
- test("falls back to CADET_PLATFORM_URL when lockfile platformBaseUrl is blank", () => {
189
- writeLockfile({ platformBaseUrl: " " });
190
- process.env.CADET_PLATFORM_URL = "https://env-after-blank.mycadet.ai";
191
- expect(getPlatformUrl()).toBe("https://env-after-blank.mycadet.ai");
188
+ test("falls back to CADET_CLOUD_URL when lockfile cloudBaseUrl is blank", () => {
189
+ writeLockfile({ cloudBaseUrl: " " });
190
+ process.env.CADET_CLOUD_URL = "https://env-after-blank.mycadet.ai";
191
+ expect(getCloudUrl()).toBe("https://env-after-blank.mycadet.ai");
192
192
  });
193
193
 
194
- test("falls back to prod env seed URL when lockfile and CADET_PLATFORM_URL are unset (prod env)", () => {
194
+ test("falls back to prod env seed URL when lockfile and CADET_CLOUD_URL are unset (prod env)", () => {
195
195
  // CADET_ENVIRONMENT is unset → production → prod seed URL.
196
- expect(getPlatformUrl()).toBe("https://cloud.mycadet.ai");
196
+ expect(getCloudUrl()).toBe("https://cloud.mycadet.ai");
197
197
  });
198
198
 
199
199
  test("falls back to dev env seed URL when CADET_ENVIRONMENT=dev", () => {
200
200
  process.env.CADET_ENVIRONMENT = "dev";
201
- expect(getPlatformUrl()).toBe("https://dev-cloud.mycadet.ai");
201
+ expect(getCloudUrl()).toBe("https://dev-cloud.mycadet.ai");
202
202
  });
203
203
 
204
- test("trims whitespace from CADET_PLATFORM_URL", () => {
205
- process.env.CADET_PLATFORM_URL = " https://trimmed.mycadet.ai ";
206
- expect(getPlatformUrl()).toBe("https://trimmed.mycadet.ai");
204
+ test("trims whitespace from CADET_CLOUD_URL", () => {
205
+ process.env.CADET_CLOUD_URL = " https://trimmed.mycadet.ai ";
206
+ expect(getCloudUrl()).toBe("https://trimmed.mycadet.ai");
207
207
  });
208
208
  });
209
209
 
@@ -5,9 +5,13 @@ import {
5
5
  fetchReleases,
6
6
  resolveImageRefs,
7
7
  resolveOwnImageRefs,
8
- } from "../lib/platform-releases.js";
8
+ } from "../lib/cloud-releases.js";
9
9
  import { dockerhubImages } from "../lib/docker.js";
10
- import { isDevVersion, isLocalVersion, isStagingVersion } from "../lib/version-compat.js";
10
+ import {
11
+ isDevVersion,
12
+ isLocalVersion,
13
+ isStagingVersion,
14
+ } from "../lib/version-compat.js";
11
15
 
12
16
  const originalFetch = globalThis.fetch;
13
17
 
@@ -27,12 +31,12 @@ function mockFetchError() {
27
31
  }
28
32
 
29
33
  beforeEach(() => {
30
- process.env.CADET_PLATFORM_URL = "https://platform.test";
34
+ process.env.CADET_CLOUD_URL = "https://platform.test";
31
35
  });
32
36
 
33
37
  afterEach(() => {
34
38
  globalThis.fetch = originalFetch;
35
- delete process.env.CADET_PLATFORM_URL;
39
+ delete process.env.CADET_CLOUD_URL;
36
40
  });
37
41
 
38
42
  const RELEASE = {
@@ -60,9 +64,9 @@ describe("fetchReleases", () => {
60
64
  expect(url).toContain("limit=100");
61
65
  });
62
66
 
63
- test("uses the platformUrl override over the resolved default", async () => {
67
+ test("uses the cloudUrl override over the resolved default", async () => {
64
68
  const fetchMock = mockFetchJson([RELEASE]);
65
- await fetchReleases({ platformUrl: "https://other-platform.test" });
69
+ await fetchReleases({ cloudUrl: "https://other-platform.test" });
66
70
  const url = String(fetchMock.mock.calls[0][0]);
67
71
  expect(url).toContain("https://other-platform.test/v1/releases/");
68
72
  });
@@ -79,12 +83,12 @@ describe("fetchReleases", () => {
79
83
  });
80
84
 
81
85
  describe("resolveImageRefs", () => {
82
- test("released CLI resolves un-suffixed repos tagged with the target version", () => {
86
+ test("released CLI resolves un-suffixed repos tagged with the bare version", () => {
83
87
  const tags = resolveImageRefs("v0.12.6", "0.12.6");
84
- expect(tags.assistant).toBe("mycadet/mycadet-assistant:v0.12.6");
85
- expect(tags.gateway).toBe("mycadet/mycadet-gateway:v0.12.6");
88
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant:0.12.6");
89
+ expect(tags.gateway).toBe("mycadet/mycadet-gateway:0.12.6");
86
90
  expect(tags["credential-executor"]).toBe(
87
- "mycadet/mycadet-credential-executor:v0.12.6",
91
+ "mycadet/mycadet-credential-executor:0.12.6",
88
92
  );
89
93
  });
90
94
 
@@ -94,31 +98,32 @@ describe("resolveImageRefs", () => {
94
98
  "0.12.6-dev.202610081023.3a27b77",
95
99
  );
96
100
  expect(tags.assistant).toBe(
97
- "mycadet/mycadet-assistant-dev:v0.12.6-dev.202610081023.3a27b77",
101
+ "mycadet/mycadet-assistant-dev:0.12.6-dev.202610081023.3a27b77",
98
102
  );
99
103
  expect(tags.gateway).toBe(
100
- "mycadet/mycadet-gateway-dev:v0.12.6-dev.202610081023.3a27b77",
104
+ "mycadet/mycadet-gateway-dev:0.12.6-dev.202610081023.3a27b77",
101
105
  );
102
106
  expect(tags["credential-executor"]).toBe(
103
- "mycadet/mycadet-credential-executor-dev:v0.12.6-dev.202610081023.3a27b77",
107
+ "mycadet/mycadet-credential-executor-dev:0.12.6-dev.202610081023.3a27b77",
104
108
  );
105
109
  });
106
110
 
107
111
  test("repo suffix follows the CLI version, tag follows the target", () => {
108
112
  const tags = resolveImageRefs("v0.12.5", "0.12.6-dev.202610081023.3a27b77");
109
- expect(tags.assistant).toBe("mycadet/mycadet-assistant-dev:v0.12.5");
113
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant-dev:0.12.5");
110
114
  });
111
115
 
112
116
  test("does not consult the network", async () => {
113
117
  mockFetchError();
114
118
  const tags = resolveImageRefs("v0.12.6", "0.12.6");
115
- expect(tags.assistant).toContain(":v0.12.6");
119
+ expect(tags.assistant).toContain(":0.12.6");
116
120
  });
117
121
 
118
- test("normalizes the v-prefix on the tag", () => {
122
+ test("normalizes the v-prefix off the tag", () => {
119
123
  const withoutPrefix = resolveImageRefs("0.12.6", "0.12.6");
120
124
  const withPrefix = resolveImageRefs("v0.12.6", "0.12.6");
121
- expect(withoutPrefix.assistant).toBe(withPrefix.assistant);
125
+ expect(withoutPrefix.assistant).toBe("mycadet/mycadet-assistant:0.12.6");
126
+ expect(withPrefix.assistant).toBe(withoutPrefix.assistant);
122
127
  });
123
128
  });
124
129
 
@@ -126,9 +131,14 @@ describe("resolveOwnImageRefs", () => {
126
131
  test("uses the CLI's own version for both tag and repo suffix", () => {
127
132
  const tags = resolveOwnImageRefs("0.12.6-dev.202610081023.3a27b77");
128
133
  expect(tags.assistant).toBe(
129
- "mycadet/mycadet-assistant-dev:v0.12.6-dev.202610081023.3a27b77",
134
+ "mycadet/mycadet-assistant-dev:0.12.6-dev.202610081023.3a27b77",
130
135
  );
131
136
  });
137
+
138
+ test("falls back to the literal latest tag when the CLI version is unknown", () => {
139
+ const tags = resolveOwnImageRefs("");
140
+ expect(tags.assistant).toBe("mycadet/mycadet-assistant:latest");
141
+ });
132
142
  });
133
143
 
134
144
  describe("dockerhubImages", () => {
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import { EventEmitter } from "node:events";
3
3
  import type { ChildProcess } from "node:child_process";
4
4
 
5
- import { waitForCloudflareTunnelUrl } from "../lib/cloudflare-tunnel.js";
5
+ import { waitForCloudflareTunnelUrl } from "../lib/tunnels/cloudflare.js";
6
6
 
7
7
  // ── Helpers ───────────────────────────────────────────────────────────────────
8
8
 
@@ -27,7 +27,9 @@ import {
27
27
  findAssistantByName,
28
28
  saveAssistantEntry,
29
29
  } from "../lib/assistant-config.js";
30
+ import { z } from "zod";
30
31
  import { loadGuardianToken } from "../lib/guardian-token.js";
32
+ import { withPreconnect } from "./helpers/http.js";
31
33
 
32
34
  const HOST = "https://host.example.ts.net";
33
35
  const PAIRING_LINK = `${HOST}/assistant/pair#device_code=dev-code`;
@@ -69,6 +71,9 @@ function approvedBody(overrides: Record<string, unknown> = {}) {
69
71
  };
70
72
  }
71
73
 
74
+ /** The commands always send flat string-keyed JSON bodies. */
75
+ const JsonRecord = z.record(z.string(), z.unknown());
76
+
72
77
  interface FetchCall {
73
78
  url: string;
74
79
  body: Record<string, unknown>;
@@ -79,17 +84,20 @@ function stubFetch(
79
84
  replies: (call: FetchCall, index: number) => Response,
80
85
  ): FetchCall[] {
81
86
  const calls: FetchCall[] = [];
82
- globalThis.fetch = (async (url: string, init?: RequestInit) => {
83
- const call = {
84
- url,
85
- body: JSON.parse((init?.body as string) ?? "{}") as Record<
86
- string,
87
- unknown
88
- >,
89
- };
90
- calls.push(call);
91
- return replies(call, calls.length - 1);
92
- }) as unknown as typeof fetch;
87
+ globalThis.fetch = withPreconnect(
88
+ async (input: RequestInfo | URL, init?: RequestInit) => {
89
+ const call = {
90
+ url: String(input),
91
+ // The command always sends a JSON string body; anything else records
92
+ // as an empty record.
93
+ body: JsonRecord.parse(
94
+ JSON.parse(typeof init?.body === "string" ? init.body : "{}"),
95
+ ),
96
+ };
97
+ calls.push(call);
98
+ return replies(call, calls.length - 1);
99
+ },
100
+ );
93
101
  return calls;
94
102
  }
95
103
 
@@ -112,14 +120,16 @@ async function runExpectingExit(): Promise<{ exited: boolean; out: string }> {
112
120
  const logSpy = spyOn(console, "log").mockImplementation((...a: unknown[]) => {
113
121
  messages.push(a.join(" "));
114
122
  });
115
- const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
116
- throw new Error(`exit:${c}`);
117
- }) as never);
123
+ const exitSpy = spyOn(process, "exit").mockImplementation(
124
+ (c?: number | string | undefined): never => {
125
+ throw new Error(`exit:${c}`);
126
+ },
127
+ );
118
128
  let exited = false;
119
129
  try {
120
130
  await connectImport();
121
131
  } catch (e) {
122
- exited = (e as Error).message === "exit:1";
132
+ exited = e instanceof Error && e.message === "exit:1";
123
133
  } finally {
124
134
  errSpy.mockRestore();
125
135
  logSpy.mockRestore();
@@ -144,10 +154,13 @@ describe("connect import", () => {
144
154
  // a multi-second cadence is exercised in full without being spent, and
145
155
  // both the cadence and the backoff are assertable rather than implicit.
146
156
  setSystemTime(new Date());
147
- sleepSpy = spyOn(Bun, "sleep").mockImplementation((async (ms: number) => {
148
- waits.push(ms);
149
- setSystemTime(new Date(Date.now() + ms));
150
- }) as never);
157
+ sleepSpy = spyOn(Bun, "sleep").mockImplementation(
158
+ async (ms: number | Date) => {
159
+ const waitMs = typeof ms === "number" ? ms : ms.getTime() - Date.now();
160
+ waits.push(waitMs);
161
+ setSystemTime(new Date(Date.now() + waitMs));
162
+ },
163
+ );
151
164
  });
152
165
 
153
166
  afterEach(() => {
@@ -312,10 +325,10 @@ describe("connect import", () => {
312
325
  "http://insecure.example.com",
313
326
  ];
314
327
  let fetched = false;
315
- globalThis.fetch = (async () => {
328
+ globalThis.fetch = withPreconnect(async () => {
316
329
  fetched = true;
317
330
  return jsonResponse({});
318
- }) as unknown as typeof fetch;
331
+ });
319
332
 
320
333
  const { exited, out } = await runExpectingExit();
321
334
 
@@ -333,10 +346,10 @@ describe("connect import", () => {
333
346
  "https://127.0.0.1:7830",
334
347
  ];
335
348
  let fetched = false;
336
- globalThis.fetch = (async () => {
349
+ globalThis.fetch = withPreconnect(async () => {
337
350
  fetched = true;
338
351
  return jsonResponse({});
339
- }) as unknown as typeof fetch;
352
+ });
340
353
 
341
354
  const { exited, out } = await runExpectingExit();
342
355
 
@@ -361,10 +374,10 @@ describe("connect import", () => {
361
374
  "legacy-box",
362
375
  ];
363
376
  let fetched = false;
364
- globalThis.fetch = (async () => {
377
+ globalThis.fetch = withPreconnect(async () => {
365
378
  fetched = true;
366
379
  return jsonResponse({});
367
- }) as unknown as typeof fetch;
380
+ });
368
381
  const { logs, restore } = captureLogs();
369
382
  try {
370
383
  await connectImport();
@@ -385,10 +398,10 @@ describe("connect import", () => {
385
398
  test("a string that is neither an address nor a bundle keeps the address error", async () => {
386
399
  process.argv = ["bun", "cadet", "connect", "import", "not-a-url"];
387
400
  let fetched = false;
388
- globalThis.fetch = (async () => {
401
+ globalThis.fetch = withPreconnect(async () => {
389
402
  fetched = true;
390
403
  return jsonResponse({});
391
- }) as unknown as typeof fetch;
404
+ });
392
405
 
393
406
  const { exited, out } = await runExpectingExit();
394
407
 
@@ -410,7 +423,8 @@ describe("connect import", () => {
410
423
  "flaky-box",
411
424
  ];
412
425
  let attempt = 0;
413
- globalThis.fetch = (async (url: string) => {
426
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
427
+ const url = String(input);
414
428
  if (url === CHALLENGE_URL) {
415
429
  return jsonResponse(challengeBody());
416
430
  }
@@ -420,7 +434,7 @@ describe("connect import", () => {
420
434
  throw new Error("connect ECONNRESET");
421
435
  }
422
436
  return jsonResponse(approvedBody());
423
- }) as unknown as typeof fetch;
437
+ });
424
438
  const { logs, restore } = captureLogs();
425
439
  try {
426
440
  await connectImport();
@@ -448,7 +462,8 @@ describe("connect import", () => {
448
462
  "repairing-box",
449
463
  ];
450
464
  let attempt = 0;
451
- globalThis.fetch = (async (url: string) => {
465
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
466
+ const url = String(input);
452
467
  if (url === CHALLENGE_URL) {
453
468
  return jsonResponse(challengeBody());
454
469
  }
@@ -459,7 +474,7 @@ describe("connect import", () => {
459
474
  return jsonResponse({ error: "guardian repair required" }, 503);
460
475
  }
461
476
  return jsonResponse(approvedBody());
462
- }) as unknown as typeof fetch;
477
+ });
463
478
  const { logs, restore } = captureLogs();
464
479
  try {
465
480
  await connectImport();
@@ -477,7 +492,8 @@ describe("connect import", () => {
477
492
  test("reports expiry once transient failures outlast the code", async () => {
478
493
  process.argv = ["bun", "cadet", "connect", "import", HOST];
479
494
  let exchanges = 0;
480
- globalThis.fetch = (async (url: string) => {
495
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
496
+ const url = String(input);
481
497
  if (url === CHALLENGE_URL) {
482
498
  return jsonResponse(
483
499
  challengeBody({
@@ -488,7 +504,7 @@ describe("connect import", () => {
488
504
  }
489
505
  exchanges += 1;
490
506
  throw new Error("connect ECONNREFUSED");
491
- }) as unknown as typeof fetch;
507
+ });
492
508
 
493
509
  const { exited, out } = await runExpectingExit();
494
510
 
@@ -507,10 +523,10 @@ describe("connect import", () => {
507
523
  // ten-minute TTL.
508
524
  process.argv = ["bun", "cadet", "connect", "import", PAIRING_LINK];
509
525
  let attempts = 0;
510
- globalThis.fetch = (async () => {
526
+ globalThis.fetch = withPreconnect(async () => {
511
527
  attempts += 1;
512
528
  throw new Error("connect ECONNREFUSED");
513
- }) as unknown as typeof fetch;
529
+ });
514
530
 
515
531
  const { exited, out } = await runExpectingExit();
516
532
 
@@ -532,10 +548,10 @@ describe("connect import", () => {
532
548
  test("rejects an unknown --flag instead of pairing against it", async () => {
533
549
  process.argv = ["bun", "cadet", "connect", "import", "--qr", HOST];
534
550
  let fetched = false;
535
- globalThis.fetch = (async () => {
551
+ globalThis.fetch = withPreconnect(async () => {
536
552
  fetched = true;
537
553
  return jsonResponse({});
538
- }) as unknown as typeof fetch;
554
+ });
539
555
 
540
556
  const { exited, out } = await runExpectingExit();
541
557
 
@@ -560,10 +576,10 @@ describe("connect import", () => {
560
576
  "--frobnicate",
561
577
  ];
562
578
  let fetched = false;
563
- globalThis.fetch = (async () => {
579
+ globalThis.fetch = withPreconnect(async () => {
564
580
  fetched = true;
565
581
  return jsonResponse({});
566
- }) as unknown as typeof fetch;
582
+ });
567
583
 
568
584
  const { exited, out } = await runExpectingExit();
569
585