@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
@@ -21,7 +21,7 @@ import { join } from "node:path";
21
21
  import type { AssistantEntry } from "../lib/assistant-config.js";
22
22
  import { loadAllAssistants } from "../lib/assistant-config.js";
23
23
  import * as loopbackFetchModule from "../lib/loopback-fetch.js";
24
- import * as platformClientModule from "../lib/platform-client.js";
24
+ import * as cloudClientModule from "../lib/cloud-client.js";
25
25
  import * as retireLocalModule from "../lib/retire-local.js";
26
26
 
27
27
  const testDir = mkdtempSync(join(tmpdir(), "cli-retire-test-"));
@@ -35,11 +35,11 @@ const originalStdinIsRaw = process.stdin.isRaw;
35
35
  const originalSetRawMode = process.stdin.setRawMode;
36
36
  const originalStdoutWrite = process.stdout.write;
37
37
  const realRetireLocalModule = { ...retireLocalModule };
38
- const realPlatformClientModule = { ...platformClientModule };
38
+ const realCloudClientModule = { ...cloudClientModule };
39
39
  const realLoopbackFetchModule = { ...loopbackFetchModule };
40
40
 
41
41
  const retireLocalMock = mock(async () => {});
42
- const readPlatformTokenMock = mock((): string | null => null);
42
+ const readCloudTokenMock = mock((): string | null => null);
43
43
  const authHeadersMock = mock(async () => ({
44
44
  "Content-Type": "application/json",
45
45
  "X-Session-Token": "session-token",
@@ -51,7 +51,7 @@ const authHeadersForKnownOrganizationMock = mock(
51
51
  "Cadet-Organization-Id": organizationId,
52
52
  }),
53
53
  );
54
- const getPlatformUrlMock = mock(() => "https://platform.example.com");
54
+ const getCloudUrlMock = mock(() => "https://platform.example.com");
55
55
  const readGatewayCredentialMock = mock(
56
56
  async (
57
57
  _gatewayUrl: string,
@@ -71,12 +71,12 @@ mock.module("../lib/retire-local.js", () => ({
71
71
  retireLocal: retireLocalMock,
72
72
  }));
73
73
 
74
- mock.module("../lib/platform-client.js", () => ({
74
+ mock.module("../lib/cloud-client.js", () => ({
75
75
  authHeaders: authHeadersMock,
76
76
  authHeadersForKnownOrganization: authHeadersForKnownOrganizationMock,
77
- getPlatformUrl: getPlatformUrlMock,
77
+ getCloudUrl: getCloudUrlMock,
78
78
  readGatewayCredential: readGatewayCredentialMock,
79
- readPlatformToken: readPlatformTokenMock,
79
+ readCloudToken: readCloudTokenMock,
80
80
  }));
81
81
 
82
82
  mock.module("../lib/loopback-fetch.js", () => ({
@@ -168,7 +168,11 @@ describe("cadet retire", () => {
168
168
  beforeEach(() => {
169
169
  process.env.CADET_LOCKFILE_DIR = testDir;
170
170
  delete process.env.CADET_PLATFORM_TOKEN;
171
- for (const name of ["lockfile.json", ".cadet.lock.json", ".cadet.lockfile.json"]) {
171
+ for (const name of [
172
+ "lockfile.json",
173
+ ".cadet.lock.json",
174
+ ".cadet.lockfile.json",
175
+ ]) {
172
176
  rmSync(join(testDir, name), { force: true });
173
177
  }
174
178
  process.argv = ["bun", "cadet", "retire"];
@@ -177,8 +181,8 @@ describe("cadet retire", () => {
177
181
  }) as typeof process.exit;
178
182
  retireLocalMock.mockReset();
179
183
  retireLocalMock.mockResolvedValue(undefined);
180
- readPlatformTokenMock.mockReset();
181
- readPlatformTokenMock.mockReturnValue(null);
184
+ readCloudTokenMock.mockReset();
185
+ readCloudTokenMock.mockReturnValue(null);
182
186
  authHeadersMock.mockReset();
183
187
  authHeadersMock.mockResolvedValue({
184
188
  "Content-Type": "application/json",
@@ -192,8 +196,8 @@ describe("cadet retire", () => {
192
196
  "Cadet-Organization-Id": organizationId,
193
197
  }),
194
198
  );
195
- getPlatformUrlMock.mockReset();
196
- getPlatformUrlMock.mockReturnValue("https://platform.example.com");
199
+ getCloudUrlMock.mockReset();
200
+ getCloudUrlMock.mockReturnValue("https://platform.example.com");
197
201
  readGatewayCredentialMock.mockReset();
198
202
  readGatewayCredentialMock.mockResolvedValue({
199
203
  value: null,
@@ -220,7 +224,7 @@ describe("cadet retire", () => {
220
224
 
221
225
  afterAll(() => {
222
226
  mock.module("../lib/retire-local.js", () => realRetireLocalModule);
223
- mock.module("../lib/platform-client.js", () => realPlatformClientModule);
227
+ mock.module("../lib/cloud-client.js", () => realCloudClientModule);
224
228
  mock.module("../lib/loopback-fetch.js", () => realLoopbackFetchModule);
225
229
  if (originalLockfileDir === undefined) {
226
230
  delete process.env.CADET_LOCKFILE_DIR;
@@ -252,35 +256,25 @@ describe("cadet retire", () => {
252
256
  );
253
257
  });
254
258
 
255
- test("local retire unregisters the self-hosted platform record first", async () => {
256
- const entry = makeEntry("assistant-1", { name: "Example Assistant" });
257
- writeLockfile([entry]);
258
- readPlatformTokenMock.mockReturnValue("session-token");
259
- getPlatformUrlMock.mockReturnValue("https://platform.example.test/api");
260
- readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
261
- if (name === "cadet:platform_assistant_id") {
262
- return { value: "platform-assistant-1", unreachable: false };
263
- }
264
- if (name === "cadet:platform_base_url") {
265
- return { value: "https://platform.example.test", unreachable: false };
266
- }
267
- if (name === "cadet:platform_organization_id") {
268
- return { value: "org-123", unreachable: false };
269
- }
270
- return { value: null, unreachable: false };
259
+ test("local retire unregisters the cloud record straight from the lockfile entry", async () => {
260
+ const entry = makeEntry("assistant-1", {
261
+ name: "Example Assistant",
262
+ localUrl: "http://127.0.0.1:7831",
263
+ cloudAssistantId: "cloud-asst-1",
264
+ cloudOrganizationId: "org-123",
271
265
  });
266
+ writeLockfile([entry]);
267
+ readCloudTokenMock.mockReturnValue("session-token");
268
+ getCloudUrlMock.mockReturnValue("https://platform.example.test");
272
269
  process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
273
270
 
274
271
  await retire();
275
272
 
276
- expect(readGatewayCredentialMock).toHaveBeenCalledWith(
277
- entry.localUrl ?? entry.runtimeUrl,
278
- "cadet:platform_assistant_id",
279
- entry.bearerToken,
280
- );
281
- expect(authHeadersMock).not.toHaveBeenCalled();
273
+ // The cloud record resolves from the entry + env — the local gateway
274
+ // credential store is never consulted.
275
+ expect(readGatewayCredentialMock).not.toHaveBeenCalled();
282
276
  expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
283
- "https://platform.example.test/api/v1/assistants/platform-assistant-1/retire/",
277
+ "https://platform.example.test/v1/assistants/cloud-asst-1/retire/",
284
278
  {
285
279
  method: "DELETE",
286
280
  headers: {
@@ -294,194 +288,67 @@ describe("cadet retire", () => {
294
288
  expect(loadAllAssistants()).toEqual([]);
295
289
  });
296
290
 
297
- test("local retire reads platform registration through the loopback gateway URL", async () => {
298
- const localGatewayUrl = "http://127.0.0.1:7831";
299
- const entry = makeEntry("assistant-1", {
300
- name: "Example Assistant",
301
- runtimeUrl: "http://assistant-1.local:7831",
302
- localUrl: localGatewayUrl,
303
- });
304
- writeLockfile([entry]);
305
- readPlatformTokenMock.mockReturnValue("session-token");
306
- getPlatformUrlMock.mockReturnValue("https://platform.example.test");
307
- readGatewayCredentialMock.mockImplementation(async (gatewayUrl, name) => {
308
- expect(gatewayUrl).toBe(localGatewayUrl);
309
- if (name === "cadet:platform_assistant_id") {
310
- return { value: "platform-assistant-1", unreachable: false };
311
- }
312
- if (name === "cadet:platform_base_url") {
313
- return { value: "https://platform.example.test", unreachable: false };
314
- }
315
- if (name === "cadet:platform_organization_id") {
316
- return { value: "org-123", unreachable: false };
317
- }
318
- return { value: null, unreachable: false };
319
- });
320
- process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
321
-
322
- await retire();
323
-
324
- expect(readGatewayCredentialMock).toHaveBeenCalledWith(
325
- localGatewayUrl,
326
- "cadet:platform_assistant_id",
327
- entry.bearerToken,
328
- );
329
- expect(readGatewayCredentialMock).toHaveBeenCalledWith(
330
- localGatewayUrl,
331
- "cadet:platform_base_url",
332
- entry.bearerToken,
333
- );
334
- expect(readGatewayCredentialMock).toHaveBeenCalledWith(
335
- localGatewayUrl,
336
- "cadet:platform_organization_id",
337
- entry.bearerToken,
338
- );
339
- expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
340
- "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
341
- expect.any(Object),
342
- );
343
- expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
344
- });
345
-
346
- test("local retire uses saved platform registration when local credentials are unreachable", async () => {
291
+ test("local retire resolves the organization via the cloud when the entry omits it", async () => {
347
292
  const entry = makeEntry("assistant-1", {
348
293
  name: "Example Assistant",
349
- platformAssistantId: "platform-assistant-1",
350
- platformBaseUrl: "https://platform.example.test",
351
- platformOrganizationId: "org-123",
294
+ cloudAssistantId: "cloud-asst-1",
352
295
  });
353
296
  writeLockfile([entry]);
354
- readPlatformTokenMock.mockReturnValue("session-token");
355
- getPlatformUrlMock.mockReturnValue("https://platform.example.test");
356
- readGatewayCredentialMock.mockResolvedValue({
357
- value: null,
358
- unreachable: true,
359
- });
297
+ readCloudTokenMock.mockReturnValue("session-token");
298
+ getCloudUrlMock.mockReturnValue("https://platform.example.test");
360
299
  process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
361
300
 
362
301
  await retire();
363
302
 
364
- expect(readGatewayCredentialMock).toHaveBeenCalledWith(
365
- entry.localUrl ?? entry.runtimeUrl,
366
- "cadet:platform_assistant_id",
367
- entry.bearerToken,
303
+ expect(authHeadersMock).toHaveBeenCalledWith(
304
+ "session-token",
305
+ "https://platform.example.test",
368
306
  );
307
+ expect(authHeadersForKnownOrganizationMock).not.toHaveBeenCalled();
369
308
  expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
370
- "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
309
+ "https://platform.example.test/v1/assistants/cloud-asst-1/retire/",
371
310
  {
372
311
  method: "DELETE",
373
312
  headers: {
374
313
  "Content-Type": "application/json",
375
314
  "X-Session-Token": "session-token",
376
- "Cadet-Organization-Id": "org-123",
377
315
  },
378
316
  },
379
317
  );
380
- expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
381
- "using saved platform registration",
382
- );
383
318
  expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
384
319
  expect(loadAllAssistants()).toEqual([]);
385
320
  });
386
321
 
387
- test("local retire continues when local credentials are unreachable and no saved platform registration exists", async () => {
322
+ test("local retire skips the cloud unregister when no assistant id is bound", async () => {
388
323
  const entry = makeEntry("assistant-1", { name: "Example Assistant" });
389
324
  writeLockfile([entry]);
390
- readPlatformTokenMock.mockReturnValue("session-token");
391
- readGatewayCredentialMock.mockResolvedValue({
392
- value: null,
393
- unreachable: true,
394
- });
325
+ readCloudTokenMock.mockReturnValue("session-token");
326
+ getCloudUrlMock.mockReturnValue("https://platform.example.test");
395
327
  process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
396
328
 
397
329
  await retire();
398
330
 
399
331
  expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
400
- expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
401
- "no saved platform registration is available",
402
- );
403
- expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
404
- expect(loadAllAssistants()).toEqual([]);
405
- });
406
-
407
- test("local retire skips platform unregister when stored platform URL origin mismatches", async () => {
408
- const entry = makeEntry("assistant-1", { name: "Example Assistant" });
409
- writeLockfile([entry]);
410
- readPlatformTokenMock.mockReturnValue("session-token");
411
- getPlatformUrlMock.mockReturnValue("https://platform.example.com");
412
- readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
413
- if (name === "cadet:platform_assistant_id") {
414
- return { value: "platform-assistant-1", unreachable: false };
415
- }
416
- if (name === "cadet:platform_base_url") {
417
- return { value: "https://malicious.example.test", unreachable: false };
418
- }
419
- if (name === "cadet:platform_organization_id") {
420
- return { value: "org-123", unreachable: false };
421
- }
422
- return { value: null, unreachable: false };
423
- });
424
- process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
425
-
426
- await retire();
427
-
428
- expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
429
- expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
430
- "Stored platform URL does not match the configured platform URL",
431
- );
432
- expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
433
- });
434
-
435
- test("local retire skips platform unregister when stored organization ID is missing", async () => {
436
- const entry = makeEntry("assistant-1", { name: "Example Assistant" });
437
- writeLockfile([entry]);
438
- readPlatformTokenMock.mockReturnValue("session-token");
439
- getPlatformUrlMock.mockReturnValue("https://platform.example.test");
440
- readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
441
- if (name === "cadet:platform_assistant_id") {
442
- return { value: "platform-assistant-1", unreachable: false };
443
- }
444
- if (name === "cadet:platform_base_url") {
445
- return { value: "https://platform.example.test", unreachable: false };
446
- }
447
- return { value: null, unreachable: false };
448
- });
449
- process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
450
-
451
- await retire();
452
-
453
- expect(loopbackSafeFetchMock).not.toHaveBeenCalled();
454
- expect(consoleWarnSpy.mock.calls.flat().join("\n")).toContain(
455
- "Could not read platform organization ID",
456
- );
457
332
  expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
458
333
  expect(loadAllAssistants()).toEqual([]);
459
334
  });
460
335
 
461
336
  test("local retire uses injected platform token from the app host", async () => {
462
- const entry = makeEntry("assistant-1", { name: "Example Assistant" });
337
+ const entry = makeEntry("assistant-1", {
338
+ name: "Example Assistant",
339
+ cloudAssistantId: "cloud-asst-1",
340
+ cloudOrganizationId: "org-123",
341
+ });
463
342
  writeLockfile([entry]);
464
343
  process.env.CADET_PLATFORM_TOKEN = "host-session-token";
465
- readPlatformTokenMock.mockReturnValue(null);
466
- getPlatformUrlMock.mockReturnValue("https://platform.example.test");
467
- readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
468
- if (name === "cadet:platform_assistant_id") {
469
- return { value: "platform-assistant-1", unreachable: false };
470
- }
471
- if (name === "cadet:platform_base_url") {
472
- return { value: "https://platform.example.test", unreachable: false };
473
- }
474
- if (name === "cadet:platform_organization_id") {
475
- return { value: "org-123", unreachable: false };
476
- }
477
- return { value: null, unreachable: false };
478
- });
344
+ readCloudTokenMock.mockReturnValue(null);
345
+ getCloudUrlMock.mockReturnValue("https://platform.example.test");
479
346
  process.argv = ["bun", "cadet", "retire", "assistant-1", "--yes"];
480
347
 
481
348
  await retire();
482
349
 
483
350
  expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
484
- "https://platform.example.test/v1/assistants/platform-assistant-1/retire/",
351
+ "https://platform.example.test/v1/assistants/cloud-asst-1/retire/",
485
352
  {
486
353
  method: "DELETE",
487
354
  headers: {
@@ -494,23 +361,15 @@ describe("cadet retire", () => {
494
361
  expect(retireLocalMock).toHaveBeenCalledWith("assistant-1", entry);
495
362
  });
496
363
 
497
- test("local retire continues when self-hosted platform unregister fails", async () => {
498
- const entry = makeEntry("assistant-1", { name: "Example Assistant" });
499
- writeLockfile([entry]);
500
- readPlatformTokenMock.mockReturnValue("session-token");
501
- getPlatformUrlMock.mockReturnValue("https://platform.example.test");
502
- readGatewayCredentialMock.mockImplementation(async (_gatewayUrl, name) => {
503
- if (name === "cadet:platform_assistant_id") {
504
- return { value: "platform-assistant-1", unreachable: false };
505
- }
506
- if (name === "cadet:platform_base_url") {
507
- return { value: "https://platform.example.test", unreachable: false };
508
- }
509
- if (name === "cadet:platform_organization_id") {
510
- return { value: "org-123", unreachable: false };
511
- }
512
- return { value: null, unreachable: false };
364
+ test("local retire continues when the cloud unregister fails", async () => {
365
+ const entry = makeEntry("assistant-1", {
366
+ name: "Example Assistant",
367
+ cloudAssistantId: "cloud-asst-1",
368
+ cloudOrganizationId: "org-123",
513
369
  });
370
+ writeLockfile([entry]);
371
+ readCloudTokenMock.mockReturnValue("session-token");
372
+ getCloudUrlMock.mockReturnValue("https://platform.example.test");
514
373
  loopbackSafeFetchMock.mockResolvedValue(
515
374
  new Response("platform unavailable", { status: 503 }),
516
375
  );
@@ -0,0 +1,99 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+ import { z } from "zod";
6
+
7
+ import {
8
+ clearRootProcess,
9
+ readRootProcesses,
10
+ stampRootProcess,
11
+ } from "../lib/assistant-config.js";
12
+
13
+ // Create a temp directory and set CADET_LOCKFILE_DIR so the real lockfile
14
+ // helpers resolve inside it (the sleep.test.ts fixture pattern).
15
+ let testDir: string;
16
+
17
+ beforeEach(() => {
18
+ testDir = mkdtempSync(join(tmpdir(), "cadet-root-process-test-"));
19
+ mkdirSync(join(testDir, "config"), { recursive: true });
20
+ process.env.CADET_LOCKFILE_DIR = join(testDir, "config");
21
+ });
22
+
23
+ afterEach(() => {
24
+ delete process.env.CADET_LOCKFILE_DIR;
25
+ rmSync(testDir, { recursive: true, force: true });
26
+ });
27
+
28
+ describe("root process lockfile entries", () => {
29
+ test("a tunnel record stamps at the root, reads back, and clears", () => {
30
+ stampRootProcess("tunnel", {
31
+ pid: 4242,
32
+ provider: "ngrok",
33
+ startedAt: "2026-10-11T00:00:00.000Z",
34
+ });
35
+
36
+ expect(readRootProcesses().tunnel).toMatchObject({
37
+ pid: 4242,
38
+ provider: "ngrok",
39
+ });
40
+
41
+ clearRootProcess("tunnel");
42
+ expect(readRootProcesses().tunnel).toBeUndefined();
43
+ });
44
+
45
+ test("tunnel and app entries stamp and clear independently", () => {
46
+ stampRootProcess("app", { pid: 77, startedAt: "2026-10-11T00:00:00.000Z" });
47
+ stampRootProcess("tunnel", {
48
+ pid: 78,
49
+ provider: "cloudflare",
50
+ startedAt: "2026-10-11T00:00:00.000Z",
51
+ });
52
+
53
+ expect(readRootProcesses().app?.pid).toBe(77);
54
+ expect(readRootProcesses().tunnel?.provider).toBe("cloudflare");
55
+
56
+ clearRootProcess("app");
57
+ expect(readRootProcesses().app).toBeUndefined();
58
+ // The sibling entry survives.
59
+ expect(readRootProcesses().tunnel?.pid).toBe(78);
60
+ });
61
+
62
+ test("a record can name the assistant it fronts, for `cadet ps` nesting", () => {
63
+ stampRootProcess("tunnel", {
64
+ pid: 4242,
65
+ provider: "ngrok",
66
+ startedAt: "2026-10-11T00:00:00.000Z",
67
+ assistantId: "assistant-1",
68
+ });
69
+
70
+ expect(readRootProcesses().tunnel).toMatchObject({
71
+ pid: 4242,
72
+ provider: "ngrok",
73
+ assistantId: "assistant-1",
74
+ });
75
+ });
76
+
77
+ test("the records live at the lockfile root under their kind keys", () => {
78
+ stampRootProcess("app", { pid: 1, startedAt: "t" });
79
+
80
+ const raw = z
81
+ .object({
82
+ app: z.object({ pid: z.number(), startedAt: z.string() }),
83
+ })
84
+ .parse(
85
+ JSON.parse(
86
+ readFileSync(
87
+ join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
88
+ "utf-8",
89
+ ),
90
+ ),
91
+ );
92
+ expect(raw.app).toEqual({ pid: 1, startedAt: "t" });
93
+ });
94
+
95
+ test("clearing an absent entry is a no-op", () => {
96
+ clearRootProcess("tunnel");
97
+ expect(readRootProcesses()).toEqual({});
98
+ });
99
+ });
@@ -7,6 +7,7 @@ import {
7
7
  spyOn,
8
8
  test,
9
9
  } from "bun:test";
10
+ import type { GuardianTokenData } from "../lib/guardian-token.js";
10
11
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
12
  import { tmpdir } from "node:os";
12
13
  import { join } from "node:path";
@@ -49,10 +50,8 @@ mock.module("../lib/drain.js", () => ({
49
50
 
50
51
  const realGuardianToken = { ...(await import("../lib/guardian-token.js")) };
51
52
  const loadGuardianTokenMock = mock(
52
- (_assistantId: string) =>
53
- ({ accessToken: "test-guardian-token" }) as unknown as ReturnType<
54
- typeof realGuardianToken.loadGuardianToken
55
- >,
53
+ (_assistantId: string): GuardianTokenData | null =>
54
+ makeGuardianTokenData({ accessToken: "test-guardian-token" }),
56
55
  );
57
56
  mock.module("../lib/guardian-token.js", () => ({
58
57
  ...realGuardianToken,
@@ -92,6 +91,7 @@ import {
92
91
  DEFAULT_GATEWAY_PORT,
93
92
  DEFAULT_QDRANT_PORT,
94
93
  } from "../lib/constants.js";
94
+ import { makeGuardianTokenData } from "./helpers/guardian-token.js";
95
95
 
96
96
  // Write a lockfile entry so the real resolveTargetAssistant() finds our test
97
97
  // assistant without needing to mock the entire assistant-config module.
@@ -153,9 +153,9 @@ describe("sleep command", () => {
153
153
  drainAssistantMock.mockReset();
154
154
  drainAssistantMock.mockResolvedValue("drained");
155
155
  loadGuardianTokenMock.mockReset();
156
- loadGuardianTokenMock.mockReturnValue({
157
- accessToken: "test-guardian-token",
158
- } as unknown as ReturnType<typeof realGuardianToken.loadGuardianToken>);
156
+ loadGuardianTokenMock.mockReturnValue(
157
+ makeGuardianTokenData({ accessToken: "test-guardian-token" }),
158
+ );
159
159
  resolveFreshBearerTokenMock.mockReset();
160
160
  resolveFreshBearerTokenMock.mockImplementation(
161
161
  async (_url, _id, bearerToken, _cloud) => bearerToken,
@@ -180,11 +180,11 @@ describe("sleep command", () => {
180
180
  process.argv = ["bun", "cadet", "sleep", "sleep-test"];
181
181
 
182
182
  const consoleError = spyOn(console, "error").mockImplementation(() => {});
183
- const exitMock = mock((code?: number) => {
183
+ const exitMock = mock((code?: number | string | undefined): never => {
184
184
  throw new Error(`process.exit:${code}`);
185
185
  });
186
186
  const originalExit = process.exit;
187
- process.exit = exitMock as unknown as typeof process.exit;
187
+ process.exit = exitMock;
188
188
 
189
189
  try {
190
190
  await expect(sleep()).rejects.toThrow("process.exit:1");
@@ -300,11 +300,11 @@ describe("sleep command", () => {
300
300
  process.argv = ["bun", "cadet", "sleep", "sleep-test", "--wait"];
301
301
 
302
302
  const consoleLog = spyOn(console, "log").mockImplementation(() => {});
303
- const exitMock = mock((code?: number) => {
303
+ const exitMock = mock((code?: number | string | undefined): never => {
304
304
  throw new Error(`process.exit:${code}`);
305
305
  });
306
306
  const originalExit = process.exit;
307
- process.exit = exitMock as unknown as typeof process.exit;
307
+ process.exit = exitMock;
308
308
  try {
309
309
  await expect(sleep()).rejects.toThrow("process.exit:130");
310
310
  } finally {
@@ -350,11 +350,11 @@ describe("sleep command", () => {
350
350
  process.argv = ["bun", "cadet", "sleep", "sleep-test", "--wait=abc"];
351
351
 
352
352
  const consoleError = spyOn(console, "error").mockImplementation(() => {});
353
- const exitMock = mock((code?: number) => {
353
+ const exitMock = mock((code?: number | string | undefined): never => {
354
354
  throw new Error(`process.exit:${code}`);
355
355
  });
356
356
  const originalExit = process.exit;
357
- process.exit = exitMock as unknown as typeof process.exit;
357
+ process.exit = exitMock;
358
358
  try {
359
359
  await expect(sleep()).rejects.toThrow("process.exit:1");
360
360
  expect(consoleError).toHaveBeenCalledWith(
@@ -484,11 +484,11 @@ describe("sleep command", () => {
484
484
  process.argv = ["bun", "cadet", "sleep", "--all"];
485
485
 
486
486
  const consoleError = spyOn(console, "error").mockImplementation(() => {});
487
- const exitMock = mock((code?: number) => {
487
+ const exitMock = mock((code?: number | string | undefined): never => {
488
488
  throw new Error(`process.exit:${code}`);
489
489
  });
490
490
  const originalExit = process.exit;
491
- process.exit = exitMock as unknown as typeof process.exit;
491
+ process.exit = exitMock;
492
492
 
493
493
  try {
494
494
  await expect(sleep()).rejects.toThrow("process.exit:1");
@@ -511,11 +511,11 @@ describe("sleep command", () => {
511
511
  process.argv = ["bun", "cadet", "sleep", "--all"];
512
512
 
513
513
  const consoleError = spyOn(console, "error").mockImplementation(() => {});
514
- const exitMock = mock((code?: number) => {
514
+ const exitMock = mock((code?: number | string | undefined): never => {
515
515
  throw new Error(`process.exit:${code}`);
516
516
  });
517
517
  const originalExit = process.exit;
518
- process.exit = exitMock as unknown as typeof process.exit;
518
+ process.exit = exitMock;
519
519
 
520
520
  try {
521
521
  await expect(sleep()).rejects.toThrow("process.exit:1");
@@ -532,11 +532,11 @@ describe("sleep command", () => {
532
532
  process.argv = ["bun", "cadet", "sleep", "sleep-test", "--all"];
533
533
 
534
534
  const consoleError = spyOn(console, "error").mockImplementation(() => {});
535
- const exitMock = mock((code?: number) => {
535
+ const exitMock = mock((code?: number | string | undefined): never => {
536
536
  throw new Error(`process.exit:${code}`);
537
537
  });
538
538
  const originalExit = process.exit;
539
- process.exit = exitMock as unknown as typeof process.exit;
539
+ process.exit = exitMock;
540
540
 
541
541
  try {
542
542
  await expect(sleep()).rejects.toThrow("process.exit:1");
@@ -35,7 +35,7 @@ describe("getBuilderManagedEnvKeys", () => {
35
35
  expect(always.has(key)).toBe(true);
36
36
  }
37
37
 
38
- expect(always.has("CADET_DISABLE_PLATFORM")).toBe(false);
38
+ expect(always.has("CADET_DISABLE_CLOUD")).toBe(false);
39
39
  expect(always.has("CADET_DEVICE_ID")).toBe(false);
40
40
  });
41
41
 
@@ -60,8 +60,8 @@ describe("getBuilderManagedEnvKeys", () => {
60
60
  a.name.localeCompare(b.name),
61
61
  );
62
62
  expect(sorted).toEqual([
63
+ { name: "CADET_CLOUD_URL", hostVar: "CADET_CLOUD_URL" },
63
64
  { name: "CADET_ENVIRONMENT", hostVar: "CADET_ENVIRONMENT" },
64
- { name: "CADET_PLATFORM_URL", hostVar: "CADET_PLATFORM_URL" },
65
65
  { name: "TUNNEL_BASE_URL", hostVar: "TUNNEL_BASE_URL" },
66
66
  ]);
67
67
  });
@@ -76,8 +76,8 @@ describe("getBuilderManagedEnvKeys", () => {
76
76
  expect(hostForwarded).toContainEqual({ name: envVar, hostVar: envVar });
77
77
  }
78
78
  expect(hostForwarded).toContainEqual({
79
- name: "CADET_PLATFORM_URL",
80
- hostVar: "CADET_PLATFORM_URL",
79
+ name: "CADET_CLOUD_URL",
80
+ hostVar: "CADET_CLOUD_URL",
81
81
  });
82
82
  });
83
83
 
@@ -126,7 +126,7 @@ describe("buildServiceRunArgs extra env routing", () => {
126
126
  gatewayContainer: "test-gateway",
127
127
  network: "test-net",
128
128
  },
129
- extraGatewayEnv: { CADET_DISABLE_PLATFORM: "1" },
129
+ extraGatewayEnv: { CADET_DISABLE_CLOUD: "1" },
130
130
  extraAssistantEnv: { FOO: "bar" },
131
131
  };
132
132
 
@@ -134,19 +134,19 @@ describe("buildServiceRunArgs extra env routing", () => {
134
134
 
135
135
  test("extraGatewayEnv lands only in gateway args", () => {
136
136
  const gatewayArgs = runArgs.gateway();
137
- expect(gatewayArgs).toContain("CADET_DISABLE_PLATFORM=1");
137
+ expect(gatewayArgs).toContain("CADET_DISABLE_CLOUD=1");
138
138
  expect(gatewayArgs).not.toContain("FOO=bar");
139
139
  });
140
140
 
141
141
  test("extraAssistantEnv lands only in assistant args", () => {
142
142
  const assistantArgs = runArgs.assistant();
143
143
  expect(assistantArgs).toContain("FOO=bar");
144
- expect(assistantArgs).not.toContain("CADET_DISABLE_PLATFORM=1");
144
+ expect(assistantArgs).not.toContain("CADET_DISABLE_CLOUD=1");
145
145
  });
146
146
 
147
147
  test("credential-executor args get neither extra env map", () => {
148
148
  const cesArgs = runArgs["credential-executor"]();
149
- expect(cesArgs).not.toContain("CADET_DISABLE_PLATFORM=1");
149
+ expect(cesArgs).not.toContain("CADET_DISABLE_CLOUD=1");
150
150
  expect(cesArgs).not.toContain("FOO=bar");
151
151
  });
152
152