@mycadet/cli 0.12.7 → 0.12.8

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 (165) 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-platform-login.ts → show-cloud-login.ts} +3 -5
  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 +1 -1
  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 +1 -1
  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 +1 -1
  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} +5 -5
  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__/guardian-token.test.ts +149 -56
  72. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  73. package/src/__tests__/helpers/guardian-token.ts +22 -0
  74. package/src/__tests__/helpers/http.ts +10 -0
  75. package/src/__tests__/nginx-ingress.test.ts +18 -18
  76. package/src/__tests__/orphan-detection.test.ts +1 -1
  77. package/src/__tests__/pair.test.ts +1 -8
  78. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
  79. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  80. package/src/__tests__/retire.test.ts +58 -199
  81. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  82. package/src/__tests__/sleep.test.ts +19 -19
  83. package/src/__tests__/statefulset.test.ts +8 -8
  84. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
  85. package/src/__tests__/teleport-backup.test.ts +3 -3
  86. package/src/__tests__/teleport.test.ts +180 -196
  87. package/src/__tests__/tunnel.test.ts +255 -227
  88. package/src/__tests__/uninstall.test.ts +13 -4
  89. package/src/__tests__/upgrade-local.test.ts +3 -3
  90. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  91. package/src/__tests__/wake.test.ts +258 -41
  92. package/src/__tests__/workos-pkce.test.ts +2 -2
  93. package/src/commands/backup.ts +22 -22
  94. package/src/commands/client.ts +76 -76
  95. package/src/commands/debug.ts +20 -20
  96. package/src/commands/exec.ts +5 -5
  97. package/src/commands/hatch.ts +22 -22
  98. package/src/commands/login.ts +22 -22
  99. package/src/commands/logs.ts +1 -1
  100. package/src/commands/ps.ts +188 -79
  101. package/src/commands/restart.ts +30 -0
  102. package/src/commands/restore.ts +138 -20
  103. package/src/commands/retire.ts +46 -148
  104. package/src/commands/rollback.ts +18 -24
  105. package/src/commands/setup.ts +2 -2
  106. package/src/commands/teleport.ts +93 -93
  107. package/src/commands/terminal.ts +157 -17
  108. package/src/commands/tunnel.ts +80 -122
  109. package/src/commands/uninstall.ts +55 -7
  110. package/src/commands/unpair.ts +3 -3
  111. package/src/commands/upgrade.ts +22 -46
  112. package/src/commands/wake.ts +162 -46
  113. package/src/index.ts +3 -9
  114. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  115. package/src/lib/__tests__/job-polling.test.ts +1 -1
  116. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  117. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  118. package/src/lib/assistant-api-key-resolution.ts +5 -5
  119. package/src/lib/assistant-client.ts +8 -8
  120. package/src/lib/assistant-config.ts +58 -12
  121. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  122. package/src/lib/cloud-login.ts +24 -24
  123. package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
  124. package/src/lib/cloud-values.ts +3 -0
  125. package/src/lib/config-utils.ts +1 -1
  126. package/src/lib/detached-process.ts +1 -1
  127. package/src/lib/docker.ts +111 -10
  128. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  129. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  130. package/src/lib/environments/resolve.ts +8 -8
  131. package/src/lib/guardian-token.ts +36 -4
  132. package/src/lib/hatch-local.ts +19 -23
  133. package/src/lib/health-check.ts +6 -6
  134. package/src/lib/ingress-config.ts +8 -35
  135. package/src/lib/job-polling.ts +1 -1
  136. package/src/lib/live-voice/connection.ts +12 -12
  137. package/src/lib/live-voice/tunnel.test.ts +11 -11
  138. package/src/lib/live-voice/tunnel.ts +15 -15
  139. package/src/lib/local-runtime-client.ts +20 -20
  140. package/src/lib/local.ts +9 -30
  141. package/src/lib/nginx-ingress.ts +41 -14
  142. package/src/lib/process.ts +1 -1
  143. package/src/lib/provider-secrets.ts +3 -3
  144. package/src/lib/retire-apple-container.ts +1 -1
  145. package/src/lib/runtime-url.ts +8 -8
  146. package/src/lib/statefulset.ts +2 -2
  147. package/src/lib/sync-cloud-assistants.ts +18 -18
  148. package/src/lib/teleport-backup.ts +4 -4
  149. package/src/lib/terminal-client.ts +17 -17
  150. package/src/lib/terminal-session.ts +24 -24
  151. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  152. package/src/lib/tunnels/cadet.ts +29 -0
  153. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
  154. package/src/lib/tunnels/gateway-notify.ts +61 -0
  155. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
  156. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
  157. package/src/lib/upgrade-lifecycle.ts +3 -17
  158. package/src/lib/upgrade-preflight.ts +2 -2
  159. package/src/lib/workos-pkce.ts +5 -5
  160. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  161. package/src/commands/recover.ts +0 -131
  162. package/src/commands/roadmap.ts +0 -459
  163. package/src/commands/ssh.ts +0 -144
  164. package/src/commands/workflows.ts +0 -305
  165. package/src/lib/managed-cloud.ts +0 -3
@@ -82,11 +82,12 @@ describe("cadet uninstall", () => {
82
82
  warnSpy.mockRestore();
83
83
  });
84
84
 
85
- test("refuses while assistants remain — retire them first", async () => {
85
+ test("warns and retires remaining assistants, then proceeds to the confirm gate", async () => {
86
86
  await withRestoredEnv(async () => {
87
87
  const root = redirectXdg();
88
- // A remaining assistant in the lockfile means there is data that
89
- // should be retired (and archived) first.
88
+ // A remaining assistant is retired — with its data deleted — as part
89
+ // of the uninstall (v0.0.10 run feedback: the old hard refusal just
90
+ // blocked the uninstall; the fs sweep deletes the archives anyway).
90
91
  writeFileSync(
91
92
  join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
92
93
  JSON.stringify({ assistants: [makeEntry("test-assistant")] }) + "\n",
@@ -97,8 +98,16 @@ describe("cadet uninstall", () => {
97
98
  throw new Error(`process.exit:${code}`);
98
99
  });
99
100
  try {
101
+ // Non-interactive without --yes, the CONFIRM gate is what refuses —
102
+ // not an assistant guard.
100
103
  await expect(uninstall()).rejects.toThrow("process.exit:1");
101
- expect(errorSpy).toHaveBeenCalledWith(
104
+ expect(warnSpy).toHaveBeenCalledWith(
105
+ expect.stringContaining("1 assistant(s) still exist"),
106
+ );
107
+ expect(warnSpy).toHaveBeenCalledWith(
108
+ expect.stringContaining("will be retired now"),
109
+ );
110
+ expect(errorSpy).not.toHaveBeenCalledWith(
102
111
  expect.stringContaining("Retire them first"),
103
112
  );
104
113
  } finally {
@@ -20,7 +20,7 @@ import type { GuardianTokenData } from "../lib/guardian-token.js";
20
20
  import * as guardianToken from "../lib/guardian-token.js";
21
21
  import * as local from "../lib/local.js";
22
22
  import * as loopbackFetch from "../lib/loopback-fetch.js";
23
- import * as ngrok from "../lib/ngrok.js";
23
+ import * as ngrok from "../lib/tunnels/ngrok.js";
24
24
  import * as upgradeLifecycle from "../lib/upgrade-lifecycle.js";
25
25
 
26
26
  const realAssistantConfig = { ...assistantConfig };
@@ -157,7 +157,7 @@ const maybeStartNgrokTunnelMock = mock<typeof ngrok.maybeStartNgrokTunnel>(
157
157
  async () => null,
158
158
  );
159
159
 
160
- mock.module("../lib/ngrok.js", () => ({
160
+ mock.module("../lib/tunnels/ngrok.js", () => ({
161
161
  ...realNgrok,
162
162
  maybeStartNgrokTunnel: maybeStartNgrokTunnelMock,
163
163
  }));
@@ -302,7 +302,7 @@ afterAll(() => {
302
302
  mock.module("../lib/guardian-token.js", () => realGuardianToken);
303
303
  mock.module("../lib/local.js", () => realLocal);
304
304
  mock.module("../lib/loopback-fetch.js", () => realLoopbackFetch);
305
- mock.module("../lib/ngrok.js", () => realNgrok);
305
+ mock.module("../lib/tunnels/ngrok.js", () => realNgrok);
306
306
  mock.module("../lib/upgrade-lifecycle.js", () => realUpgradeLifecycle);
307
307
  });
308
308
 
@@ -6,7 +6,7 @@ import { buildReplayEnv, buildReplayState } from "../lib/upgrade-lifecycle.js";
6
6
  import { snapshotEnv } from "./helpers/env.js";
7
7
 
8
8
  const restoreEnv = snapshotEnv([
9
- "CADET_PLATFORM_URL",
9
+ "CADET_CLOUD_URL",
10
10
  "ANTHROPIC_API_KEY",
11
11
  "CADET_DEVICE_ID",
12
12
  ]);
@@ -24,33 +24,33 @@ describe("buildReplayEnv", () => {
24
24
  ACTOR_TOKEN_SIGNING_KEY: "s3",
25
25
  PATH: "/usr/bin",
26
26
  GATEWAY_PORT: "18080",
27
- CADET_DISABLE_PLATFORM: "1",
27
+ CADET_DISABLE_CLOUD: "1",
28
28
  CADET_DEVICE_ID: "abc",
29
29
  };
30
30
 
31
31
  expect(buildReplayEnv(captured, "gateway")).toEqual({
32
- CADET_DISABLE_PLATFORM: "1",
32
+ CADET_DISABLE_CLOUD: "1",
33
33
  CADET_DEVICE_ID: "abc",
34
34
  });
35
35
  });
36
36
 
37
- test("gateway: captured CADET_PLATFORM_URL dropped when set on host", () => {
38
- process.env.CADET_PLATFORM_URL = "https://host.example.com";
37
+ test("gateway: captured CADET_CLOUD_URL dropped when set on host", () => {
38
+ process.env.CADET_CLOUD_URL = "https://host.example.com";
39
39
  const replay = buildReplayEnv(
40
- { CADET_PLATFORM_URL: "https://stale.example.com" },
40
+ { CADET_CLOUD_URL: "https://stale.example.com" },
41
41
  "gateway",
42
42
  );
43
43
  expect(replay).toEqual({});
44
44
  });
45
45
 
46
- test("gateway: captured CADET_PLATFORM_URL kept when unset on host", () => {
47
- delete process.env.CADET_PLATFORM_URL;
46
+ test("gateway: captured CADET_CLOUD_URL kept when unset on host", () => {
47
+ delete process.env.CADET_CLOUD_URL;
48
48
  const replay = buildReplayEnv(
49
- { CADET_PLATFORM_URL: "https://stale.example.com" },
49
+ { CADET_CLOUD_URL: "https://stale.example.com" },
50
50
  "gateway",
51
51
  );
52
52
  expect(replay).toEqual({
53
- CADET_PLATFORM_URL: "https://stale.example.com",
53
+ CADET_CLOUD_URL: "https://stale.example.com",
54
54
  });
55
55
  });
56
56
 
@@ -118,9 +118,9 @@ describe("buildReplayState", () => {
118
118
  });
119
119
 
120
120
  test("backfills CADET_DEVICE_ID on gateway replay env when absent", () => {
121
- const state = buildReplayState({}, { CADET_DISABLE_PLATFORM: "1" });
121
+ const state = buildReplayState({}, { CADET_DISABLE_CLOUD: "1" });
122
122
  expect(state.extraGatewayEnv).toEqual({
123
- CADET_DISABLE_PLATFORM: "1",
123
+ CADET_DISABLE_CLOUD: "1",
124
124
  CADET_DEVICE_ID: "host-device-id",
125
125
  });
126
126
  });
@@ -20,10 +20,11 @@ import * as httpClient from "../lib/http-client.js";
20
20
  import * as ingressConfig from "../lib/ingress-config.js";
21
21
  import * as local from "../lib/local.js";
22
22
  import * as nginxIngress from "../lib/nginx-ingress.js";
23
- import * as ngrok from "../lib/ngrok.js";
23
+ import * as ngrok from "../lib/tunnels/ngrok.js";
24
24
  import * as processLib from "../lib/process.js";
25
25
  import * as portAllocator from "../lib/port-allocator.js";
26
26
  import type { AssistantEntry } from "../lib/assistant-config.js";
27
+ import { makeGuardianTokenData } from "./helpers/guardian-token.js";
27
28
 
28
29
  const realAssistantConfig = { ...assistantConfig };
29
30
  const realDocker = { ...docker };
@@ -61,11 +62,15 @@ const dockerResourceNamesMock = mock<typeof docker.dockerResourceNames>(
61
62
  realDocker.dockerResourceNames,
62
63
  );
63
64
  const wakeContainersMock = mock<typeof docker.wakeContainers>(async () => {});
65
+ const recreateDockerContainersMock = mock<typeof docker.recreateDockerContainers>(
66
+ async () => {},
67
+ );
64
68
 
65
69
  mock.module("../lib/docker.js", () => ({
66
70
  ...realDocker,
67
71
  dockerResourceNames: dockerResourceNamesMock,
68
72
  wakeContainers: wakeContainersMock,
73
+ recreateDockerContainers: recreateDockerContainersMock,
69
74
  }));
70
75
 
71
76
  const seedGuardianTokenFromSiblingEnvMock = mock<
@@ -73,18 +78,14 @@ const seedGuardianTokenFromSiblingEnvMock = mock<
73
78
  >(() => false);
74
79
  // Default: a token exists, so the re-provision recovery path is skipped. Tests
75
80
  // that exercise recovery override loadGuardianToken to return null.
76
- const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(
77
- () =>
78
- ({ accessToken: "existing" }) as ReturnType<
79
- typeof guardianToken.loadGuardianToken
80
- >,
81
+ const loadGuardianTokenMock = mock<typeof guardianToken.loadGuardianToken>(() =>
82
+ makeGuardianTokenData({ accessToken: "existing" }),
81
83
  );
82
84
  const resetGuardianBootstrapMock = mock<
83
85
  typeof guardianToken.resetGuardianBootstrap
84
86
  >(async () => {});
85
87
  const leaseGuardianTokenMock = mock<typeof guardianToken.leaseGuardianToken>(
86
- async () =>
87
- ({}) as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>,
88
+ async () => makeGuardianTokenData(),
88
89
  );
89
90
 
90
91
  mock.module("../lib/guardian-token.js", () => ({
@@ -156,7 +157,7 @@ const maybeStartNgrokTunnelMock = mock<typeof ngrok.maybeStartNgrokTunnel>(
156
157
  async () => null,
157
158
  );
158
159
 
159
- mock.module("../lib/ngrok", () => ({
160
+ mock.module("../lib/tunnels/ngrok", () => ({
160
161
  ...realNgrok,
161
162
  maybeStartNgrokTunnel: maybeStartNgrokTunnelMock,
162
163
  }));
@@ -288,15 +289,13 @@ beforeEach(() => {
288
289
  seedGuardianTokenFromSiblingEnvMock.mockReset();
289
290
  seedGuardianTokenFromSiblingEnvMock.mockReturnValue(false);
290
291
  loadGuardianTokenMock.mockReset();
291
- loadGuardianTokenMock.mockReturnValue({
292
- accessToken: "existing",
293
- } as ReturnType<typeof guardianToken.loadGuardianToken>);
292
+ loadGuardianTokenMock.mockReturnValue(
293
+ makeGuardianTokenData({ accessToken: "existing" }),
294
+ );
294
295
  resetGuardianBootstrapMock.mockReset();
295
296
  resetGuardianBootstrapMock.mockResolvedValue(undefined);
296
297
  leaseGuardianTokenMock.mockReset();
297
- leaseGuardianTokenMock.mockResolvedValue(
298
- {} as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>,
299
- );
298
+ leaseGuardianTokenMock.mockResolvedValue(makeGuardianTokenData());
300
299
  maybeStartNgrokTunnelMock.mockReset();
301
300
  maybeStartNgrokTunnelMock.mockResolvedValue(null);
302
301
  loadRawConfigMock.mockReset();
@@ -330,7 +329,7 @@ afterAll(() => {
330
329
  mock.module("../lib/process", () => realProcessLib);
331
330
  mock.module("../lib/port-allocator.js", () => realPortAllocator);
332
331
  mock.module("../lib/local", () => realLocal);
333
- mock.module("../lib/ngrok", () => realNgrok);
332
+ mock.module("../lib/tunnels/ngrok", () => realNgrok);
334
333
  mock.module("../lib/ingress-config.js", () => realIngressConfig);
335
334
  mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
336
335
  mock.module("../lib/http-client.js", () => realHttpClient);
@@ -819,11 +818,11 @@ describe("cadet wake: daemon startup failure", () => {
819
818
  // AND a foreign listener on the runtime HTTP port answers readiness in
820
819
  // the dead daemon's place
821
820
  probeDaemonReadinessWithRetryMock.mockResolvedValue("ready");
822
- const exitSpy = spyOn(process, "exit").mockImplementation(((
823
- code?: number,
824
- ) => {
825
- throw new Error(`process.exit:${code}`);
826
- }) as never);
821
+ const exitSpy = spyOn(process, "exit").mockImplementation(
822
+ (code?: number | string | undefined): never => {
823
+ throw new Error(`process.exit:${code}`);
824
+ },
825
+ );
827
826
 
828
827
  // WHEN wake runs
829
828
  const result = wake();
@@ -945,7 +944,7 @@ describe("cadet wake — tunnel edge restore", () => {
945
944
  runtimeUrl: "http://localhost:7930",
946
945
  cloud: "docker",
947
946
  ingressUrl: "https://assistant.example.com",
948
- } as AssistantEntry);
947
+ });
949
948
  loadRawConfigMock.mockReturnValue(enabledConfig);
950
949
  process.argv = ["bun", "cadet", "wake", "docker-1"];
951
950
 
@@ -957,9 +956,13 @@ describe("cadet wake — tunnel edge restore", () => {
957
956
  workspaceDir: defaultWorkspace,
958
957
  gatewayPort: 7930,
959
958
  });
960
- // Container wakes never tracked a spawned ngrok PID, so the auto-tunnel
961
- // stays out of this path.
962
- expect(maybeStartNgrokTunnelMock).not.toHaveBeenCalled();
959
+ // Container wakes run the same auto-tunnel as local wakes (docker
960
+ // parity); the spawned agent records itself in the root lockfile.
961
+ expect(maybeStartNgrokTunnelMock).toHaveBeenCalledWith(
962
+ 7840,
963
+ defaultWorkspace,
964
+ "docker-1",
965
+ );
963
966
  expect(logSpy).toHaveBeenCalledWith("Wake complete.");
964
967
  } finally {
965
968
  if (originalWorkspaceDir === undefined) {
@@ -971,6 +974,48 @@ describe("cadet wake — tunnel edge restore", () => {
971
974
  }
972
975
  });
973
976
 
977
+ test("a migrated docker assistant (containers never created) is recreated from the entry", async () => {
978
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
979
+ const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
980
+ process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
981
+ const migratedEntry = {
982
+ assistantId: "docker-1",
983
+ runtimeUrl: "http://localhost:7930",
984
+ cloud: "docker" as const,
985
+ containerInfo: {
986
+ assistantImage: "mycadet/mycadet-assistant:v0.12.7",
987
+ gatewayImage: "mycadet/mycadet-gateway:v0.12.7",
988
+ cesImage: "mycadet/mycadet-credential-executor:v0.12.7",
989
+ },
990
+ };
991
+ resolveTargetAssistantMock.mockReturnValue(migratedEntry);
992
+ loadRawConfigMock.mockReturnValue(enabledConfig);
993
+ // The v0.0.10 run feedback: the containers were never created —
994
+ // `docker start` answers "No such container".
995
+ wakeContainersMock.mockImplementation(async () => {
996
+ throw new Error(
997
+ "Error response from daemon: No such container: docker-1-assistant",
998
+ );
999
+ });
1000
+ process.argv = ["bun", "cadet", "wake", "docker-1"];
1001
+
1002
+ try {
1003
+ await wake();
1004
+
1005
+ expect(recreateDockerContainersMock).toHaveBeenCalledWith(migratedEntry);
1006
+ expect(logSpy).toHaveBeenCalledWith("Docker containers started.");
1007
+ expect(logSpy).toHaveBeenCalledWith("Wake complete.");
1008
+ } finally {
1009
+ wakeContainersMock.mockImplementation(async () => {});
1010
+ if (originalWorkspaceDir === undefined) {
1011
+ delete process.env.CADET_WORKSPACE_DIR;
1012
+ } else {
1013
+ process.env.CADET_WORKSPACE_DIR = originalWorkspaceDir;
1014
+ }
1015
+ rmSync(defaultWorkspace, { recursive: true, force: true });
1016
+ }
1017
+ });
1018
+
974
1019
  test("a docker wake leaves an edge fronting another container alone", async () => {
975
1020
  const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
976
1021
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
@@ -980,7 +1025,7 @@ describe("cadet wake — tunnel edge restore", () => {
980
1025
  runtimeUrl: "http://localhost:7930",
981
1026
  cloud: "docker",
982
1027
  ingressUrl: "https://assistant.example.com",
983
- } as AssistantEntry);
1028
+ });
984
1029
  loadRawConfigMock.mockReturnValue(enabledConfig);
985
1030
  isIngressRunningMock.mockReturnValue(true);
986
1031
  readIngressStateMock.mockReturnValue({
@@ -1014,7 +1059,7 @@ describe("cadet wake — tunnel edge restore", () => {
1014
1059
  runtimeUrl: "http://localhost:7930",
1015
1060
  cloud: "docker",
1016
1061
  ingressUrl: "https://assistant.example.com",
1017
- } as AssistantEntry);
1062
+ });
1018
1063
  loadRawConfigMock.mockReturnValue(enabledConfig);
1019
1064
  isIngressRunningMock.mockReturnValue(true);
1020
1065
  readIngressStateMock.mockReturnValue({
@@ -1052,7 +1097,7 @@ describe("cadet wake — tunnel edge restore", () => {
1052
1097
  }
1053
1098
  });
1054
1099
 
1055
- test("a failed docker edge restore does not claim webhooks still work", async () => {
1100
+ test("a failed docker edge restore falls back to the gateway tunnel", async () => {
1056
1101
  const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
1057
1102
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
1058
1103
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
@@ -1061,7 +1106,7 @@ describe("cadet wake — tunnel edge restore", () => {
1061
1106
  runtimeUrl: "http://localhost:7930",
1062
1107
  cloud: "docker",
1063
1108
  ingressUrl: "https://assistant.example.com",
1064
- } as AssistantEntry);
1109
+ });
1065
1110
  loadRawConfigMock.mockReturnValue(enabledConfig);
1066
1111
  ensureTunnelEdgeMock.mockRejectedValue(
1067
1112
  new Error("nginx is not installed."),
@@ -1071,11 +1116,14 @@ describe("cadet wake — tunnel edge restore", () => {
1071
1116
  try {
1072
1117
  await wake();
1073
1118
 
1119
+ // Docker wakes get the same tunnel logic as local ones: when the edge
1120
+ // cannot be rebuilt, the auto-tunnel still fronts the gateway port so
1121
+ // webhook delivery survives.
1074
1122
  expect(warnSpy).toHaveBeenCalledWith(
1075
- expect.stringContaining("webhook delivery are unavailable"),
1123
+ expect.stringContaining("Webhooks still work"),
1076
1124
  );
1077
1125
  expect(warnSpy).not.toHaveBeenCalledWith(
1078
- expect.stringContaining("Webhooks still work"),
1126
+ expect.stringContaining("webhook delivery are unavailable"),
1079
1127
  );
1080
1128
  } finally {
1081
1129
  if (originalWorkspaceDir === undefined) {
@@ -1098,14 +1146,14 @@ describe("cadet wake — tunnel edge restore", () => {
1098
1146
  runtimeUrl: "http://localhost:7930",
1099
1147
  cloud: "docker",
1100
1148
  ingressUrl: "https://other.example.com",
1101
- } as AssistantEntry);
1149
+ });
1102
1150
  loadAllAssistantsMock.mockReturnValue([
1103
1151
  {
1104
1152
  assistantId: "docker-2",
1105
1153
  runtimeUrl: "http://localhost:8030",
1106
1154
  cloud: "docker",
1107
1155
  ingressUrl: "https://assistant.example.com",
1108
- } as AssistantEntry,
1156
+ },
1109
1157
  ]);
1110
1158
  loadRawConfigMock.mockReturnValue(enabledConfig);
1111
1159
  isIngressRunningMock.mockReturnValue(false);
@@ -1135,7 +1183,7 @@ describe("cadet wake — tunnel edge restore", () => {
1135
1183
  runtimeUrl: "http://localhost:7930",
1136
1184
  cloud: "docker",
1137
1185
  ingressUrl: "https://assistant.example.com",
1138
- } as AssistantEntry);
1186
+ });
1139
1187
  loadRawConfigMock.mockReturnValue(enabledConfig);
1140
1188
  process.argv = ["bun", "cadet", "wake", "docker-1"];
1141
1189
 
@@ -1163,7 +1211,7 @@ describe("cadet wake — tunnel edge restore", () => {
1163
1211
  runtimeUrl: "http://localhost:7930",
1164
1212
  cloud: "docker",
1165
1213
  ingressUrl: "https://assistant.example.com",
1166
- } as AssistantEntry);
1214
+ });
1167
1215
  loadRawConfigMock.mockReturnValue(enabledConfig);
1168
1216
  waitForDaemonReadyMock.mockImplementation(async () => false);
1169
1217
  process.argv = ["bun", "cadet", "wake", "docker-1"];
@@ -1194,7 +1242,7 @@ describe("cadet wake — tunnel edge restore", () => {
1194
1242
  assistantId: "docker-1",
1195
1243
  runtimeUrl: "http://localhost:7930",
1196
1244
  cloud: "docker",
1197
- } as AssistantEntry);
1245
+ });
1198
1246
  process.argv = ["bun", "cadet", "wake", "docker-1"];
1199
1247
 
1200
1248
  try {
@@ -1222,13 +1270,13 @@ describe("cadet wake — tunnel edge restore", () => {
1222
1270
  assistantId: "docker-1",
1223
1271
  runtimeUrl: "http://localhost:7930",
1224
1272
  cloud: "docker",
1225
- } as AssistantEntry);
1273
+ });
1226
1274
  loadAllAssistantsMock.mockReturnValue([
1227
1275
  {
1228
1276
  assistantId: "docker-1",
1229
1277
  runtimeUrl: "http://localhost:7930",
1230
1278
  cloud: "docker",
1231
- } as AssistantEntry,
1279
+ },
1232
1280
  ]);
1233
1281
  loadRawConfigMock.mockReturnValue(enabledConfig);
1234
1282
  isIngressRunningMock.mockReturnValue(false);
@@ -1260,14 +1308,14 @@ describe("cadet wake — tunnel edge restore", () => {
1260
1308
  assistantId: "docker-1",
1261
1309
  runtimeUrl: "http://localhost:7930",
1262
1310
  cloud: "docker",
1263
- } as AssistantEntry);
1311
+ });
1264
1312
  loadAllAssistantsAcrossEnvsMock.mockReturnValue([
1265
1313
  {
1266
1314
  assistantId: "docker-2",
1267
1315
  runtimeUrl: "http://localhost:8030",
1268
1316
  cloud: "docker",
1269
1317
  ingressUrl: "https://assistant.example.com",
1270
- } as AssistantEntry,
1318
+ },
1271
1319
  ]);
1272
1320
  loadRawConfigMock.mockReturnValue(enabledConfig);
1273
1321
  isIngressRunningMock.mockReturnValue(false);
@@ -1296,7 +1344,7 @@ describe("cadet wake — tunnel edge restore", () => {
1296
1344
  runtimeUrl: "http://localhost:7930",
1297
1345
  cloud: "docker",
1298
1346
  ingressUrl: "https://assistant.example.com",
1299
- } as AssistantEntry);
1347
+ });
1300
1348
  process.argv = ["bun", "cadet", "wake", "docker-1"];
1301
1349
 
1302
1350
  try {
@@ -1541,4 +1589,173 @@ describe("cadet wake — tunnel edge restore", () => {
1541
1589
  "local-assistant",
1542
1590
  );
1543
1591
  });
1592
+
1593
+ describe("--all", () => {
1594
+ function makeAllEntry(
1595
+ assistantId: string,
1596
+ instanceDir: string,
1597
+ ): AssistantEntry {
1598
+ return {
1599
+ assistantId,
1600
+ runtimeUrl: "http://127.0.0.1:7830",
1601
+ cloud: "local",
1602
+ resources: {
1603
+ instanceDir,
1604
+ daemonPort: 7821,
1605
+ gatewayPort: 7830,
1606
+ qdrantPort: 6333,
1607
+ cesPort: 7822,
1608
+ signingKey: "ab".repeat(32),
1609
+ },
1610
+ };
1611
+ }
1612
+
1613
+ function makeAllEntries(): AssistantEntry[] {
1614
+ mkdirSync(join(tempDir, "instance-1"), { recursive: true });
1615
+ mkdirSync(join(tempDir, "instance-2"), { recursive: true });
1616
+ return [
1617
+ makeAllEntry("wake-all-1", join(tempDir, "instance-1")),
1618
+ makeAllEntry("wake-all-2", join(tempDir, "instance-2")),
1619
+ ];
1620
+ }
1621
+
1622
+ // Cold-start both assistants so each wake's starts are observable.
1623
+ function mockColdStart(): void {
1624
+ resolveProcessStateMock.mockImplementation(async () => ({
1625
+ status: "needs_start",
1626
+ pid: null,
1627
+ }));
1628
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
1629
+ }
1630
+
1631
+ test("starts every assistant in the lockfile", async () => {
1632
+ mockColdStart();
1633
+ loadAllAssistantsMock.mockReturnValue(makeAllEntries());
1634
+ process.argv = ["bun", "cadet", "wake", "--all"];
1635
+
1636
+ await wake();
1637
+
1638
+ expect(startCesMock).toHaveBeenCalledTimes(2);
1639
+ expect(startLocalDaemonMock).toHaveBeenCalledTimes(2);
1640
+ expect(startGatewayMock).toHaveBeenCalledTimes(2);
1641
+ });
1642
+
1643
+ test("skips assistants whose lifecycle is managed elsewhere", async () => {
1644
+ mockColdStart();
1645
+ const entries = makeAllEntries();
1646
+ entries[1] = {
1647
+ assistantId: "paired-one",
1648
+ runtimeUrl: "http://127.0.0.1:1",
1649
+ cloud: "paired",
1650
+ };
1651
+ loadAllAssistantsMock.mockReturnValue(entries);
1652
+ process.argv = ["bun", "cadet", "wake", "--all"];
1653
+
1654
+ await wake();
1655
+
1656
+ expect(startCesMock).toHaveBeenCalledTimes(1);
1657
+ expect(startLocalDaemonMock).toHaveBeenCalledTimes(1);
1658
+ expect(startGatewayMock).toHaveBeenCalledTimes(1);
1659
+ expect(logSpy.mock.calls.flat().join("\n")).toContain(
1660
+ "Skipping 'paired-one' (paired) — its lifecycle is managed elsewhere.",
1661
+ );
1662
+ });
1663
+
1664
+ test("continues past a failing assistant and exits 1", async () => {
1665
+ mockColdStart();
1666
+ loadAllAssistantsMock.mockReturnValue([
1667
+ // Missing resources is a deliberate refusal for this entry.
1668
+ {
1669
+ assistantId: "wake-all-1",
1670
+ runtimeUrl: "http://127.0.0.1:7830",
1671
+ cloud: "local",
1672
+ },
1673
+ makeAllEntry("wake-all-2", join(tempDir, "instance-2")),
1674
+ ]);
1675
+ mkdirSync(join(tempDir, "instance-2"), { recursive: true });
1676
+ process.argv = ["bun", "cadet", "wake", "--all"];
1677
+
1678
+ const exitSpy = spyOn(process, "exit").mockImplementation(
1679
+ (code?: number | string | undefined): never => {
1680
+ throw new Error(`process.exit:${code}`);
1681
+ },
1682
+ );
1683
+
1684
+ try {
1685
+ await expect(wake()).rejects.toThrow("process.exit:1");
1686
+ expect(errorSpy.mock.calls.flat().join("\n")).toContain(
1687
+ "failed to wake 'wake-all-1'",
1688
+ );
1689
+ } finally {
1690
+ exitSpy.mockRestore();
1691
+ }
1692
+
1693
+ // The healthy assistant was still started.
1694
+ expect(startCesMock).toHaveBeenCalledTimes(1);
1695
+ expect(startLocalDaemonMock).toHaveBeenCalledTimes(1);
1696
+ expect(startGatewayMock).toHaveBeenCalledTimes(1);
1697
+ });
1698
+
1699
+ test("exits 1 when there is nothing to start", async () => {
1700
+ loadAllAssistantsMock.mockReturnValue([]);
1701
+ process.argv = ["bun", "cadet", "wake", "--all"];
1702
+
1703
+ const exitSpy = spyOn(process, "exit").mockImplementation(
1704
+ (code?: number | string | undefined): never => {
1705
+ throw new Error(`process.exit:${code}`);
1706
+ },
1707
+ );
1708
+
1709
+ try {
1710
+ await expect(wake()).rejects.toThrow("process.exit:1");
1711
+ expect(errorSpy).toHaveBeenCalledWith(
1712
+ expect.stringContaining("no assistants found"),
1713
+ );
1714
+ } finally {
1715
+ exitSpy.mockRestore();
1716
+ }
1717
+ });
1718
+
1719
+ test("refuses --all combined with a name", async () => {
1720
+ process.argv = ["bun", "cadet", "wake", "local-assistant", "--all"];
1721
+
1722
+ const exitSpy = spyOn(process, "exit").mockImplementation(
1723
+ (code?: number | string | undefined): never => {
1724
+ throw new Error(`process.exit:${code}`);
1725
+ },
1726
+ );
1727
+
1728
+ try {
1729
+ await expect(wake()).rejects.toThrow("process.exit:1");
1730
+ expect(errorSpy).toHaveBeenCalledWith(
1731
+ expect.stringContaining("--all cannot be combined"),
1732
+ );
1733
+ } finally {
1734
+ exitSpy.mockRestore();
1735
+ }
1736
+ });
1737
+
1738
+ test("refuses --all with --foreground and --repair-guardian", async () => {
1739
+ const exitSpy = spyOn(process, "exit").mockImplementation(
1740
+ (code?: number | string | undefined): never => {
1741
+ throw new Error(`process.exit:${code}`);
1742
+ },
1743
+ );
1744
+
1745
+ try {
1746
+ for (const flag of ["--foreground", "--repair-guardian"]) {
1747
+ process.argv = ["bun", "cadet", "wake", "--all", flag];
1748
+ await expect(wake()).rejects.toThrow("process.exit:1");
1749
+ }
1750
+ } finally {
1751
+ exitSpy.mockRestore();
1752
+ }
1753
+
1754
+ expect(errorSpy).toHaveBeenCalledWith(
1755
+ expect.stringContaining(
1756
+ "--all cannot be combined with --foreground or --repair-guardian",
1757
+ ),
1758
+ );
1759
+ });
1760
+ });
1544
1761
  });
@@ -88,7 +88,7 @@ describe("buildAuthorizeUrl", () => {
88
88
  });
89
89
 
90
90
  describe("selectWorkosClientId", () => {
91
- // During coexistence the platform lists two providers that share the
91
+ // During coexistence the cloud lists two providers that share the
92
92
  // "workos-oidc" id; only the OAuth2 one (no discovery URL) is usable.
93
93
  const legacy = {
94
94
  id: "workos-oidc",
@@ -110,7 +110,7 @@ describe("selectWorkosClientId", () => {
110
110
  expect(selectWorkosClientId([modern, legacy])).toBe("client_um");
111
111
  });
112
112
 
113
- test("returns null when the platform lacks token auth", () => {
113
+ test("returns null when the cloud lacks token auth", () => {
114
114
  const preTokenAuth = { ...modern, flows: ["provider_redirect"] };
115
115
  expect(selectWorkosClientId([legacy, preTokenAuth])).toBeNull();
116
116
  expect(selectWorkosClientId([])).toBeNull();