@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64

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 (185) 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 +6 -4
  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 +1 -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 +15 -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 +149 -85
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -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 +180 -211
  90. package/src/__tests__/tunnel.test.ts +490 -270
  91. package/src/__tests__/uninstall.test.ts +150 -9
  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 -78
  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 +27 -24
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/pair.ts +140 -37
  107. package/src/commands/ps.ts +232 -86
  108. package/src/commands/restart.ts +30 -0
  109. package/src/commands/restore.ts +140 -22
  110. package/src/commands/retire.ts +48 -150
  111. package/src/commands/rollback.ts +20 -26
  112. package/src/commands/setup.ts +2 -2
  113. package/src/commands/sleep.ts +2 -2
  114. package/src/commands/teleport.ts +110 -129
  115. package/src/commands/terminal.ts +158 -18
  116. package/src/commands/tunnel.ts +156 -175
  117. package/src/commands/uninstall.ts +137 -54
  118. package/src/commands/unpair.ts +3 -3
  119. package/src/commands/upgrade.ts +30 -54
  120. package/src/commands/use.ts +1 -1
  121. package/src/commands/wake.ts +166 -50
  122. package/src/index.ts +18 -9
  123. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  124. package/src/lib/__tests__/docker.test.ts +168 -20
  125. package/src/lib/__tests__/job-polling.test.ts +2 -2
  126. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  127. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  128. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  129. package/src/lib/assistant-api-key-resolution.ts +5 -5
  130. package/src/lib/assistant-client.ts +8 -8
  131. package/src/lib/assistant-config.ts +58 -12
  132. package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
  133. package/src/lib/cloud-login.ts +23 -24
  134. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  135. package/src/lib/cloud-values.ts +3 -0
  136. package/src/lib/color.ts +4 -0
  137. package/src/lib/config-utils.ts +1 -1
  138. package/src/lib/detached-process.ts +1 -1
  139. package/src/lib/docker.ts +300 -111
  140. package/src/lib/ensure-binary.ts +171 -0
  141. package/src/lib/environments/__tests__/paths.test.ts +29 -3
  142. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  143. package/src/lib/environments/paths.ts +16 -8
  144. package/src/lib/environments/resolve.ts +10 -12
  145. package/src/lib/flag-args.test.ts +1 -1
  146. package/src/lib/flag-args.ts +1 -1
  147. package/src/lib/guardian-token.ts +37 -5
  148. package/src/lib/hatch-local.ts +32 -29
  149. package/src/lib/hatch-next-steps.ts +5 -9
  150. package/src/lib/health-check.ts +6 -6
  151. package/src/lib/ingress-config.ts +8 -35
  152. package/src/lib/job-polling.ts +2 -2
  153. package/src/lib/live-voice/connection.ts +12 -12
  154. package/src/lib/live-voice/tunnel.test.ts +11 -11
  155. package/src/lib/live-voice/tunnel.ts +15 -15
  156. package/src/lib/local-runtime-client.ts +20 -20
  157. package/src/lib/local.ts +46 -34
  158. package/src/lib/nginx-ingress.ts +40 -15
  159. package/src/lib/pair.ts +1 -1
  160. package/src/lib/process.ts +1 -1
  161. package/src/lib/provider-secrets.ts +3 -3
  162. package/src/lib/retire-apple-container.ts +1 -1
  163. package/src/lib/runtime-url.ts +8 -8
  164. package/src/lib/statefulset.ts +2 -2
  165. package/src/lib/step-runner.ts +6 -1
  166. package/src/lib/sync-cloud-assistants.ts +19 -19
  167. package/src/lib/teleport-backup.ts +4 -4
  168. package/src/lib/terminal-client.ts +17 -17
  169. package/src/lib/terminal-session.ts +24 -24
  170. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  171. package/src/lib/tunnels/cadet.ts +29 -0
  172. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  173. package/src/lib/tunnels/gateway-notify.ts +61 -0
  174. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  175. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  176. package/src/lib/upgrade-lifecycle.ts +8 -21
  177. package/src/lib/upgrade-preflight.ts +2 -2
  178. package/src/lib/workos-pkce.ts +5 -5
  179. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  180. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  181. package/src/commands/recover.ts +0 -131
  182. package/src/commands/roadmap.ts +0 -459
  183. package/src/commands/ssh.ts +0 -144
  184. package/src/commands/workflows.ts +0 -305
  185. 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,8 @@ 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,
164
163
  },
165
164
  },
166
165
  });
@@ -330,7 +329,7 @@ describe("buildIngressNginxConfig", () => {
330
329
  expect(remoteConf).toContain("default_type application/json;");
331
330
  expect(remoteConf).toContain('add_header Cache-Control "no-store";');
332
331
  expect(remoteConf).toContain(
333
- 'return 200 "{\\"mode\\":\\"remote-gateway\\",\\"apiBaseUrl\\":\\"/v1\\",\\"platformDisabled\\":true,\\"disablePlatform\\":true}";',
332
+ 'return 200 "{\\"remote\\":\\"docker\\",\\"apiBaseUrl\\":\\"/v1\\"}";',
334
333
  );
335
334
  expect(remoteConf).not.toContain("assistantId");
336
335
  });
@@ -347,7 +346,7 @@ describe("buildIngressNginxConfig", () => {
347
346
  },
348
347
  });
349
348
  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\\"}";',
349
+ 'return 200 "{\\"remote\\":\\"custom\\",\\"apiBaseUrl\\":\\"/v1\\",\\"assistantName\\":\\"Homelab\\",\\"assistantId\\":\\"assistant-1\\",\\"hubUrl\\":\\"https://mycadet.ai/assistant\\"}";',
351
350
  );
352
351
  });
353
352
 
@@ -447,19 +446,18 @@ describe("buildRemoteWebIndexHtml", () => {
447
446
  const html =
448
447
  '<html><head><script>window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}</script></head><body></body></html>';
449
448
  const result = buildRemoteWebIndexHtml(html, {
450
- mode: "remote-gateway",
449
+ remote: "docker",
451
450
  apiBaseUrl: "/v1",
452
- disablePlatform: true,
453
451
  });
454
452
 
455
453
  expect(result).toContain(
456
454
  'window.__CADET_CONFIG__={"webUrl":"https://mycadet.ai"}',
457
455
  );
458
456
  expect(result).toContain(
459
- 'window.__CADET_CONFIG__={"mode":"remote-gateway","apiBaseUrl":"/v1","disablePlatform":true}',
457
+ 'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1"}',
460
458
  );
461
459
  expect(result.indexOf('"webUrl"')).toBeLessThan(
462
- result.indexOf('"remote-gateway"'),
460
+ result.indexOf('"docker"'),
463
461
  );
464
462
  });
465
463
 
@@ -484,7 +482,7 @@ describe("buildRemoteWebIndexHtml", () => {
484
482
  "</head><body></body></html>",
485
483
  ].join("\n");
486
484
 
487
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
485
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
488
486
 
489
487
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(4);
490
488
  expect(result).toContain('href="/assistant/assets/a-1.js"');
@@ -506,7 +504,7 @@ describe("buildRemoteWebIndexHtml", () => {
506
504
  "</head><body></body></html>",
507
505
  ].join("\n");
508
506
 
509
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
507
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
510
508
 
511
509
  expect(result).not.toContain("modulepreload");
512
510
  expect(result).not.toContain("/assistant/assets/chunk-0.js");
@@ -522,7 +520,7 @@ describe("buildRemoteWebIndexHtml", () => {
522
520
  );
523
521
  const html = `<html><head>${preloads.join("\n")}</head><body></body></html>`;
524
522
 
525
- const result = buildRemoteWebIndexHtml(html, { mode: "remote-gateway" });
523
+ const result = buildRemoteWebIndexHtml(html, { remote: "docker" });
526
524
 
527
525
  expect(result.match(/rel="modulepreload"/g)).toHaveLength(
528
526
  MAX_PRESERVED_MODULE_PRELOADS,
@@ -862,10 +860,8 @@ function spaConfigHash(
862
860
  JSON.stringify({
863
861
  template: EDGE_TEMPLATE_VERSION,
864
862
  config: {
865
- mode: "remote-gateway",
863
+ remote: "custom",
866
864
  apiBaseUrl: "/v1",
867
- platformDisabled: true,
868
- disablePlatform: true,
869
865
  ...(opts.assistantName ? { assistantName: opts.assistantName } : {}),
870
866
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
871
867
  hubUrl: PRODUCTION_HUB_URL,
@@ -988,7 +984,7 @@ describe("startRemoteWebIngress", () => {
988
984
  join(ws, "data", "ingress", "assistant-index.html"),
989
985
  "utf-8",
990
986
  );
991
- expect(indexHtml).toContain("remote-gateway");
987
+ expect(indexHtml).toContain('"remote":"custom"');
992
988
  expect(indexHtml).toContain(`"hubUrl":"${PRODUCTION_HUB_URL}"`);
993
989
  // No assistant name was provided, so the served config omits the label.
994
990
  expect(indexHtml).not.toContain("assistantName");
@@ -1352,10 +1348,8 @@ describe("startRemoteWebIngress", () => {
1352
1348
  JSON.stringify({
1353
1349
  template: 1,
1354
1350
  config: {
1355
- mode: "remote-gateway",
1351
+ remote: "custom",
1356
1352
  apiBaseUrl: "/v1",
1357
- platformDisabled: true,
1358
- disablePlatform: true,
1359
1353
  hubUrl: PRODUCTION_HUB_URL,
1360
1354
  },
1361
1355
  }),
@@ -2095,8 +2089,9 @@ describe("ensureTunnelEdge", () => {
2095
2089
  gatewayPort: 7830,
2096
2090
  });
2097
2091
 
2098
- await expect(promise).rejects.toThrow("brew install nginx");
2099
- await expect(promise).rejects.toThrow("apt install nginx");
2092
+ await expect(promise).rejects.toThrow(
2093
+ "See https://nginx.org/en/docs/install.html for how to install",
2094
+ );
2100
2095
  await expect(promise).rejects.toThrow("NGINX_BIN");
2101
2096
  expect(spawnMock).not.toHaveBeenCalled();
2102
2097
  });
@@ -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
  };
@@ -4,6 +4,7 @@ import {
4
4
  beforeEach,
5
5
  describe,
6
6
  expect,
7
+ mock,
7
8
  spyOn,
8
9
  test,
9
10
  } from "bun:test";
@@ -19,15 +20,35 @@ process.env.CADET_LOCKFILE_DIR = testDir;
19
20
  const originalEnvironment = process.env.CADET_ENVIRONMENT;
20
21
  process.env.CADET_ENVIRONMENT = "production";
21
22
 
22
- import { buildAppConnectUrl, pair } from "../commands/pair.js";
23
+ import { withPreconnect } from "./helpers/http.js";
24
+
25
+ // The gateway-CLI mint runs in a SUBPROCESS for local and docker assistants;
26
+ // answer it in-process and record the invocations (the fetch stubs below
27
+ // still serve the loopback HTTP mint the other topologies keep using).
28
+ const mintCalls: Array<{ command: string; args: readonly string[] }> = [];
29
+ const actualStepRunner = await import("../lib/step-runner.js");
30
+ const mockExecOutput = mock(
31
+ (command: string, args: readonly string[]): Promise<string> => {
32
+ mintCalls.push({ command, args });
33
+ const mintedUrl = args[args.length - 1] ?? "";
34
+ return Promise.resolve(
35
+ JSON.stringify({
36
+ deviceCode: "device-code",
37
+ userCode: "ABCD-EFGH",
38
+ verificationUri: `${mintedUrl}/assistant/pair`,
39
+ expiresAt: "2026-06-04T00:10:00.000Z",
40
+ expiresInSeconds: 600,
41
+ intervalSeconds: 5,
42
+ }),
43
+ );
44
+ },
45
+ );
46
+ mock.module("../lib/step-runner.js", () => ({
47
+ ...actualStepRunner,
48
+ execOutput: mockExecOutput,
49
+ }));
23
50
 
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
- }
51
+ const { buildAppConnectUrl, pair } = await import("../commands/pair.js");
31
52
 
32
53
  // Distinct loopback (mint) vs reachable (advertised) URLs to verify the split.
33
54
  const LOCAL_URL = "http://127.0.0.1:7830";
@@ -100,6 +121,7 @@ const ORIGINAL_ARGV = [...process.argv];
100
121
 
101
122
  describe("pair command", () => {
102
123
  beforeEach(() => {
124
+ mintCalls.length = 0;
103
125
  writeLockfile();
104
126
  });
105
127
 
@@ -137,18 +159,16 @@ describe("pair command", () => {
137
159
  globalThis.fetch = origFetch;
138
160
  }
139
161
 
140
- // Mint over the loopback gateway, then approve: running this command on
141
- // the host IS the approval, so the link alone completes the pairing.
142
- expect(calls.map((c) => c[0])).toEqual([
143
- `${LOCAL_URL}/v1/remote-web/pairing-challenge`,
144
- `${LOCAL_URL}/v1/remote-web/pairing-verification`,
162
+ // Mint through the gateway CLI subprocess: process execution IS the
163
+ // local-presence proof and the approval, so the link alone completes
164
+ // the pairing — no HTTP at all for a local assistant.
165
+ expect(mintCalls).toHaveLength(1);
166
+ expect(mintCalls[0]?.args.slice(-3)).toEqual([
167
+ "pairing",
168
+ "mint",
169
+ PUBLIC_URL,
145
170
  ]);
146
- expect(JSON.parse(String(calls[0][1]?.body))).toEqual({
147
- publicBaseUrl: PUBLIC_URL,
148
- });
149
- expect(JSON.parse(String(calls[1][1]?.body))).toEqual({
150
- userCode: "ABCD-EFGH",
151
- });
171
+ expect(calls).toHaveLength(0);
152
172
 
153
173
  const output = logs.join("\n");
154
174
  // One artifact, two renderings: the link and the same link as a QR.
@@ -398,10 +418,9 @@ describe("pair command", () => {
398
418
  }
399
419
 
400
420
  // Resolution succeeded (no exit), so the challenge was minted.
401
- expect(calls.map((c) => c[0])).toEqual([
402
- `${LOCAL_URL}/v1/remote-web/pairing-challenge`,
403
- `${LOCAL_URL}/v1/remote-web/pairing-verification`,
404
- ]);
421
+ expect(mintCalls).toHaveLength(1);
422
+ expect(mintCalls[0]?.args.at(-1)).toBe(PUBLIC_URL);
423
+ expect(calls).toHaveLength(0);
405
424
  });
406
425
 
407
426
  test("rejects an unknown --flag before any network call", async () => {
@@ -594,11 +613,8 @@ describe("pair command", () => {
594
613
  globalThis.fetch = origFetch;
595
614
  }
596
615
 
597
- // Mint still over loopback; the link advertises the override.
598
- expect(calls[0][0]).toBe(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
599
- expect(JSON.parse(String(calls[0][1]?.body))).toEqual({
600
- publicBaseUrl: OVERRIDE,
601
- });
616
+ // Mint still through the gateway CLI; the link advertises the override.
617
+ expect(mintCalls[0]?.args.at(-1)).toBe(OVERRIDE);
602
618
  expect(JSON.parse(logs.join("\n")).pairUrl).toBe(
603
619
  `${OVERRIDE}/assistant/pair#device_code=device-code`,
604
620
  );
@@ -1076,35 +1092,8 @@ describe("pair command", () => {
1076
1092
 
1077
1093
  const calls: string[] = [];
1078
1094
  const origFetch = globalThis.fetch;
1079
- globalThis.fetch = withPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
1080
- const url = String(input);
1081
- calls.push(url);
1082
- if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
1083
- // The minted challenge must advertise the entry's recorded URL.
1084
- expect(JSON.parse(String(init?.body))).toEqual({
1085
- publicBaseUrl: "https://saved.example.ts.net",
1086
- });
1087
- return new Response(
1088
- JSON.stringify({
1089
- deviceCode: "device-code",
1090
- userCode: "ABCD-EFGH",
1091
- verificationUri: "https://saved.example.ts.net/assistant/pair",
1092
- expiresAt: "2026-06-04T00:10:00.000Z",
1093
- expiresInSeconds: 600,
1094
- }),
1095
- { status: 200, headers: { "content-type": "application/json" } },
1096
- );
1097
- }
1098
- if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
1099
- return new Response(
1100
- JSON.stringify({
1101
- status: "approved",
1102
- verificationUri: "https://saved.example.ts.net/assistant/pair",
1103
- expiresAt: "2026-06-04T00:10:00.000Z",
1104
- }),
1105
- { status: 200, headers: { "content-type": "application/json" } },
1106
- );
1107
- }
1095
+ globalThis.fetch = withPreconnect(async (input: RequestInfo | URL) => {
1096
+ calls.push(String(input));
1108
1097
  return new Response("not found", { status: 404 });
1109
1098
  });
1110
1099
 
@@ -1123,7 +1112,9 @@ describe("pair command", () => {
1123
1112
  globalThis.fetch = origFetch;
1124
1113
  }
1125
1114
 
1126
- expect(calls).toContain(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
1115
+ // The minted challenge must advertise the entry's recorded URL.
1116
+ expect(mintCalls[0]?.args.at(-1)).toBe("https://saved.example.ts.net");
1117
+ expect(calls).toHaveLength(0);
1127
1118
  const output = logs.join("\n");
1128
1119
  expect(output).toContain(
1129
1120
  "Using saved ingress URL https://saved.example.ts.net",
@@ -1137,33 +1128,7 @@ describe("pair command", () => {
1137
1128
  writeLockfileWithIngress("https://saved.example.ts.net");
1138
1129
 
1139
1130
  const origFetch = globalThis.fetch;
1140
- globalThis.fetch = withPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
1141
- const url = String(input);
1142
- if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
1143
- expect(JSON.parse(String(init?.body))).toEqual({
1144
- publicBaseUrl: "https://explicit.example.ts.net",
1145
- });
1146
- return new Response(
1147
- JSON.stringify({
1148
- deviceCode: "device-code",
1149
- userCode: "ABCD-EFGH",
1150
- verificationUri: "https://explicit.example.ts.net/assistant/pair",
1151
- expiresAt: "2026-06-04T00:10:00.000Z",
1152
- expiresInSeconds: 600,
1153
- }),
1154
- { status: 200, headers: { "content-type": "application/json" } },
1155
- );
1156
- }
1157
- if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
1158
- return new Response(
1159
- JSON.stringify({
1160
- status: "approved",
1161
- verificationUri: "https://explicit.example.ts.net/assistant/pair",
1162
- expiresAt: "2026-06-04T00:10:00.000Z",
1163
- }),
1164
- { status: 200, headers: { "content-type": "application/json" } },
1165
- );
1166
- }
1131
+ globalThis.fetch = withPreconnect(async () => {
1167
1132
  return new Response("not found", { status: 404 });
1168
1133
  });
1169
1134
 
@@ -1189,6 +1154,8 @@ describe("pair command", () => {
1189
1154
  }
1190
1155
 
1191
1156
  expect(logs.join("\n")).not.toContain("Using saved ingress URL");
1157
+ // The override reached the mint.
1158
+ expect(mintCalls[0]?.args.at(-1)).toBe("https://explicit.example.ts.net");
1192
1159
  });
1193
1160
 
1194
1161
  test("a non-https recorded ingress URL is ignored", async () => {
@@ -1270,4 +1237,101 @@ describe("pair command", () => {
1270
1237
  expect(errors.join("\n")).toContain(RUNTIME_URL);
1271
1238
  expect(minted).toBe(false);
1272
1239
  });
1240
+
1241
+ test("refuses a paired (connect-import) assistant before minting", async () => {
1242
+ writeFileSync(
1243
+ join(testDir, "lockfile.json"),
1244
+ JSON.stringify({
1245
+ assistants: [
1246
+ {
1247
+ assistantId: "pair-test",
1248
+ runtimeUrl: RUNTIME_URL,
1249
+ cloud: "paired",
1250
+ },
1251
+ ],
1252
+ activeAssistant: "pair-test",
1253
+ }),
1254
+ );
1255
+
1256
+ const errors: string[] = [];
1257
+ const errSpy = spyOn(console, "error").mockImplementation(
1258
+ (...a: unknown[]) => {
1259
+ errors.push(a.join(" "));
1260
+ },
1261
+ );
1262
+ const exitSpy = spyOn(process, "exit").mockImplementation(
1263
+ (code?: number | string | undefined): never => {
1264
+ throw new Error(`exit:${code}`);
1265
+ },
1266
+ );
1267
+
1268
+ process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
1269
+ let exited = false;
1270
+ try {
1271
+ await pair();
1272
+ } catch (e) {
1273
+ exited = e instanceof Error && e.message === "exit:1";
1274
+ } finally {
1275
+ errSpy.mockRestore();
1276
+ exitSpy.mockRestore();
1277
+ }
1278
+
1279
+ expect(exited).toBe(true);
1280
+ expect(errors.join("\n")).toContain("was paired as a remote");
1281
+ // Keyed on the entry's cloud, not on which fields happen to exist.
1282
+ expect(mintCalls).toHaveLength(0);
1283
+ });
1284
+
1285
+ test("a custom (ssh-managed) assistant keeps the loopback HTTP mint", async () => {
1286
+ writeFileSync(
1287
+ join(testDir, "lockfile.json"),
1288
+ JSON.stringify({
1289
+ assistants: [
1290
+ {
1291
+ assistantId: "pair-test",
1292
+ runtimeUrl: LOCAL_URL,
1293
+ cloud: "custom",
1294
+ },
1295
+ ],
1296
+ activeAssistant: "pair-test",
1297
+ }),
1298
+ );
1299
+
1300
+ const calls: Array<[string, RequestInit | undefined]> = [];
1301
+ const origFetch = globalThis.fetch;
1302
+ globalThis.fetch = withPreconnect(
1303
+ async (input: RequestInfo | URL, init?: RequestInit) => {
1304
+ const url = String(input);
1305
+ calls.push([url, init]);
1306
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
1307
+ return challengeResponse(PUBLIC_URL);
1308
+ }
1309
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
1310
+ return approvalResponse(PUBLIC_URL);
1311
+ }
1312
+ return new Response("not found", { status: 404 });
1313
+ },
1314
+ );
1315
+
1316
+ const logSpy = spyOn(console, "log").mockImplementation(() => {});
1317
+
1318
+ process.argv = ["bun", "cadet", "pair", "--url", PUBLIC_URL];
1319
+ try {
1320
+ await pair();
1321
+ } finally {
1322
+ logSpy.mockRestore();
1323
+ globalThis.fetch = origFetch;
1324
+ }
1325
+
1326
+ // Not local/docker: no CLI execution — the mint rides the loopback HTTP
1327
+ // route, keyed on the entry's cloud, not on containerInfo.
1328
+ expect(mintCalls).toHaveLength(0);
1329
+ expect(calls.map((c) => c[0])).toEqual([
1330
+ `${LOCAL_URL}/v1/remote-web/pairing-challenge`,
1331
+ `${LOCAL_URL}/v1/remote-web/pairing-verification`,
1332
+ ]);
1333
+ expect(JSON.parse(String(calls[0]?.[1]?.body))).toEqual({
1334
+ publicBaseUrl: PUBLIC_URL,
1335
+ });
1336
+ });
1273
1337
  });