@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
@@ -61,7 +61,7 @@ afterAll(() => {
61
61
  mock.module("../lib/step-runner.js", () => ({ exec: realExec }));
62
62
  });
63
63
 
64
- import { recover } from "../commands/recover.js";
64
+ import { restore } from "../commands/restore.js";
65
65
 
66
66
  // ── Test fixtures ─────────────────────────────────────────────────────────────
67
67
 
@@ -175,18 +175,19 @@ afterAll(() => {
175
175
 
176
176
  // ── Tests ─────────────────────────────────────────────────────────────────────
177
177
 
178
- describe("recover --help", () => {
178
+ describe("restore --help", () => {
179
179
  test("prints usage, description, and examples then exits 0", async () => {
180
- process.argv = ["bun", "cadet", "recover", "--help"];
181
- await expect(recover()).rejects.toThrow("process.exit(0)");
180
+ process.argv = ["bun", "cadet", "restore", "--help"];
181
+ await expect(restore()).rejects.toThrow("process.exit(0)");
182
182
  const output = consoleLogSpy.mock.calls.flat().join("\n");
183
- expect(output).toContain("Usage: cadet recover <name>");
183
+ expect(output).toContain("Usage: cadet restore <name>");
184
+ expect(output).toContain("recovers a retired assistant");
184
185
  expect(output).toContain("Examples:");
185
- expect(output).toContain("cadet recover");
186
+ expect(output).toContain("cadet restore");
186
187
  });
187
188
  });
188
189
 
189
- describe("recover error cases", () => {
190
+ describe("restore error cases", () => {
190
191
  for (const failure of ["missing", "conflict"] as const) {
191
192
  test(`recovery ${failure} leaves destination and registry untouched and permits retry`, async () => {
192
193
  const name = `retry-${failure}`;
@@ -211,8 +212,8 @@ describe("recover error cases", () => {
211
212
  }
212
213
  const registry = resolveLockfilePaths(process.env)[0]!;
213
214
  const before = readFileSync(registry, "utf8");
214
- process.argv = ["bun", "cadet", "recover", name];
215
- await expect(recover()).rejects.toThrow(`signing key ${failure}`);
215
+ process.argv = ["bun", "cadet", "restore", name];
216
+ await expect(restore()).rejects.toThrow(`signing key ${failure}`);
216
217
  expect(existsSync(instanceDir)).toBe(false);
217
218
  expect(readFileSync(registry, "utf8")).toBe(before);
218
219
  expect(existsSync(archivePath)).toBe(true);
@@ -222,7 +223,7 @@ describe("recover error cases", () => {
222
223
  // Restore the authoritative identity in the archive metadata and retry.
223
224
  entry.resources!.signingKey = "ab".repeat(32);
224
225
  writeFileSync(metadataPath, JSON.stringify(entry));
225
- await recover();
226
+ await restore();
226
227
  expect(existsSync(instanceDir)).toBe(true);
227
228
  expect(existsSync(archivePath)).toBe(false);
228
229
  expect(startGatewayMock).toHaveBeenCalledTimes(1);
@@ -241,8 +242,8 @@ describe("recover error cases", () => {
241
242
  join(dir, "actor-token-signing-key"),
242
243
  Buffer.from("cd".repeat(32), "hex"),
243
244
  );
244
- process.argv = ["bun", "cadet", "recover", name];
245
- await recover();
245
+ process.argv = ["bun", "cadet", "restore", name];
246
+ await restore();
246
247
  expect(startGatewayMock).toHaveBeenCalledWith(
247
248
  false,
248
249
  expect.objectContaining({ signingKey: "cd".repeat(32) }),
@@ -256,8 +257,8 @@ describe("recover error cases", () => {
256
257
  const entry = makeEntry(name, join(testDir, "cadet", "assistants", name));
257
258
  delete entry.resources!.signingKey;
258
259
  const { archivePath, metadataPath } = writeArchiveFixtures(name, entry);
259
- process.argv = ["bun", "cadet", "recover", name];
260
- await expect(recover()).rejects.toThrow("signing key missing");
260
+ process.argv = ["bun", "cadet", "restore", name];
261
+ await expect(restore()).rejects.toThrow("signing key missing");
261
262
  expect(existsSync(archivePath)).toBe(true);
262
263
  expect(existsSync(metadataPath)).toBe(true);
263
264
  expect(startLocalDaemonMock).not.toHaveBeenCalled();
@@ -265,14 +266,14 @@ describe("recover error cases", () => {
265
266
  });
266
267
 
267
268
  test("exits 1 when no name is given", async () => {
268
- process.argv = ["bun", "cadet", "recover"];
269
- await expect(recover()).rejects.toThrow("process.exit(1)");
270
- expect(consoleErrorSpy.mock.calls[0][0]).toContain("Usage:");
269
+ process.argv = ["bun", "cadet", "restore"];
270
+ await expect(restore()).rejects.toThrow("process.exit(1)");
271
+ expect(consoleErrorSpy.mock.calls[0][0]).toContain("<name> is required");
271
272
  });
272
273
 
273
274
  test("exits 1 when archive is missing", async () => {
274
- process.argv = ["bun", "cadet", "recover", "ghost-assistant"];
275
- await expect(recover()).rejects.toThrow("process.exit(1)");
275
+ process.argv = ["bun", "cadet", "restore", "ghost-assistant"];
276
+ await expect(restore()).rejects.toThrow("process.exit(1)");
276
277
  expect(consoleErrorSpy.mock.calls[0][0]).toContain(
277
278
  "No retired archive found for 'ghost-assistant'",
278
279
  );
@@ -292,8 +293,8 @@ describe("recover error cases", () => {
292
293
  join(retiredDir, `${name}.json`),
293
294
  JSON.stringify(entry, null, 2) + "\n",
294
295
  );
295
- process.argv = ["bun", "cadet", "recover", name];
296
- await expect(recover()).rejects.toThrow("missing resource configuration");
296
+ process.argv = ["bun", "cadet", "restore", name];
297
+ await expect(restore()).rejects.toThrow("missing resource configuration");
297
298
  });
298
299
 
299
300
  test("exits 1 when target .cadet/ already exists", async () => {
@@ -303,13 +304,13 @@ describe("recover error cases", () => {
303
304
  writeArchiveFixtures(name, entry);
304
305
  // Pre-create the collision path that recover checks
305
306
  mkdirSync(join(instanceDir, ".cadet"), { recursive: true });
306
- process.argv = ["bun", "cadet", "recover", name];
307
- await expect(recover()).rejects.toThrow("process.exit(1)");
307
+ process.argv = ["bun", "cadet", "restore", name];
308
+ await expect(restore()).rejects.toThrow("process.exit(1)");
308
309
  expect(consoleErrorSpy.mock.calls[0][0]).toContain("already exists");
309
310
  });
310
311
  });
311
312
 
312
- describe("recover extraction path — named instance (instanceDir !== homedir())", () => {
313
+ describe("archive-recovery extraction path — named instance (instanceDir !== homedir())", () => {
313
314
  test("extracts to retiredDir and renames staging dir to instanceDir directly", async () => {
314
315
  const name = "named-instance";
315
316
  const instanceDir = join(testDir, "custom-location", name);
@@ -321,8 +322,8 @@ describe("recover extraction path — named instance (instanceDir !== homedir())
321
322
  // Parent dir must exist for renameSync
322
323
  mkdirSync(join(testDir, "custom-location"), { recursive: true });
323
324
 
324
- process.argv = ["bun", "cadet", "recover", name];
325
- await recover();
325
+ process.argv = ["bun", "cadet", "restore", name];
326
+ await restore();
326
327
 
327
328
  // exec must have been called with -C retiredDir
328
329
  expect(execMock).toHaveBeenCalledTimes(1);
@@ -355,8 +356,8 @@ describe("recover extraction path — named instance (instanceDir !== homedir())
355
356
  const entry = makeEntry(name, instanceDir);
356
357
  const { archivePath, extractedPath } = writeArchiveFixtures(name, entry);
357
358
 
358
- process.argv = ["bun", "cadet", "recover", name];
359
- await recover();
359
+ process.argv = ["bun", "cadet", "restore", name];
360
+ await restore();
360
361
 
361
362
  expect(existsSync(extractedPath)).toBe(false);
362
363
  expect(existsSync(instanceDir)).toBe(true);
@@ -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
+ });