@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
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import cliPkg from "../package.json";
14
14
  import { backup } from "./commands/backup";
15
15
  import { clean } from "./commands/clean";
16
16
  import { client } from "./commands/client";
17
+ import { completeCadet, completions } from "./commands/completions";
17
18
  import { confirm } from "./commands/confirm";
18
19
  import { connect } from "./commands/connect";
19
20
  import { devices } from "./commands/devices";
@@ -27,14 +28,12 @@ import { logs } from "./commands/logs";
27
28
  import { message } from "./commands/message";
28
29
  import { pair } from "./commands/pair";
29
30
  import { ps } from "./commands/ps";
30
- import { recover } from "./commands/recover";
31
31
  import { restore } from "./commands/restore";
32
- import { roadmap } from "./commands/roadmap";
33
32
  import { retire } from "./commands/retire";
33
+ import { restart } from "./commands/restart";
34
34
  import { rollback } from "./commands/rollback";
35
35
  import { setup } from "./commands/setup";
36
36
  import { sleep } from "./commands/sleep";
37
- import { ssh } from "./commands/ssh";
38
37
  import { debug } from "./commands/debug";
39
38
  import { teleport } from "./commands/teleport";
40
39
  import { terminal } from "./commands/terminal";
@@ -45,7 +44,6 @@ import { upgrade } from "./commands/upgrade";
45
44
  import { use } from "./commands/use";
46
45
  import { voice } from "./commands/voice";
47
46
  import { wake } from "./commands/wake";
48
- import { workflows } from "./commands/workflows";
49
47
  import { red } from "./lib/color";
50
48
  import { resolveAssistant, setActiveAssistant } from "./lib/assistant-config";
51
49
  import { loadGuardianToken } from "./lib/guardian-token";
@@ -56,6 +54,7 @@ const commands = {
56
54
  backup,
57
55
  clean,
58
56
  client,
57
+ completions,
59
58
  confirm,
60
59
  connect,
61
60
  debug,
@@ -66,19 +65,17 @@ const commands = {
66
65
  gateway,
67
66
  hatch,
68
67
  login,
69
- logout,
70
68
  logs,
69
+ logout,
71
70
  message,
72
71
  pair,
73
72
  ps,
74
- recover,
75
73
  restore,
76
74
  retire,
77
- roadmap,
75
+ restart,
78
76
  rollback,
79
77
  setup,
80
78
  sleep,
81
- ssh,
82
79
  teleport,
83
80
  terminal,
84
81
  tunnel,
@@ -89,7 +86,6 @@ const commands = {
89
86
  voice,
90
87
  wake,
91
88
  whoami,
92
- workflows,
93
89
  };
94
90
 
95
91
  type CommandName = keyof typeof commands;
@@ -237,6 +233,19 @@ async function main() {
237
233
  process.exit(0);
238
234
  }
239
235
 
236
+ // Hidden completion hook: the bash completion script calls
237
+ // `cadet __complete <words-up-to-cursor>` and filters the printed
238
+ // candidates against the word under the cursor. Deliberately unlisted in
239
+ // the command map so it never shows up in help.
240
+ if (commandName === "__complete") {
241
+ const candidates = completeCadet(
242
+ process.argv.slice(3),
243
+ Object.keys(commands),
244
+ );
245
+ console.log(candidates.join("\n"));
246
+ process.exit(0);
247
+ }
248
+
240
249
  const command = commands[commandName as CommandName];
241
250
 
242
251
  if (!command) {
@@ -2,10 +2,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  platformPollJobStatus,
5
- platformRequestSignedUrl,
5
+ cloudRequestSignedUrl,
6
6
  VersionMismatchError,
7
7
  type UnifiedJobStatus,
8
- } from "../platform-client.js";
8
+ } from "../cloud-client.js";
9
9
 
10
10
  const PLATFORM_URL = "https://platform.example.test";
11
11
  const VAK_TOKEN = "vak_test_1234567890"; // API-key path skips org-ID fetch.
@@ -69,7 +69,7 @@ afterEach(() => {
69
69
  globalThis.fetch = originalFetch;
70
70
  });
71
71
 
72
- describe("platformRequestSignedUrl", () => {
72
+ describe("cloudRequestSignedUrl", () => {
73
73
  test("upload operation with just operation → posts correct body and parses response", async () => {
74
74
  const { calls, fetchMock } = captureFetch(() => {
75
75
  return new Response(
@@ -83,7 +83,7 @@ describe("platformRequestSignedUrl", () => {
83
83
  });
84
84
  globalThis.fetch = fetchMock;
85
85
 
86
- const result = await platformRequestSignedUrl(
86
+ const result = await cloudRequestSignedUrl(
87
87
  { operation: "upload" },
88
88
  VAK_TOKEN,
89
89
  PLATFORM_URL,
@@ -117,7 +117,7 @@ describe("platformRequestSignedUrl", () => {
117
117
  });
118
118
  globalThis.fetch = fetchMock;
119
119
 
120
- const result = await platformRequestSignedUrl(
120
+ const result = await cloudRequestSignedUrl(
121
121
  {
122
122
  operation: "upload",
123
123
  contentType: "application/octet-stream",
@@ -148,7 +148,7 @@ describe("platformRequestSignedUrl", () => {
148
148
  });
149
149
  globalThis.fetch = fetchMock;
150
150
 
151
- const result = await platformRequestSignedUrl(
151
+ const result = await cloudRequestSignedUrl(
152
152
  { operation: "download", bundleKey: "bundles/xyz.tar.gz" },
153
153
  VAK_TOKEN,
154
154
  PLATFORM_URL,
@@ -182,7 +182,7 @@ describe("platformRequestSignedUrl", () => {
182
182
  });
183
183
  globalThis.fetch = fetchMock;
184
184
 
185
- const result = await platformRequestSignedUrl(
185
+ const result = await cloudRequestSignedUrl(
186
186
  { operation: "upload" },
187
187
  VAK_TOKEN,
188
188
  PLATFORM_URL,
@@ -263,7 +263,7 @@ describe("platformRequestSignedUrl", () => {
263
263
  );
264
264
  globalThis.fetch = fetchMock as unknown as typeof globalThis.fetch;
265
265
 
266
- const result = await platformRequestSignedUrl(
266
+ const result = await cloudRequestSignedUrl(
267
267
  { operation: "upload" },
268
268
  SESSION_TOKEN,
269
269
  PLATFORM_URL,
@@ -305,7 +305,7 @@ describe("platformRequestSignedUrl", () => {
305
305
  });
306
306
  globalThis.fetch = fetchMock;
307
307
 
308
- await platformRequestSignedUrl(
308
+ await cloudRequestSignedUrl(
309
309
  {
310
310
  operation: "upload",
311
311
  minRuntimeVersion: "1.2.3",
@@ -335,7 +335,7 @@ describe("platformRequestSignedUrl", () => {
335
335
  });
336
336
  globalThis.fetch = fetchMock;
337
337
 
338
- await platformRequestSignedUrl(
338
+ await cloudRequestSignedUrl(
339
339
  {
340
340
  operation: "download",
341
341
  bundleKey: "bundles/dl-v.tar.gz",
@@ -370,7 +370,7 @@ describe("platformRequestSignedUrl", () => {
370
370
 
371
371
  let caught: unknown;
372
372
  try {
373
- await platformRequestSignedUrl(
373
+ await cloudRequestSignedUrl(
374
374
  {
375
375
  operation: "download",
376
376
  bundleKey: "bundles/foo.tar.gz",
@@ -414,7 +414,7 @@ describe("platformRequestSignedUrl", () => {
414
414
 
415
415
  let caught: unknown;
416
416
  try {
417
- await platformRequestSignedUrl(
417
+ await cloudRequestSignedUrl(
418
418
  {
419
419
  operation: "download",
420
420
  bundleKey: "bundles/foo.tar.gz",
@@ -444,7 +444,7 @@ describe("platformRequestSignedUrl", () => {
444
444
 
445
445
  let caught: unknown;
446
446
  try {
447
- await platformRequestSignedUrl(
447
+ await cloudRequestSignedUrl(
448
448
  { operation: "download", bundleKey: "bundles/foo.tar.gz" },
449
449
  VAK_TOKEN,
450
450
  PLATFORM_URL,
@@ -483,7 +483,7 @@ describe("platformRequestSignedUrl", () => {
483
483
 
484
484
  let caught: unknown;
485
485
  try {
486
- await platformRequestSignedUrl(
486
+ await cloudRequestSignedUrl(
487
487
  {
488
488
  operation: "download",
489
489
  bundleKey: "bundles/foo.tar.gz",
@@ -513,7 +513,7 @@ describe("platformRequestSignedUrl", () => {
513
513
  });
514
514
  globalThis.fetch = fetchMock;
515
515
 
516
- const result = await platformRequestSignedUrl(
516
+ const result = await cloudRequestSignedUrl(
517
517
  { operation: "download", bundleKey: "bundles/xyz.tar.gz" },
518
518
  VAK_TOKEN,
519
519
  PLATFORM_URL,
@@ -535,7 +535,7 @@ describe("platformRequestSignedUrl", () => {
535
535
  globalThis.fetch = fetchMock;
536
536
 
537
537
  await expect(
538
- platformRequestSignedUrl(
538
+ cloudRequestSignedUrl(
539
539
  { operation: "upload" },
540
540
  VAK_TOKEN,
541
541
  PLATFORM_URL,
@@ -8,6 +8,7 @@ import {
8
8
  collectWatchTargets,
9
9
  dockerResourceNames,
10
10
  ensureDockerDaemonRunning,
11
+ pullImage,
11
12
  resolveAvatarDevicePath,
12
13
  resolveDockerHatchMode,
13
14
  resolveDockerProviderCredentialSetupAction,
@@ -158,6 +159,38 @@ describe("resolveDockerProviderCredentialSetupAction", () => {
158
159
  }),
159
160
  ).toBe("skip");
160
161
  });
162
+
163
+ test("detached hatch with a pre-collected key configures instead of deferring", () => {
164
+ expect(
165
+ resolveDockerProviderCredentialSetupAction({
166
+ provider: "fireworks",
167
+ guardianAccessToken: "guardian-token",
168
+ detached: true,
169
+ precollectedKey: "fw-key-123",
170
+ }),
171
+ ).toBe("configure");
172
+ });
173
+
174
+ test("pre-collected key without a guardian token reports missing-token (no silent defer)", () => {
175
+ expect(
176
+ resolveDockerProviderCredentialSetupAction({
177
+ provider: "fireworks",
178
+ detached: true,
179
+ precollectedKey: "fw-key-123",
180
+ }),
181
+ ).toBe("missing-token");
182
+ });
183
+
184
+ test("an explicitly skipped key still counts as answered — configure runs and reports the gap", () => {
185
+ expect(
186
+ resolveDockerProviderCredentialSetupAction({
187
+ provider: "fireworks",
188
+ guardianAccessToken: "guardian-token",
189
+ detached: true,
190
+ precollectedKey: "",
191
+ }),
192
+ ).toBe("configure");
193
+ });
161
194
  });
162
195
 
163
196
  describe("buildServiceRunArgs — gateway", () => {
@@ -384,25 +417,48 @@ describe("collectWatchTargets", () => {
384
417
  });
385
418
 
386
419
  /**
387
- * Build a fake `exec` that records calls and fails the probes named in `fail`.
388
- * Injected into `ensureDockerDaemonRunning` so these cases need no `mock.module`
389
- * (which is process-global and would leak across the CLI's single-process test
390
- * run) and no real Docker/Colima subprocesses.
420
+ * Build a STATEFUL fake `exec` modeling the Docker daemon + Colima interplay:
421
+ * `docker info` succeeds only while `daemonUp`; a `colima start` heals the
422
+ * daemon (unless the fake is told to model the zombie state, where Colima's
423
+ * state file says "already running, ignoring" but the daemon stays dead —
424
+ * only the stop → start cycle recovers). Injected into
425
+ * `ensureDockerDaemonRunning` so these cases need no `mock.module` (which is
426
+ * process-global and would leak across the CLI's single-process test run) and
427
+ * no real Docker/Colima subprocesses.
391
428
  */
392
- function fakeExec(fail: { dockerInfo?: boolean; colimaStatus?: boolean }) {
429
+ function fakeExec(
430
+ opts: {
431
+ /** Daemon already reachable when the test starts. */
432
+ daemonUpAtStart?: boolean;
433
+ /** `colima start` exits nonzero instead of healing. */
434
+ startFails?: boolean;
435
+ /** First `colima start` is a zombie no-op that does NOT heal the daemon. */
436
+ zombieFirstStart?: boolean;
437
+ } = {},
438
+ ) {
393
439
  const calls: Array<{ cmd: string; args: string[] }> = [];
440
+ let daemonUp = opts.daemonUpAtStart ?? false;
441
+ let starts = 0;
394
442
  const exec = async (cmd: string, args: string[] = []): Promise<void> => {
395
443
  calls.push({ cmd, args });
396
- if (cmd === "docker" && args[0] === "info" && fail.dockerInfo) {
397
- throw new Error("Cannot connect to the Docker daemon");
444
+ if (cmd === "docker" && args[0] === "info") {
445
+ if (!daemonUp) throw new Error("Cannot connect to the Docker daemon");
446
+ return;
447
+ }
448
+ if (cmd === "colima" && args[0] === "start") {
449
+ starts++;
450
+ if (opts.startFails) throw new Error("colima start exited 1");
451
+ if (!opts.zombieFirstStart || starts > 1) daemonUp = true;
452
+ return;
398
453
  }
399
- if (cmd === "colima" && args[0] === "status" && fail.colimaStatus) {
400
- throw new Error("colima is not running");
454
+ if (cmd === "colima" && args[0] === "stop") {
455
+ daemonUp = false;
456
+ return;
401
457
  }
402
458
  };
403
459
  const ran = (cmd: string, sub: string): boolean =>
404
460
  calls.some((c) => c.cmd === cmd && c.args[0] === sub);
405
- return { exec: exec as typeof import("../step-runner.js").exec, ran };
461
+ return { exec, ran };
406
462
  }
407
463
 
408
464
  describe("ensureDockerDaemonRunning", () => {
@@ -410,30 +466,122 @@ describe("ensureDockerDaemonRunning", () => {
410
466
  // Colima, because that switches the active `docker context` and points
411
467
  // `docker start` at a VM that never held the instance's containers.
412
468
  test("reuses an already-reachable daemon (e.g. Docker Desktop) and never touches Colima", async () => {
413
- const { exec, ran } = fakeExec({ dockerInfo: false });
469
+ const { exec, ran } = fakeExec({ daemonUpAtStart: true });
414
470
 
415
471
  await ensureDockerDaemonRunning(exec);
416
472
 
417
473
  expect(ran("docker", "info")).toBe(true);
418
- expect(ran("colima", "status")).toBe(false);
419
474
  expect(ran("colima", "start")).toBe(false);
475
+ expect(ran("colima", "stop")).toBe(false);
420
476
  });
421
477
 
422
- test("starts Colima only when no daemon is reachable", async () => {
423
- const { exec, ran } = fakeExec({ dockerInfo: true, colimaStatus: true });
478
+ test("starts Colima when no daemon is reachable and confirms the daemon answered", async () => {
479
+ const { exec, ran } = fakeExec();
424
480
 
425
- await ensureDockerDaemonRunning(exec);
481
+ await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
426
482
 
427
483
  expect(ran("docker", "info")).toBe(true);
428
484
  expect(ran("colima", "start")).toBe(true);
485
+ expect(ran("colima", "stop")).toBe(false);
429
486
  });
430
487
 
431
- test("does not restart Colima when it is already running", async () => {
432
- const { exec, ran } = fakeExec({ dockerInfo: true, colimaStatus: false });
488
+ // Regression (v0.12.8 release feedback): a zombie Colima — stale state file
489
+ // saying "already running" while the daemon inside is dead — used to sail
490
+ // through wake (no `colima status` oracle anymore) and leave every later
491
+ // docker command dying on the unreachable socket.
492
+ test("recovers a zombie Colima: start no-ops, daemon stays dead → stop --force → start → daemon up", async () => {
493
+ const { exec, ran } = fakeExec({ zombieFirstStart: true });
433
494
 
434
- await ensureDockerDaemonRunning(exec);
495
+ await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
435
496
 
436
- expect(ran("colima", "status")).toBe(true);
437
- expect(ran("colima", "start")).toBe(false);
497
+ expect(ran("colima", "start")).toBe(true);
498
+ expect(ran("colima", "stop")).toBe(true);
499
+ });
500
+
501
+ test("never deletes the Colima VM — the VM disk holds every image and volume", async () => {
502
+ const { exec, ran } = fakeExec({ zombieFirstStart: true });
503
+
504
+ await ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 });
505
+
506
+ expect(ran("colima", "delete")).toBe(false);
507
+ });
508
+
509
+ test("throws an actionable error when even the restart cannot bring the daemon up", () => {
510
+ // A start that always exits nonzero: the first failure surfaces its
511
+ // output, the recovery stop+start runs, and the final wait still finds
512
+ // no daemon → the helper throws instead of letting hatch proceed.
513
+ const { exec } = fakeExec({ startFails: true });
514
+ expect(
515
+ ensureDockerDaemonRunning(exec, { settleTimeoutMs: 0 }),
516
+ ).rejects.toThrow(/Failed to restart Colima/);
517
+ });
518
+ });
519
+
520
+ describe("pullImage", () => {
521
+ function fakePull(
522
+ opts: {
523
+ failFirstPullWith?: string;
524
+ failAllPullsWith?: string;
525
+ } = {},
526
+ ) {
527
+ const calls: Array<{ cmd: string; args: string[] }> = [];
528
+ let pulls = 0;
529
+ const exec = async (cmd: string, args: string[] = []): Promise<void> => {
530
+ calls.push({ cmd, args });
531
+ if (cmd === "docker" && args[0] === "pull") {
532
+ pulls++;
533
+ if (opts.failAllPullsWith) throw new Error(opts.failAllPullsWith);
534
+ if (opts.failFirstPullWith && pulls === 1) {
535
+ throw new Error(opts.failFirstPullWith);
536
+ }
537
+ }
538
+ // `docker logout` (and anything else) succeeds silently.
539
+ };
540
+ const ran = (sub: string): boolean =>
541
+ calls.some((c) => c.cmd === "docker" && c.args[0] === sub);
542
+ return { exec, ran };
543
+ }
544
+
545
+ test("pulls without touching stored credentials on success", async () => {
546
+ const { exec, ran } = fakePull();
547
+
548
+ await pullImage("mycadet/mycadet-assistant:v1", () => {}, exec);
549
+
550
+ expect(ran("pull")).toBe(true);
551
+ expect(ran("logout")).toBe(false);
552
+ });
553
+
554
+ test("clears stale Docker Hub credentials and retries once on an expired-token error", async () => {
555
+ const { exec, ran } = fakePull({
556
+ failFirstPullWith:
557
+ "Error response from daemon: authentication required - personal access token is expired",
558
+ });
559
+
560
+ await pullImage("mycadet/mycadet-assistant:v1", () => {}, exec);
561
+
562
+ expect(ran("logout")).toBe(true);
563
+ });
564
+
565
+ test("wraps the failure with actionable copy when the auth retry also fails", () => {
566
+ const { exec } = fakePull({
567
+ failAllPullsWith:
568
+ "Error response from daemon: authentication required - personal access token is expired",
569
+ });
570
+
571
+ expect(
572
+ pullImage("mycadet/mycadet-assistant:v1", () => {}, exec),
573
+ ).rejects.toThrow("Failed to pull mycadet/mycadet-assistant:v1");
574
+ });
575
+
576
+ test("rethrows non-auth pull errors raw and never logs out", () => {
577
+ const { exec, ran } = fakePull({
578
+ failAllPullsWith:
579
+ "Cannot connect to the Docker daemon at unix:///var/run/docker.sock",
580
+ });
581
+
582
+ expect(
583
+ pullImage("mycadet/mycadet-assistant:v1", () => {}, exec),
584
+ ).rejects.toThrow("Cannot connect to the Docker daemon");
585
+ expect(ran("logout")).toBe(false);
438
586
  });
439
587
  });
@@ -1,7 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
2
 
3
3
  import { pollJobUntilDone } from "../job-polling.js";
4
- import type { UnifiedJobStatus } from "../platform-client.js";
4
+ import type { UnifiedJobStatus } from "../cloud-client.js";
5
5
 
6
6
  describe("pollJobUntilDone", () => {
7
7
  test("returns terminal 'complete' after N processing polls", async () => {
@@ -203,7 +203,7 @@ describe("pollJobUntilDone", () => {
203
203
  expect(refreshes).toBe(1);
204
204
  expect(calls).toBe(2);
205
205
  // The 401 branch logs its own distinct warning (not the generic
206
- // "polling failed, retrying" one) so operators can distinguish an
206
+ // "polling failed:" one) so operators can distinguish an
207
207
  // auth refresh from a transient-error retry in the output.
208
208
  expect(warnSpy).toHaveBeenCalledWith(
209
209
  expect.stringContaining("refreshing auth"),
@@ -14,7 +14,7 @@ const TOKEN = "local-bearer-token";
14
14
 
15
15
  // All tests in this file exercise the local/docker code path (cloud="local"),
16
16
  // which builds `{runtimeUrl}/v1/migrations/<subpath>` URLs and uses
17
- // guardian-token bearer auth. The platform path (cloud="cadet") is covered
17
+ // guardian-token bearer auth. The cloud path (cloud="cadet") is covered
18
18
  // by `runtime-url.test.ts` (URL construction) and the teleport tests
19
19
  // (call-site wiring).
20
20
  const ENTRY: Pick<AssistantEntry, "cloud" | "runtimeUrl" | "assistantId"> = {
@@ -384,7 +384,7 @@ describe("localRuntimePollJobStatus", () => {
384
384
  });
385
385
 
386
386
  // ---------------------------------------------------------------------------
387
- // Platform-managed assistants (cloud="cadet") route through the platform's
387
+ // Cloud assistants (cloud="cadet") route through the cloud's
388
388
  // wildcard runtime proxy at `/v1/assistants/<id>/migrations/...` with
389
389
  // platform-token auth (NOT guardian-token bearer). This block asserts the
390
390
  // actual URL and headers built by the helpers — not mocked, not abstracted.
@@ -588,7 +588,7 @@ describe("localRuntimeIdentity", () => {
588
588
  });
589
589
 
590
590
  // ---------------------------------------------------------------------
591
- // 401-retry parity with platformRequestSignedUrl (Codex P2 regression
591
+ // 401-retry parity with cloudRequestSignedUrl (Codex P2 regression
592
592
  // guard). localRuntimeIdentity is the first network call in the
593
593
  // backup/teleport export flow for cadet-cloud assistants, so a stale
594
594
  // Cadet-Organization-Id cache entry would surface as a hard abort
@@ -613,7 +613,7 @@ describe("localRuntimeIdentity", () => {
613
613
  orgIdFetchedAs.push(`org-${orgIdFetchedAs.length + 1}`);
614
614
  return new Response(
615
615
  JSON.stringify({
616
- results: [{ id: orgIdFetchedAs[orgIdFetchedAs.length - 1]! }],
616
+ results: [{ id: orgIdFetchedAs[orgIdFetchedAs.length - 1]!, name: "My Cadet" }],
617
617
  }),
618
618
  { status: 200 },
619
619
  );
@@ -49,7 +49,7 @@ describe("resolveRuntimeMigrationUrl", () => {
49
49
  );
50
50
  });
51
51
 
52
- test("cadet (platform-managed) cloud uses wildcard-proxy /v1/assistants/<id>/migrations/<subpath>", () => {
52
+ test("cadet (cloud-managed) cloud uses wildcard-proxy /v1/assistants/<id>/migrations/<subpath>", () => {
53
53
  const entry = makeEntry({
54
54
  cloud: "cadet",
55
55
  runtimeUrl: "https://cloud.mycadet.ai",
@@ -8,8 +8,8 @@
8
8
  import {
9
9
  readGatewayCredential,
10
10
  reprovisionAssistantApiKey,
11
- verifyGatewayManagedCredential,
12
- } from "./platform-client.js";
11
+ verifyGatewayCloudCredential,
12
+ } from "./cloud-client.js";
13
13
 
14
14
  /** Where the key came from, so callers can report what they did. */
15
15
  export type AssistantApiKeySource =
@@ -39,13 +39,13 @@ export interface ResolvedAssistantApiKey {
39
39
  * Three steps, in order: the key the registration just issued, the key the
40
40
  * assistant already holds, then a rotation.
41
41
  *
42
- * A stored key the platform has rejected is skipped, because reinjecting it
42
+ * A stored key the cloud has rejected is skipped, because reinjecting it
43
43
  * repairs nothing while looking like a repair. The verdict is asked for
44
44
  * before the key is read, so the key returned is one the verdict covers: a
45
45
  * repair that lands between the two requests is then read, not overwritten
46
46
  * by the value it replaced. Rotation is still withheld when the gateway is
47
47
  * merely unreachable: rotating without being able to store the replacement
48
- * would start the platform's grace clock on a key the assistant still needs,
48
+ * would start the cloud's grace clock on a key the assistant still needs,
49
49
  * with nothing to hand it in exchange.
50
50
  */
51
51
  export async function resolveAssistantApiKeyForInjection(
@@ -55,7 +55,7 @@ export async function resolveAssistantApiKeyForInjection(
55
55
  return { apiKey: args.registrationApiKey, source: "registration" };
56
56
  }
57
57
 
58
- const status = await verifyGatewayManagedCredential(
58
+ const status = await verifyGatewayCloudCredential(
59
59
  args.runtimeUrl,
60
60
  args.bearerToken,
61
61
  );
@@ -14,10 +14,10 @@
14
14
  * The auth scheme follows the assistant's topology, so callers never pick it.
15
15
  * Platform-managed entries in particular have no local guardian token: they
16
16
  * authenticate against the wildcard runtime proxy at
17
- * `{platformUrl}/v1/assistants/<id>/<rest>` with the stored cloud credential.
17
+ * `{cloudUrl}/v1/assistants/<id>/<rest>` with the stored cloud credential.
18
18
  */
19
19
 
20
- import { isManagedCloud } from "./managed-cloud.js";
20
+ import { isCloudHome } from "./cloud-values.js";
21
21
  import { resolveAssistant } from "./assistant-config.js";
22
22
  import { GATEWAY_PORT } from "./constants.js";
23
23
  import {
@@ -29,8 +29,8 @@ import { loopbackSafeFetch } from "./loopback-fetch.js";
29
29
  import {
30
30
  authHeaders,
31
31
  invalidateOrgIdCache,
32
- readPlatformToken,
33
- } from "./platform-client.js";
32
+ readCloudToken,
33
+ } from "./cloud-client.js";
34
34
 
35
35
  const DEFAULT_TIMEOUT_MS = 30_000;
36
36
  const FALLBACK_RUNTIME_URL = `http://127.0.0.1:${GATEWAY_PORT}`;
@@ -54,7 +54,7 @@ export interface AssistantClientOpts {
54
54
  * sent as `X-Session-Token: <sessionToken>` and the org id is
55
55
  * sent via the `Cadet-Organization-Id` header.
56
56
  *
57
- * Platform-managed assistants do NOT need this: they are detected from the
57
+ * Cloud assistants do NOT need this: they are detected from the
58
58
  * lockfile entry and authenticated from the stored platform token.
59
59
  */
60
60
  sessionToken?: string;
@@ -75,7 +75,7 @@ export class AssistantClient {
75
75
  private readonly _assistantId: string;
76
76
  /**
77
77
  * The credential for the current mode. Mutable: a 401 on the guardian path
78
- * refreshes it in place (see request), and the platform path fills it in on
78
+ * refreshes it in place (see request), and the cloud path fills it in on
79
79
  * first use (see resolveAuthHeaders).
80
80
  */
81
81
  private token: string | undefined;
@@ -103,7 +103,7 @@ export class AssistantClient {
103
103
  }
104
104
 
105
105
  this._assistantId = entry.assistantId;
106
- const platformManaged = !opts?.sessionToken && isManagedCloud(entry.cloud);
106
+ const platformManaged = !opts?.sessionToken && isCloudHome(entry.cloud);
107
107
  const platformHost = stripTrailingSlashes(entry.runtimeUrl);
108
108
 
109
109
  // SECURITY: a cloud credential is account-scoped, far broader than the
@@ -155,7 +155,7 @@ export class AssistantClient {
155
155
  * error rather than letting an anonymous request fall through to a bare 403.
156
156
  */
157
157
  private requirePlatformToken(): string {
158
- const token = readPlatformToken();
158
+ const token = readCloudToken();
159
159
  if (!token) {
160
160
  throw new Error(
161
161
  "Not logged in. Run `cadet login` first to authenticate with Cadet Cloud.",