@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
@@ -292,7 +292,7 @@ describe("hatch provider secrets", () => {
292
292
  test("HATCH_PROVIDER_CHOICES only lists labeled providers", () => {
293
293
  for (const provider of HATCH_PROVIDER_CHOICES) {
294
294
  expect(formatProviderName(provider)).toBeTruthy();
295
- // Cadet Cloud is the managed-connection exception: no env var, no
295
+ // Cadet Cloud is the cloud-connection exception: no env var, no
296
296
  // key collection — everything else must have a supported setup flow.
297
297
  if (provider !== "cadet") {
298
298
  expect(isSupportedLlmProvider(provider)).toBe(true);
@@ -313,7 +313,7 @@ describe("hatch provider secrets", () => {
313
313
  ]);
314
314
  });
315
315
 
316
- test("Cadet Cloud configures silently through the managed connection", async () => {
316
+ test("Cadet Cloud configures silently through the cloud connection", async () => {
317
317
  const { calls, fetchImpl } = makeFetch([]);
318
318
  const logs: string[] = [];
319
319
 
@@ -0,0 +1,22 @@
1
+ import type { GuardianTokenData } from "../../lib/guardian-token.js";
2
+
3
+ /**
4
+ * A complete `GuardianTokenData` fixture with per-test overrides — the type's
5
+ * nine fields are all required, so partial fakes need this to stand in.
6
+ */
7
+ export function makeGuardianTokenData(
8
+ over: Partial<GuardianTokenData> = {},
9
+ ): GuardianTokenData {
10
+ return {
11
+ guardianPrincipalId: "principal-1",
12
+ accessToken: "local-token",
13
+ accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
14
+ refreshToken: "refresh-token",
15
+ refreshTokenExpiresAt: new Date(Date.now() + 3_600_000).toISOString(),
16
+ refreshAfter: new Date(Date.now() + 30_000).toISOString(),
17
+ isNew: false,
18
+ deviceId: "device-1",
19
+ leasedAt: new Date().toISOString(),
20
+ ...over,
21
+ };
22
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * bun-types' global fetch carries a `preconnect` member; these mocks only need
3
+ * the call shape, so hand the assignment a member-complete callable. The SUT
4
+ * never calls `preconnect`.
5
+ */
6
+ export function withPreconnect(
7
+ impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
8
+ ): typeof globalThis.fetch {
9
+ return Object.assign(impl, { preconnect: () => {} });
10
+ }
@@ -158,9 +158,9 @@ describe("buildIngressNginxConfig", () => {
158
158
  remoteWebIngress: {
159
159
  webDistDir: "/tmp/cadet web/dist",
160
160
  config: {
161
- mode: "remote-gateway",
161
+ remote: "docker",
162
162
  apiBaseUrl: "/v1",
163
- platformDisabled: true,
163
+ cloudDisabled: true,
164
164
  },
165
165
  },
166
166
  });
@@ -330,7 +330,7 @@ describe("buildIngressNginxConfig", () => {
330
330
  expect(remoteConf).toContain("default_type application/json;");
331
331
  expect(remoteConf).toContain('add_header Cache-Control "no-store";');
332
332
  expect(remoteConf).toContain(
333
- 'return 200 "{\\"mode\\":\\"remote-gateway\\",\\"apiBaseUrl\\":\\"/v1\\",\\"platformDisabled\\":true,\\"disablePlatform\\":true}";',
333
+ 'return 200 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true}";',
334
334
  );
335
335
  expect(remoteConf).not.toContain("assistantId");
336
336
  });
@@ -347,7 +347,7 @@ describe("buildIngressNginxConfig", () => {
347
347
  },
348
348
  });
349
349
  expect(named).toContain(
350
- 'return 200 "{\\"mode\\":\\"remote-gateway\\",\\"apiBaseUrl\\":\\"/v1\\",\\"platformDisabled\\":true,\\"disablePlatform\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
350
+ 'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"cloudDisabled\\":true,\\"disableCloud\\":true,\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
351
351
  );
352
352
  });
353
353
 
@@ -447,19 +447,19 @@ describe("buildRemoteWebIndexHtml", () => {
447
447
  const html =
448
448
  '<html><head><script>window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}</script></head><body></body></html>';
449
449
  const result = buildRemoteWebIndexHtml(html, {
450
- mode: "remote-gateway",
450
+ remote: "docker",
451
451
  apiBaseUrl: "/v1",
452
- disablePlatform: true,
452
+ disableCloud: true,
453
453
  });
454
454
 
455
455
  expect(result).toContain(
456
456
  'window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}',
457
457
  );
458
458
  expect(result).toContain(
459
- 'window.__CADET_CONFIG__={"mode":"remote-gateway","apiBaseUrl":"/v1","disablePlatform":true}',
459
+ 'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1","disableCloud":true}',
460
460
  );
461
461
  expect(result.indexOf('"webUrl"')).toBeLessThan(
462
- result.indexOf('"remote-gateway"'),
462
+ result.indexOf('"docker"'),
463
463
  );
464
464
  });
465
465
 
@@ -484,7 +484,7 @@ describe("buildRemoteWebIndexHtml", () => {
484
484
  "</head><body></body></html>",
485
485
  ].join("\n");
486
486
 
487
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
487
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
488
488
 
489
489
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(4);
490
490
  expect(result).toContain('href="/assistant/assets/a-1.js"');
@@ -506,7 +506,7 @@ describe("buildRemoteWebIndexHtml", () => {
506
506
  "</head><body></body></html>",
507
507
  ].join("\n");
508
508
 
509
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
509
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
510
510
 
511
511
  expect(result).not.toContain("modulepreload");
512
512
  expect(result).not.toContain("/assistant/assets/chunk-0.js");
@@ -522,7 +522,7 @@ describe("buildRemoteWebIndexHtml", () => {
522
522
  );
523
523
  const html = `<html><head>${preloads.join("\n")}</head><body></body></html>`;
524
524
 
525
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
525
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
526
526
 
527
527
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(
528
528
  MAX_PRESERVED_MODULE_PRELOADS,
@@ -862,10 +862,10 @@ function spaConfigHash(
862
862
  JSON.stringify({
863
863
  template: EDGE_TEMPLATE_VERSION,
864
864
  config: {
865
- mode: "remote-gateway",
865
+ remote: "custom",
866
866
  apiBaseUrl: "/v1",
867
- platformDisabled: true,
868
- disablePlatform: true,
867
+ cloudDisabled: true,
868
+ disableCloud: true,
869
869
  ...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
870
870
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
871
871
  hubUrl: PRODUCTION_HUB_URL,
@@ -988,7 +988,7 @@ describe("startRemoteWebIngress", () => {
988
988
  join(ws, "data", "ingress", "assistant-index.html"),
989
989
  "utf-8",
990
990
  );
991
- expect(indexHtml).toContain("remote-gateway");
991
+ expect(indexHtml).toContain('"remote":"custom"');
992
992
  expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
993
993
  // No assistant name was provided, so the served config omits the label.
994
994
  expect(indexHtml).not.toContain("assistantName");
@@ -1352,10 +1352,10 @@ describe("startRemoteWebIngress", () => {
1352
1352
  JSON.stringify({
1353
1353
  template: 1,
1354
1354
  config: {
1355
- mode: "remote-gateway",
1355
+ remote: "custom",
1356
1356
  apiBaseUrl: "/v1",
1357
- platformDisabled: true,
1358
- disablePlatform: true,
1357
+ cloudDisabled: true,
1358
+ disableCloud: true,
1359
1359
  hubUrl: PRODUCTION_HUB_URL,
1360
1360
  },
1361
1361
  }),
@@ -2095,8 +2095,9 @@ describe("ensureTunnelEdge", () => {
2095
2095
  gatewayPort: 7830,
2096
2096
  });
2097
2097
 
2098
- await expect(promise).rejects.toThrow("brew install nginx");
2099
- await expect(promise).rejects.toThrow("apt install nginx");
2098
+ await expect(promise).rejects.toThrow(
2099
+ "See https://nginx.org/en/docs/install.html for how to install",
2100
+ );
2100
2101
  await expect(promise).rejects.toThrow("NGINX_BIN");
2101
2102
  expect(spawnMock).not.toHaveBeenCalled();
2102
2103
  });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildNgrokSpawnArgs, isNgrokAuthError } from "../lib/tunnels/ngrok.js";
4
+
5
+ describe("buildNgrokSpawnArgs", () => {
6
+ test("spawns a plain http agent against the loopback edge", () => {
7
+ expect(buildNgrokSpawnArgs(7830)).toEqual([
8
+ "http",
9
+ "127.0.0.1:7830",
10
+ "--log=stdout",
11
+ ]);
12
+ });
13
+
14
+ test("never passes --config so the default (authtoken-bearing) config loads", () => {
15
+ const args = buildNgrokSpawnArgs(7830, "me.ngrok.app");
16
+ expect(args.some((arg) => arg.startsWith("--config"))).toBe(false);
17
+ });
18
+
19
+ test("binds a reserved domain when given", () => {
20
+ expect(buildNgrokSpawnArgs(7830, "my-assistant.ngrok.app")).toEqual([
21
+ "http",
22
+ "127.0.0.1:7830",
23
+ "--log=stdout",
24
+ "--domain=my-assistant.ngrok.app",
25
+ ]);
26
+ });
27
+ });
28
+
29
+ describe("isNgrokAuthError", () => {
30
+ test("recognizes the free-account authtoken requirement", () => {
31
+ expect(
32
+ isNgrokAuthError(
33
+ "FATAL: authentication failed: Usage of ngrok requires a free authtoken. ERR_NGROK_105",
34
+ ),
35
+ ).toBe(true);
36
+ });
37
+
38
+ test("recognizes an unauthorized token", () => {
39
+ expect(isNgrokAuthError("unauthorized authtoken ERR_NGROK_401")).toBe(true);
40
+ });
41
+
42
+ test("does not claim other startup failures are auth errors", () => {
43
+ expect(
44
+ isNgrokAuthError(
45
+ "creating tunnel session failed: dial tcp 3.219.x.x:443: connection refused",
46
+ ),
47
+ ).toBe(false);
48
+ });
49
+ });
@@ -141,7 +141,7 @@ describe("loadAllAssistantsAcrossEnvs", () => {
141
141
  function makeEnv(name: string, lockfileDir: string): EnvironmentDefinition {
142
142
  return {
143
143
  name,
144
- platformUrl: "https://example.invalid",
144
+ cloudUrl: "https://example.invalid",
145
145
  webUrl: "https://example.invalid",
146
146
  lockfileDirOverride: lockfileDir,
147
147
  };
@@ -20,14 +20,7 @@ const originalEnvironment = process.env.CADET_ENVIRONMENT;
20
20
  process.env.CADET_ENVIRONMENT = "production";
21
21
 
22
22
  import { buildAppConnectUrl, pair } from "../commands/pair.js";
23
-
24
- // bun-types' global fetch carries a `preconnect` member; these mocks only need
25
- // the call shape, so hand the assignment a member-complete callable.
26
- function withPreconnect(
27
- impl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
28
- ): typeof fetch {
29
- return Object.assign(impl, { preconnect: () => {} });
30
- }
23
+ import { withPreconnect } from "./helpers/http.js";
31
24
 
32
25
  // Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
33
26
  const LOCAL_URL = "http://127.0.0.1:7830";
@@ -26,12 +26,16 @@ process.env.CADET_LOCKFILE_DIR = testDir;
26
26
  import * as assistantConfig from "../lib/assistant-config.js";
27
27
  import * as healthCheck from "../lib/health-check.js";
28
28
  import * as orphanDetection from "../lib/orphan-detection.js";
29
- import * as platformClient from "../lib/platform-client.js";
29
+ import * as cloudClient from "../lib/cloud-client.js";
30
30
 
31
31
  const loadAllAssistantsMock = spyOn(
32
32
  assistantConfig,
33
33
  "loadAllAssistants",
34
34
  ).mockReturnValue([]);
35
+ const readRootProcessesMock = spyOn(
36
+ assistantConfig,
37
+ "readRootProcesses",
38
+ ).mockReturnValue({});
35
39
  const getActiveAssistantMock = spyOn(
36
40
  assistantConfig,
37
41
  "getActiveAssistant",
@@ -40,31 +44,31 @@ const detectOrphanedProcessesMock = spyOn(
40
44
  orphanDetection,
41
45
  "detectOrphanedProcesses",
42
46
  ).mockResolvedValue([]);
43
- const getPlatformUrlMock = spyOn(
44
- platformClient,
45
- "getPlatformUrl",
47
+ const getCloudUrlMock = spyOn(
48
+ cloudClient,
49
+ "getCloudUrl",
46
50
  ).mockReturnValue("http://platform.test");
47
51
 
48
- // Per-test toggle for `readPlatformToken`.
49
- const readPlatformTokenMock = spyOn(
50
- platformClient,
51
- "readPlatformToken",
52
+ // Per-test toggle for `readCloudToken`.
53
+ const readCloudTokenMock = spyOn(
54
+ cloudClient,
55
+ "readCloudToken",
52
56
  ).mockReturnValue(null);
53
57
 
54
- // `fetchCurrentUser` + `fetchPlatformAssistants` are spied so we can assert
58
+ // `fetchCurrentUser` + `fetchCloudAssistants` are spied so we can assert
55
59
  // they're never invoked on the no-token path, and re-shaped per-test for the
56
60
  // token-but-unreachable path.
57
61
  const fetchCurrentUserMock = spyOn(
58
- platformClient,
62
+ cloudClient,
59
63
  "fetchCurrentUser",
60
64
  ).mockResolvedValue({
61
65
  id: "u1",
62
66
  email: "test@example.com",
63
67
  display: "Test",
64
68
  });
65
- const fetchPlatformAssistantsMock = spyOn(
66
- platformClient,
67
- "fetchPlatformAssistants",
69
+ const fetchCloudAssistantsMock = spyOn(
70
+ cloudClient,
71
+ "fetchCloudAssistants",
68
72
  ).mockResolvedValue([]);
69
73
  const checkManagedHealthMock = spyOn(
70
74
  healthCheck,
@@ -99,33 +103,36 @@ beforeEach(() => {
99
103
  }) as typeof process.stdout.write;
100
104
  });
101
105
 
106
+ const NL = String.fromCharCode(10);
107
+
102
108
  afterEach(() => {
103
109
  console.log = originalLog;
104
110
  console.error = originalError;
105
111
  process.stdout.write = originalStdoutWrite;
106
112
  loadAllAssistantsMock.mockReturnValue([]);
107
113
  getActiveAssistantMock.mockReturnValue(null);
108
- readPlatformTokenMock.mockReturnValue(null);
114
+ readCloudTokenMock.mockReturnValue(null);
109
115
  fetchCurrentUserMock.mockReset();
110
116
  fetchCurrentUserMock.mockResolvedValue({
111
117
  id: "u1",
112
118
  email: "test@example.com",
113
119
  display: "Test",
114
120
  });
115
- fetchPlatformAssistantsMock.mockReset();
116
- fetchPlatformAssistantsMock.mockResolvedValue([]);
121
+ fetchCloudAssistantsMock.mockReset();
122
+ fetchCloudAssistantsMock.mockResolvedValue([]);
117
123
  checkManagedHealthMock.mockReset();
118
124
  checkManagedHealthMock.mockResolvedValue({ status: "healthy", detail: null });
125
+ readRootProcessesMock.mockReturnValue({});
119
126
  });
120
127
 
121
128
  afterAll(() => {
122
129
  loadAllAssistantsMock.mockRestore();
123
130
  getActiveAssistantMock.mockRestore();
124
131
  detectOrphanedProcessesMock.mockRestore();
125
- getPlatformUrlMock.mockRestore();
126
- readPlatformTokenMock.mockRestore();
132
+ getCloudUrlMock.mockRestore();
133
+ readCloudTokenMock.mockRestore();
127
134
  fetchCurrentUserMock.mockRestore();
128
- fetchPlatformAssistantsMock.mockRestore();
135
+ fetchCloudAssistantsMock.mockRestore();
129
136
  checkManagedHealthMock.mockRestore();
130
137
  rmSync(testDir, { recursive: true, force: true });
131
138
  });
@@ -142,7 +149,7 @@ import { listAllAssistants } from "../commands/ps.js";
142
149
 
143
150
  describe("cadet ps — platform status line", () => {
144
151
  test("no local token: no login-status line, skips ALL network fetches", async () => {
145
- readPlatformTokenMock.mockReturnValue(null);
152
+ readCloudTokenMock.mockReturnValue(null);
146
153
 
147
154
  await listAllAssistants(false);
148
155
 
@@ -155,20 +162,20 @@ describe("cadet ps — platform status line", () => {
155
162
  false,
156
163
  );
157
164
 
158
- // Structural guarantee: we never even tried to talk to the platform.
165
+ // Structural guarantee: we never even tried to talk to the cloud.
159
166
  expect(fetchCurrentUserMock).not.toHaveBeenCalled();
160
- expect(fetchPlatformAssistantsMock).not.toHaveBeenCalled();
167
+ expect(fetchCloudAssistantsMock).not.toHaveBeenCalled();
161
168
  });
162
169
 
163
170
  test("local token present but platform is unreachable: no status line, no leaked org-fetch error", async () => {
164
- readPlatformTokenMock.mockReturnValue("session_abc123");
171
+ readCloudTokenMock.mockReturnValue("session_abc123");
165
172
  // Simulate the exact Bun connect failure the user reported:
166
173
  // "Unable to connect. Is the computer able to access the url?"
167
174
  const connectError = new Error(
168
175
  "Unable to connect. Is the computer able to access the url?",
169
176
  );
170
177
  fetchCurrentUserMock.mockRejectedValue(connectError);
171
- fetchPlatformAssistantsMock.mockRejectedValue(connectError);
178
+ fetchCloudAssistantsMock.mockRejectedValue(connectError);
172
179
 
173
180
  await listAllAssistants(false);
174
181
 
@@ -183,13 +190,13 @@ describe("cadet ps — platform status line", () => {
183
190
  });
184
191
 
185
192
  test("local token present and platform reachable: no login-status line", async () => {
186
- readPlatformTokenMock.mockReturnValue("session_abc123");
193
+ readCloudTokenMock.mockReturnValue("session_abc123");
187
194
  fetchCurrentUserMock.mockResolvedValue({
188
195
  id: "u1",
189
196
  email: "user@example.com",
190
197
  display: "Example User",
191
198
  });
192
- fetchPlatformAssistantsMock.mockResolvedValue([]);
199
+ fetchCloudAssistantsMock.mockResolvedValue([]);
193
200
 
194
201
  await listAllAssistants(false);
195
202
 
@@ -217,6 +224,52 @@ describe("cadet ps — platform status line", () => {
217
224
  }
218
225
  });
219
226
 
227
+ test("CLI-managed root processes nest under the assistant they front", async () => {
228
+ loadAllAssistantsMock.mockReturnValue([
229
+ {
230
+ assistantId: "assistant-123",
231
+ name: "Alice",
232
+ runtimeUrl: "https://platform.example",
233
+ cloud: "cadet",
234
+ },
235
+ {
236
+ assistantId: "assistant-456",
237
+ name: "Bob",
238
+ runtimeUrl: "https://platform.example/b",
239
+ cloud: "cadet",
240
+ },
241
+ ]);
242
+ getActiveAssistantMock.mockReturnValue(null);
243
+ checkManagedHealthMock.mockImplementation(async () => ({
244
+ status: "healthy",
245
+ detail: null,
246
+ }));
247
+ // The tunnel fronts Alice (recorded id); the edge records no assistant.
248
+ readRootProcessesMock.mockReturnValue({
249
+ tunnel: {
250
+ pid: 4242,
251
+ provider: "ngrok",
252
+ startedAt: "2026-10-11T00:00:00.000Z",
253
+ assistantId: "assistant-123",
254
+ },
255
+ app: { pid: 77, startedAt: "2026-10-11T00:00:00.000Z" },
256
+ });
257
+
258
+ await listAllAssistants(false);
259
+
260
+ const output = stdout.join(NL);
261
+ // The tunnel nests under Alice — its name is just the provider now…
262
+ expect(output).toContain("└ ngrok");
263
+ // …the unattributed edge renders machine-level…
264
+ expect(output).toContain("nginx edge");
265
+ // …the type column distinguishes the three process kinds…
266
+ expect(output).toContain("🧑‍🚀 Assistant");
267
+ expect(output).toContain("🚇 Tunnel");
268
+ expect(output).toContain("🌐 App");
269
+ // …and the second table is gone: one header row only.
270
+ expect(output.match(/NAME/g)?.length).toBe(1);
271
+ });
272
+
220
273
  test("assistant rows use display name as primary label and keep id visible", async () => {
221
274
  loadAllAssistantsMock.mockReturnValue([
222
275
  {
@@ -0,0 +1,79 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { displayWidth, formatRow } from "../commands/ps.js";
4
+
5
+ describe("displayWidth", () => {
6
+ test("ascii strings count one cell per character", () => {
7
+ expect(displayWidth("healthy")).toBe(7);
8
+ expect(displayWidth("")).toBe(0);
9
+ });
10
+
11
+ test("emoji count two cells", () => {
12
+ expect(displayWidth("🟢")).toBe(2);
13
+ expect(displayWidth("🚀")).toBe(2);
14
+ });
15
+
16
+ test("the ZWJ astronaut sequence counts two cells, not its UTF-16 length", () => {
17
+ // "🧑‍🚀" is 5 UTF-16 units but renders 2 cells wide.
18
+ expect(displayWidth("🧑‍🚀 Assistant")).toBe(12);
19
+ });
20
+
21
+ test("a variation selector widens a narrow symbol to two cells", () => {
22
+ expect(displayWidth("⚠️ warn")).toBe(7);
23
+ });
24
+ });
25
+
26
+ describe("ps table alignment", () => {
27
+ const colWidths = { name: 15, type: 14, status: 10, info: 0 };
28
+
29
+ /** The terminal column where `marker` starts (string indices shift under
30
+ * emoji, which render 2 cells but span 2 UTF-16 units or more). */
31
+ const columnOf = (s: string, marker: string): number =>
32
+ displayWidth(s.slice(0, s.indexOf(marker)));
33
+
34
+ test("emoji cells keep every later column aligned", () => {
35
+ const emojiRow = formatRow(
36
+ {
37
+ name: "slim-yak",
38
+ type: "🧑‍🚀 Assistant",
39
+ status: "🟢 healthy",
40
+ info: "http://x",
41
+ },
42
+ colWidths,
43
+ );
44
+ const asciiRow = formatRow(
45
+ {
46
+ name: "plain",
47
+ type: "Assistant",
48
+ status: "healthy",
49
+ info: "http://y",
50
+ },
51
+ colWidths,
52
+ );
53
+ expect(columnOf(emojiRow, "http://x")).toBe(columnOf(asciiRow, "http://y"));
54
+ });
55
+
56
+ test("the header separator matches the emoji row's column starts", () => {
57
+ const widths = {
58
+ name: displayWidth("slim-yak-yhpdq6"),
59
+ type: displayWidth("🧑‍🚀 Assistant"),
60
+ status: displayWidth("🟢 healthy"),
61
+ info: 0,
62
+ };
63
+ const header = formatRow(
64
+ { name: "NAME", type: "TYPE", status: "STATUS", info: "INFO" },
65
+ widths,
66
+ );
67
+ const row = formatRow(
68
+ {
69
+ name: "slim-yak-yhpdq6",
70
+ type: "🧑‍🚀 Assistant",
71
+ status: "🟢 healthy",
72
+ info: "http://127.0.0.1:7830",
73
+ },
74
+ widths,
75
+ );
76
+ expect(columnOf(header, "TYPE")).toBe(columnOf(row, "🧑‍🚀"));
77
+ expect(columnOf(header, "STATUS")).toBe(columnOf(row, "🟢"));
78
+ });
79
+ });