@mycadet/cli 0.12.10 → 0.13.0

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 (105) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  8. package/package.json +3 -2
  9. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  10. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  11. package/src/__tests__/backup-ops.test.ts +105 -0
  12. package/src/__tests__/backup.test.ts +114 -673
  13. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  14. package/src/__tests__/cloud-releases.test.ts +82 -0
  15. package/src/__tests__/commands-mock.ts +101 -0
  16. package/src/__tests__/debug.test.ts +17 -91
  17. package/src/__tests__/devices.test.ts +64 -2
  18. package/src/__tests__/ensure-binary.test.ts +52 -0
  19. package/src/__tests__/gateway-token.test.ts +267 -0
  20. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  21. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  22. package/src/__tests__/host-teleport.test.ts +405 -0
  23. package/src/__tests__/multi-local.test.ts +3 -3
  24. package/src/__tests__/nginx-ingress.test.ts +233 -36
  25. package/src/__tests__/orphan-detection.test.ts +1 -1
  26. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  27. package/src/__tests__/ps-cloud-status.test.ts +8 -7
  28. package/src/__tests__/root-backup-docker.test.ts +294 -0
  29. package/src/__tests__/root-backup.test.ts +527 -0
  30. package/src/__tests__/root-restore-command.test.ts +147 -0
  31. package/src/__tests__/root-signing-key.test.ts +50 -0
  32. package/src/__tests__/setup.test.ts +17 -7
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  36. package/src/__tests__/tunnel-stop.test.ts +226 -0
  37. package/src/__tests__/tunnel.test.ts +13 -3
  38. package/src/__tests__/upgrade-local.test.ts +21 -11
  39. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  40. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  41. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  42. package/src/commands/backup.ts +44 -399
  43. package/src/commands/client.ts +3 -3
  44. package/src/commands/completions.ts +0 -2
  45. package/src/commands/debug.ts +75 -131
  46. package/src/commands/devices.ts +117 -47
  47. package/src/commands/gateway/token.ts +77 -23
  48. package/src/commands/hatch.ts +10 -52
  49. package/src/commands/pair.ts +9 -3
  50. package/src/commands/ps.ts +4 -2
  51. package/src/commands/restore.ts +77 -444
  52. package/src/commands/retire.ts +2 -3
  53. package/src/commands/sleep.ts +15 -2
  54. package/src/commands/teleport.ts +47 -1559
  55. package/src/commands/tunnel.ts +43 -4
  56. package/src/commands/upgrade.ts +84 -157
  57. package/src/commands/wake.ts +1 -1
  58. package/src/index.ts +0 -4
  59. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  60. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  61. package/src/lib/__tests__/web-dist.test.ts +27 -1
  62. package/src/lib/assistant-client.ts +1 -1
  63. package/src/lib/assistant-config.ts +20 -23
  64. package/src/lib/backup-ops.ts +37 -251
  65. package/src/lib/cloud-client.ts +10 -13
  66. package/src/lib/cloud-login-poll.ts +75 -0
  67. package/src/lib/cloud-login.ts +5 -2
  68. package/src/lib/cloud-releases.ts +54 -10
  69. package/src/lib/cloud-values.ts +1 -1
  70. package/src/lib/confirm-action.ts +2 -2
  71. package/src/lib/docker.ts +65 -34
  72. package/src/lib/ensure-binary.ts +65 -6
  73. package/src/lib/github-repository.ts +2 -0
  74. package/src/lib/guardian-token.ts +1 -23
  75. package/src/lib/hatch-local.ts +18 -52
  76. package/src/lib/hatch-next-steps.ts +15 -2
  77. package/src/lib/host-teleport.ts +506 -0
  78. package/src/lib/http-client.ts +25 -0
  79. package/src/lib/live-voice/tunnel.test.ts +1 -1
  80. package/src/lib/live-voice/tunnel.ts +3 -3
  81. package/src/lib/local.ts +10 -8
  82. package/src/lib/nginx-ingress.ts +254 -36
  83. package/src/lib/process.ts +13 -0
  84. package/src/lib/root-backup.ts +917 -0
  85. package/src/lib/root-signing-key.ts +31 -0
  86. package/src/lib/runtime-url.ts +0 -29
  87. package/src/lib/teleport-backup.ts +3 -16
  88. package/src/lib/tunnel.ts +86 -34
  89. package/src/lib/tunnels/cadet.ts +12 -1
  90. package/src/lib/tunnels/cloudflare.ts +36 -1
  91. package/src/lib/tunnels/ngrok.ts +16 -2
  92. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  93. package/src/lib/tunnels/tailscale.ts +79 -1
  94. package/src/lib/upgrade-lifecycle.ts +63 -141
  95. package/src/lib/web-dist.ts +23 -1
  96. package/src/lib/xdg-log.ts +11 -0
  97. package/src/__tests__/bundle-staging.test.ts +0 -268
  98. package/src/__tests__/unpair.test.ts +0 -160
  99. package/src/commands/rollback.ts +0 -479
  100. package/src/commands/unpair.ts +0 -122
  101. package/src/lib/__tests__/job-polling.test.ts +0 -278
  102. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  103. package/src/lib/bundle-staging.ts +0 -220
  104. package/src/lib/job-polling.ts +0 -195
  105. package/src/lib/local-runtime-client.ts +0 -374
@@ -51,6 +51,10 @@ mock.module("node:fs", () => ({
51
51
  readFileSync: readFileSyncMock,
52
52
  }));
53
53
 
54
+ const fetchCurrentVersionMock = mock<typeof httpClient.fetchCurrentVersion>(
55
+ async () => "1.2.3",
56
+ );
57
+
54
58
  const waitForDaemonReadyMock = mock<typeof httpClient.waitForDaemonReady>(
55
59
  async () => true,
56
60
  );
@@ -58,6 +62,7 @@ const waitForDaemonReadyMock = mock<typeof httpClient.waitForDaemonReady>(
58
62
  mock.module("../lib/http-client.js", () => ({
59
63
  ...httpClient,
60
64
  waitForDaemonReady: waitForDaemonReadyMock,
65
+ fetchCurrentVersion: fetchCurrentVersionMock,
61
66
  }));
62
67
 
63
68
  // Restore the real modules once this file finishes so the mocks do not leak
@@ -72,10 +77,10 @@ import {
72
77
  buildIngressNginxConfig,
73
78
  hasIpv6Loopback,
74
79
  buildRemoteWebIndexHtml,
75
- cloudWebHubUrl,
76
80
  MAX_PRESERVED_MODULE_PRELOADS,
77
81
  ensureTunnelEdge,
78
82
  EDGE_TEMPLATE_VERSION,
83
+ findWebDistDir,
79
84
  startRemoteWebIngress,
80
85
  stopContainerTunnelEdge,
81
86
  stopIngressNginx,
@@ -123,6 +128,8 @@ afterEach(() => {
123
128
  existsSyncMock.mockImplementation(realFs.existsSync);
124
129
  readFileSyncMock.mockReset();
125
130
  readFileSyncMock.mockImplementation(realFs.readFileSync);
131
+ fetchCurrentVersionMock.mockReset();
132
+ fetchCurrentVersionMock.mockImplementation(async () => "1.2.3");
126
133
  waitForDaemonReadyMock.mockReset();
127
134
  waitForDaemonReadyMock.mockImplementation(async () => true);
128
135
  for (const dir of workspaces.splice(0)) {
@@ -306,7 +313,7 @@ describe("buildIngressNginxConfig", () => {
306
313
  ).toEqual([]);
307
314
  });
308
315
 
309
- test("revalidates the shell and forbids storing only the inline config", () => {
316
+ test("revalidates the shell and forbids storing inline config", () => {
310
317
  const blocks = locationBlocks(remoteConf);
311
318
  const cacheControlOf = (matcher: string) =>
312
319
  blocks
@@ -317,11 +324,8 @@ describe("buildIngressNginxConfig", () => {
317
324
  expect(cacheControlOf("^~ /assistant/assets/")).toBe(
318
325
  "public, max-age=31536000, immutable",
319
326
  );
320
- // Only the inline __config return, which nginx gives no validator, still
321
- // refuses storage. A second no-store means a revalidating location
322
- // regressed to re-sending its whole body on every load.
327
+ // Inline config has no validator and must not be stored.
323
328
  expect(cacheControlOf("= /assistant/__config")).toBe("no-store");
324
- expect(remoteConf.match(/no-store/g)).toHaveLength(1);
325
329
  });
326
330
 
327
331
  test("serves remote web config for the SPA", () => {
@@ -456,9 +460,7 @@ describe("buildRemoteWebIndexHtml", () => {
456
460
  expect(result).toContain(
457
461
  'window.__CADET_CONFIG__={"remote":"docker","apiBaseUrl":"/v1"}',
458
462
  );
459
- expect(result.indexOf('"webUrl"')).toBeLessThan(
460
- result.indexOf('"docker"'),
461
- );
463
+ expect(result.indexOf('"webUrl"')).toBeLessThan(result.indexOf('"docker"'));
462
464
  });
463
465
 
464
466
  test("escapes config JSON before embedding it in a script tag", () => {
@@ -528,26 +530,6 @@ describe("buildRemoteWebIndexHtml", () => {
528
530
  });
529
531
  });
530
532
 
531
- describe("cloudWebHubUrl", () => {
532
- test("maps build environments to their cloud SPA base", () => {
533
- expect(cloudWebHubUrl("production")).toBe(
534
- "https://mycadet.ai/assistant",
535
- );
536
- expect(cloudWebHubUrl("staging")).toBe(
537
- "https://staging.mycadet.ai/assistant",
538
- );
539
- expect(cloudWebHubUrl("dev")).toBe(
540
- "https://dev.mycadet.ai/assistant",
541
- );
542
- expect(cloudWebHubUrl("local")).toBe(
543
- "https://dev.mycadet.ai/assistant",
544
- );
545
- expect(cloudWebHubUrl(undefined)).toBe(
546
- "https://dev.mycadet.ai/assistant",
547
- );
548
- });
549
- });
550
-
551
533
  describe("nginx ingress process state", () => {
552
534
  function writeIngressState(workspaceDir: string, listenPort: number): void {
553
535
  writeFileSync(
@@ -755,17 +737,19 @@ function mockWebDistMissing(): void {
755
737
  }
756
738
 
757
739
  /** Force findWebDistDir() to resolve a dist dir, regardless of checkout state. */
758
- function mockWebDistPresent(): void {
740
+ function mockWebDistPresent(version: string | null = "1.2.3"): void {
759
741
  existsSyncMock.mockImplementation((path) =>
760
742
  String(path).endsWith(WEB_INDEX_SUFFIX) ? true : realFs.existsSync(path),
761
743
  );
762
744
  readFileSyncMock.mockImplementation(((path, options) =>
763
745
  String(path).endsWith(WEB_INDEX_SUFFIX)
764
746
  ? FAKE_INDEX_HTML
765
- : realFs.readFileSync(
766
- path as never,
767
- options as never,
768
- )) as typeof readFileSync);
747
+ : String(path).endsWith("release.json")
748
+ ? JSON.stringify(version === null ? {} : { version })
749
+ : realFs.readFileSync(
750
+ path as never,
751
+ options as never,
752
+ )) as typeof readFileSync);
769
753
  }
770
754
 
771
755
  /** Record a running edge: ingress state, live pidfile, matching ps output. */
@@ -846,14 +830,19 @@ const PRODUCTION_HUB_URL = "https://mycadet.ai/assistant";
846
830
 
847
831
  /**
848
832
  * Mirror of the SPA config fingerprint the edge records in its ingress state
849
- * (sha256 over the edge template version and the injected config JSON). Pins
833
+ * (sha256 over the template, injected config, bundle directory and index). Pins
850
834
  * both the injected config shape and the hash format; assumes the
851
835
  * production-pinned environment. The version itself is imported rather than
852
836
  * mirrored: bumping it is the correct response to any template edit, so a
853
837
  * copy here would fail every such edit without naming a defect.
854
838
  */
855
839
  function spaConfigHash(
856
- opts: { assistantName?: string; assistantId?: string } = {},
840
+ opts: {
841
+ assistantName?: string;
842
+ assistantId?: string;
843
+ indexHtml?: string;
844
+ webDistDir?: string;
845
+ } = {},
857
846
  ): string {
858
847
  return createHash("sha256")
859
848
  .update(
@@ -866,6 +855,9 @@ function spaConfigHash(
866
855
  ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
867
856
  hubUrl: PRODUCTION_HUB_URL,
868
857
  },
858
+ webDistDir: opts.webDistDir ?? findWebDistDir(),
859
+ version: "1.2.3",
860
+ indexHtml: opts.indexHtml ?? FAKE_INDEX_HTML,
869
861
  }),
870
862
  )
871
863
  .digest("hex");
@@ -890,6 +882,70 @@ function pinProductionEnvironment(): void {
890
882
  describe("startRemoteWebIngress", () => {
891
883
  pinProductionEnvironment();
892
884
 
885
+ for (const [webVersion, runtimeVersion, ready] of [
886
+ ["1.2.3", "1.2.3", true],
887
+ ["1.2.3", "v1.2.3", true],
888
+ ["1.2.2", "1.2.3", false],
889
+ ["1.2.3", undefined, false],
890
+ [null, "1.2.3", false],
891
+ ] as const) {
892
+ test(`release resolution: UI ${webVersion}, assistant ${runtimeVersion}`, async () => {
893
+ const ws = makeWorkspace();
894
+ mockNginxInstalled();
895
+ mockNginxSpawn();
896
+ mockWebDistPresent(webVersion);
897
+ fetchCurrentVersionMock.mockResolvedValue(runtimeVersion);
898
+ const result = await startRemoteWebIngress({
899
+ workspaceDir: ws,
900
+ gatewayPort: 7830,
901
+ listenPort: 7845,
902
+ assistantId: "assistant-1",
903
+ });
904
+ expect(result.status).toBe("started");
905
+ expect(fetchCurrentVersionMock).toHaveBeenCalledWith(
906
+ "http://127.0.0.1:7830",
907
+ );
908
+ const conf = realFs.readFileSync(ingressConfPath(ws), "utf8");
909
+ const detail = conf
910
+ .split('location = "/v1/assistants/assistant-1/" {')[1]
911
+ ?.split("\n }")[0];
912
+ expect(detail).toContain(`return ${ready ? 200 : 409} `);
913
+ if (ready) {
914
+ expect(detail).toContain('\\"version\\":\\"1.2.3\\"');
915
+ } else {
916
+ expect(detail).not.toContain('\\"url\\"');
917
+ }
918
+ });
919
+ }
920
+
921
+ test("restarts an edge when its verified assistant/UI release changes", async () => {
922
+ const ws = makeWorkspace();
923
+ mockNginxInstalled();
924
+ mockNginxSpawn();
925
+ mockWebDistPresent("1.2.3");
926
+ fetchCurrentVersionMock.mockResolvedValue("1.2.4");
927
+ const pid = mockRunningEdge(ws, {
928
+ listenPort: 7845,
929
+ includeWebApp: true,
930
+ gatewayPort: 7830,
931
+ remoteWebConfigHash: spaConfigHash({ assistantId: "assistant-1" }),
932
+ });
933
+ const edge = mockKillableNginx(pid);
934
+ const result = await startRemoteWebIngress({
935
+ workspaceDir: ws,
936
+ gatewayPort: 7830,
937
+ listenPort: 7845,
938
+ assistantId: "assistant-1",
939
+ });
940
+ expect(result.status).toBe("started");
941
+ expect(edge.killed()).toBe(true);
942
+ const conf = realFs.readFileSync(ingressConfPath(ws), "utf8");
943
+ const detail = conf
944
+ .split('location = "/v1/assistants/assistant-1/" {')[1]
945
+ ?.split("\n }")[0];
946
+ expect(detail).toContain("return 409 ");
947
+ });
948
+
893
949
  test("webhooks-only mode starts the denylist+proxy edge without a web dist", async () => {
894
950
  const ws = makeWorkspace();
895
951
  mockNginxInstalled();
@@ -1247,6 +1303,7 @@ describe("startRemoteWebIngress", () => {
1247
1303
  const ws = makeWorkspace();
1248
1304
  mockNginxInstalled();
1249
1305
  mockNginxSpawn();
1306
+ mockWebDistPresent();
1250
1307
  const pid = mockRunningEdge(ws, {
1251
1308
  listenPort: 7845,
1252
1309
  gatewayPort: 7830,
@@ -1300,6 +1357,44 @@ describe("startRemoteWebIngress", () => {
1300
1357
  expect(spawnMock).not.toHaveBeenCalled();
1301
1358
  });
1302
1359
 
1360
+ for (const drift of ["indexHtml", "webDistDir"] as const) {
1361
+ test(`restarts a detached edge when the web bundle ${drift} changes`, async () => {
1362
+ const ws = makeWorkspace();
1363
+ mockNginxInstalled();
1364
+ mockNginxSpawn();
1365
+ mockWebDistPresent();
1366
+ const pid = mockRunningEdge(ws, {
1367
+ listenPort: 7845,
1368
+ includeWebApp: true,
1369
+ gatewayPort: 7830,
1370
+ remoteWebConfigHash: spaConfigHash({
1371
+ ...(drift === "indexHtml"
1372
+ ? {
1373
+ indexHtml:
1374
+ '<html><head><script src="/assistant/assets/old.js"></script></head></html>',
1375
+ }
1376
+ : { webDistDir: "/old-release/dist" }),
1377
+ }),
1378
+ });
1379
+ const edge = mockKillableNginx(pid);
1380
+ const result = await startRemoteWebIngress({
1381
+ workspaceDir: ws,
1382
+ gatewayPort: 7830,
1383
+ });
1384
+ expect(result).toMatchObject({ status: "started", listenPort: 7845 });
1385
+ expect(edge.killed()).toBe(true);
1386
+ expect(readConfig(ws).ingress?.nginx?.remoteWebConfigHash).toBe(
1387
+ spaConfigHash(),
1388
+ );
1389
+ expect(
1390
+ realFs.readFileSync(
1391
+ join(ws, "data", "ingress", "assistant-index.html"),
1392
+ "utf-8",
1393
+ ),
1394
+ ).not.toContain("old.js");
1395
+ });
1396
+ }
1397
+
1303
1398
  test("restarts the edge when the injected SPA config drifts from the recorded one", async () => {
1304
1399
  const ws = makeWorkspace();
1305
1400
  mockNginxInstalled();
@@ -1801,6 +1896,7 @@ describe("ensureTunnelEdge", () => {
1801
1896
  const ws = makeWorkspace();
1802
1897
  mockNginxInstalled();
1803
1898
  mockNginxSpawn();
1899
+ mockWebDistPresent();
1804
1900
  const pid = mockRunningEdge(ws, {
1805
1901
  listenPort: 7845,
1806
1902
  includeWebApp: true,
@@ -2223,3 +2319,104 @@ describe("stopContainerTunnelEdge", () => {
2223
2319
  expect(await stopContainerTunnelEdge(containerEntry(7930))).toBe(false);
2224
2320
  });
2225
2321
  });
2322
+
2323
+ describe("assistant metadata API", () => {
2324
+ function config(id = "assistant-1") {
2325
+ return buildIngressNginxConfig({
2326
+ gatewayPort: 7830,
2327
+ listenPort: 7840,
2328
+ remoteWebIngress: {
2329
+ webDistDir: "/tmp/web",
2330
+ version: "1.2.3",
2331
+ assistantId: id,
2332
+ assistantName: "My assistant",
2333
+ config: { appAllowedOrigins: ["https://parent.example"] },
2334
+ },
2335
+ });
2336
+ }
2337
+ function body(conf: string, path: string, status = 200): unknown {
2338
+ const location = conf
2339
+ .split(`location = "${path}" {`)[1]
2340
+ ?.split("\n }")[0];
2341
+ const value = location?.match(/return (200|409) ("(?:\\.|[^"\\])*");/);
2342
+ expect(Number(value?.[1])).toBe(status);
2343
+ if (!value) {
2344
+ throw new Error(`Missing metadata for ${path}`);
2345
+ }
2346
+ return JSON.parse(JSON.parse(value[2]));
2347
+ }
2348
+ test("lists only this host's assistant and returns its actual UI version and URL", () => {
2349
+ const conf = config();
2350
+ expect(body(conf, "/v1/assistants/")).toEqual({
2351
+ results: [{ id: "assistant-1", name: "My assistant" }],
2352
+ next: null,
2353
+ });
2354
+ expect(body(conf, "/v1/assistants/assistant-1/")).toEqual({
2355
+ id: "assistant-1",
2356
+ name: "My assistant",
2357
+ status: "active",
2358
+ version: "1.2.3",
2359
+ url: "assistant/",
2360
+ });
2361
+ expect(conf).not.toContain('location = "/v1/assistants/other/"');
2362
+ });
2363
+ test("metadata supports both slash forms and only read methods", () => {
2364
+ const conf = config();
2365
+ for (const path of [
2366
+ "/v1/assistants",
2367
+ "/v1/assistants/",
2368
+ "/v1/assistants/assistant-1",
2369
+ "/v1/assistants/assistant-1/",
2370
+ ]) {
2371
+ expect(body(conf, path)).toBeDefined();
2372
+ }
2373
+ expect(conf).toContain(
2374
+ "if ($request_method !~ ^(GET|HEAD)$) { return 405; }",
2375
+ );
2376
+ expect(conf).toContain(
2377
+ '"https://parent.example" "https://parent.example";',
2378
+ );
2379
+ expect(conf).toContain(
2380
+ "add_header Access-Control-Allow-Origin $cadet_app_origin always;",
2381
+ );
2382
+ });
2383
+ test("unknown identity yields an empty list without inventing an assistant ID", () => {
2384
+ const conf = buildIngressNginxConfig({
2385
+ gatewayPort: 7830,
2386
+ listenPort: 7840,
2387
+ remoteWebIngress: { webDistDir: "/tmp/web" },
2388
+ });
2389
+ expect(body(conf, "/v1/assistants/")).toEqual({ results: [], next: null });
2390
+ });
2391
+ test("a known assistant without a UI bundle returns 409 on both detail paths", () => {
2392
+ const conf = buildIngressNginxConfig({
2393
+ gatewayPort: 7830,
2394
+ listenPort: 7840,
2395
+ remoteWebIngress: { webDistDir: "/tmp/web", assistantId: "assistant-1" },
2396
+ });
2397
+ expect(body(conf, "/v1/assistants/")).toEqual({
2398
+ results: [{ id: "assistant-1" }],
2399
+ next: null,
2400
+ });
2401
+ for (const path of [
2402
+ "/v1/assistants/assistant-1",
2403
+ "/v1/assistants/assistant-1/",
2404
+ ]) {
2405
+ expect(body(conf, path, 409)).toEqual({
2406
+ detail: "assistant is not ready",
2407
+ });
2408
+ }
2409
+ });
2410
+ test("invalid browser origins fail before nginx starts", () => {
2411
+ expect(() =>
2412
+ buildIngressNginxConfig({
2413
+ gatewayPort: 7830,
2414
+ listenPort: 7840,
2415
+ remoteWebIngress: {
2416
+ webDistDir: "/tmp/web",
2417
+ config: { appAllowedOrigins: ["https://parent.example/path"] },
2418
+ },
2419
+ }),
2420
+ ).toThrow("App origins");
2421
+ });
2422
+ });
@@ -107,7 +107,7 @@ describe("getKnownPidsFromAssistants", () => {
107
107
  const entry: AssistantEntry = {
108
108
  assistantId: "managed-1",
109
109
  runtimeUrl: "https://cloud.mycadet.ai/foo",
110
- cloud: "cadet",
110
+ cloud: "cloud",
111
111
  };
112
112
  const pids = getKnownPidsFromAssistants([entry]);
113
113
  expect(pids.size).toBe(0);
@@ -102,13 +102,13 @@ describe("paired lifecycle guards", () => {
102
102
  expect(errors).not.toContain("only works with local and docker");
103
103
  });
104
104
 
105
- test("retire refuses a paired entry and points to unpair", async () => {
105
+ test("retire refuses a paired entry and points to the host", async () => {
106
106
  process.argv = ["bun", "cadet", "retire", "px", "--yes"];
107
107
  const { exited, errors } = await runGuarded(retire);
108
108
 
109
109
  expect(exited).toBe(true);
110
110
  expect(errors).toContain("paired from another machine");
111
- expect(errors).toContain("cadet unpair");
111
+ expect(errors).toContain("cadet devices");
112
112
  // It must NOT fall through to the generic "Unknown cloud type" path.
113
113
  expect(errors).not.toContain("Unknown cloud type");
114
114
  });
@@ -230,13 +230,13 @@ describe("cadet ps — platform status line", () => {
230
230
  assistantId: "assistant-123",
231
231
  name: "Alice",
232
232
  runtimeUrl: "https://platform.example",
233
- cloud: "cadet",
233
+ cloud: "cloud",
234
234
  },
235
235
  {
236
236
  assistantId: "assistant-456",
237
237
  name: "Bob",
238
238
  runtimeUrl: "https://platform.example/b",
239
- cloud: "cadet",
239
+ cloud: "cloud",
240
240
  },
241
241
  ]);
242
242
  getActiveAssistantMock.mockReturnValue(null);
@@ -262,7 +262,8 @@ describe("cadet ps — platform status line", () => {
262
262
  // children of an assistant row…
263
263
  expect(output).toContain("ngrok");
264
264
  expect(output).not.toContain("└ ngrok");
265
- expect(output).toContain("nginx edge");
265
+ expect(output).toContain("nginx");
266
+ expect(output).toContain("port ");
266
267
  // …the type column distinguishes the three process kinds…
267
268
  expect(output).toContain("🧑‍🚀 Assistant");
268
269
  expect(output).toContain("🚇 Tunnel");
@@ -277,7 +278,7 @@ describe("cadet ps — platform status line", () => {
277
278
  assistantId: "assistant-123",
278
279
  name: "Alice",
279
280
  runtimeUrl: "https://platform.example",
280
- cloud: "cadet",
281
+ cloud: "cloud",
281
282
  },
282
283
  ]);
283
284
  getActiveAssistantMock.mockReturnValue("assistant-123");
@@ -286,7 +287,7 @@ describe("cadet ps — platform status line", () => {
286
287
 
287
288
  const output = stdout.join("\n");
288
289
  expect(output).toContain("* Alice");
289
- expect(output).toContain("remote: cadet");
290
+ expect(output).toContain("remote: cloud");
290
291
  expect(output).toContain("https://platform.example");
291
292
  expect(output).not.toContain("* assistant-123");
292
293
  });
@@ -297,13 +298,13 @@ describe("cadet ps — platform status line", () => {
297
298
  assistantId: "assistant-123",
298
299
  name: "Alice",
299
300
  runtimeUrl: "https://platform.example/a",
300
- cloud: "cadet",
301
+ cloud: "cloud",
301
302
  },
302
303
  {
303
304
  assistantId: "assistant-456",
304
305
  name: "Bob",
305
306
  runtimeUrl: "https://platform.example/b",
306
- cloud: "cadet",
307
+ cloud: "cloud",
307
308
  },
308
309
  ]);
309
310
  getActiveAssistantMock.mockReturnValue("assistant-123");