@mycadet/cli 0.12.8 → 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 (67) hide show
  1. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +2 -2
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
  4. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  5. package/package.json +4 -3
  6. package/src/__tests__/client-web-config.test.ts +0 -1
  7. package/src/__tests__/cloud-releases.test.ts +23 -13
  8. package/src/__tests__/ensure-binary.test.ts +182 -0
  9. package/src/__tests__/nginx-ingress.test.ts +6 -11
  10. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  11. package/src/__tests__/pair.test.ts +149 -78
  12. package/src/__tests__/ps-cloud-status.test.ts +4 -3
  13. package/src/__tests__/ps-table.test.ts +79 -0
  14. package/src/__tests__/tailscale.test.ts +1 -1
  15. package/src/__tests__/teleport.test.ts +1 -16
  16. package/src/__tests__/tunnel.test.ts +274 -82
  17. package/src/__tests__/uninstall.test.ts +141 -9
  18. package/src/__tests__/wake.test.ts +2 -2
  19. package/src/commands/backup.ts +3 -3
  20. package/src/commands/client.ts +4 -6
  21. package/src/commands/completions.test.ts +104 -0
  22. package/src/commands/completions.ts +87 -0
  23. package/src/commands/exec.ts +7 -7
  24. package/src/commands/flags.ts +2 -2
  25. package/src/commands/hatch.ts +5 -2
  26. package/src/commands/logs.ts +7 -7
  27. package/src/commands/pair.ts +140 -37
  28. package/src/commands/ps.ts +63 -26
  29. package/src/commands/restore.ts +2 -2
  30. package/src/commands/retire.ts +2 -2
  31. package/src/commands/rollback.ts +2 -2
  32. package/src/commands/sleep.ts +2 -2
  33. package/src/commands/teleport.ts +18 -37
  34. package/src/commands/terminal.ts +1 -1
  35. package/src/commands/tunnel.ts +101 -78
  36. package/src/commands/uninstall.ts +108 -73
  37. package/src/commands/upgrade.ts +8 -8
  38. package/src/commands/use.ts +1 -1
  39. package/src/commands/wake.ts +6 -6
  40. package/src/index.ts +15 -0
  41. package/src/lib/__tests__/docker.test.ts +168 -20
  42. package/src/lib/__tests__/job-polling.test.ts +1 -1
  43. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  44. package/src/lib/cloud-client.ts +0 -3
  45. package/src/lib/cloud-login.ts +0 -1
  46. package/src/lib/cloud-releases.ts +6 -4
  47. package/src/lib/color.ts +4 -0
  48. package/src/lib/docker.ts +190 -102
  49. package/src/lib/ensure-binary.ts +171 -0
  50. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  51. package/src/lib/environments/paths.ts +16 -8
  52. package/src/lib/environments/resolve.ts +2 -4
  53. package/src/lib/flag-args.test.ts +1 -1
  54. package/src/lib/flag-args.ts +1 -1
  55. package/src/lib/guardian-token.ts +1 -1
  56. package/src/lib/hatch-local.ts +15 -8
  57. package/src/lib/hatch-next-steps.ts +5 -9
  58. package/src/lib/job-polling.ts +1 -1
  59. package/src/lib/local.ts +37 -4
  60. package/src/lib/nginx-ingress.ts +3 -5
  61. package/src/lib/pair.ts +1 -1
  62. package/src/lib/step-runner.ts +6 -1
  63. package/src/lib/sync-cloud-assistants.ts +2 -2
  64. package/src/lib/tunnels/cloudflare.ts +21 -11
  65. package/src/lib/tunnels/ngrok.ts +198 -108
  66. package/src/lib/tunnels/tailscale.ts +34 -4
  67. package/src/lib/upgrade-lifecycle.ts +5 -4
@@ -84,9 +84,13 @@ const EDGE_PORT = 18080;
84
84
  const ensureTunnelEdgeMock = mock<typeof nginxIngress.ensureTunnelEdge>(
85
85
  async () => ({ port: EDGE_PORT, started: true, includesWebApp: true }),
86
86
  );
87
+ const getNginxVersionMock = mock<typeof nginxIngress.getNginxVersion>(
88
+ () => "nginx version (test)",
89
+ );
87
90
  mock.module("../lib/nginx-ingress.js", () => ({
88
91
  ...realNginxIngress,
89
92
  ensureTunnelEdge: ensureTunnelEdgeMock,
93
+ getNginxVersion: getNginxVersionMock,
90
94
  }));
91
95
 
92
96
  const { tunnel } = await import("../commands/tunnel.js");
@@ -241,7 +245,14 @@ async function runTunnelCapturingLogs(): Promise<string> {
241
245
 
242
246
  describe("tunnel edge targeting", () => {
243
247
  beforeEach(() => {
244
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
248
+ process.argv = [
249
+ "bun",
250
+ "cadet",
251
+ "tunnel",
252
+ "--provider",
253
+ "tailscale",
254
+ "--foreground",
255
+ ];
245
256
  writeLockfile(makeLocalEntry());
246
257
  globalThis.fetch = withPreconnect(async () => {
247
258
  throw new Error("gateway unavailable");
@@ -307,7 +318,14 @@ describe("tunnel edge targeting", () => {
307
318
  const entry = makeLocalEntry();
308
319
  entry.runtimeUrl = "https://stale-tunnel.ngrok-free.dev";
309
320
  writeLockfile(entry);
310
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
321
+ process.argv = [
322
+ "bun",
323
+ "cadet",
324
+ "tunnel",
325
+ "--provider",
326
+ "ngrok",
327
+ "--foreground",
328
+ ];
311
329
 
312
330
  const logs = await runTunnelCapturingLogs();
313
331
 
@@ -324,12 +342,18 @@ describe("tunnel edge targeting", () => {
324
342
  });
325
343
  expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
326
344
  expect(logs).toContain(`Started the nginx edge on 127.0.0.1:${EDGE_PORT}`);
327
- expect(logs).toContain("serves remote web + webhooks");
328
345
  });
329
346
 
330
347
  test("the tunnel level notifies the gateway of the provider's public URL", async () => {
331
348
  writeLockfile(makeLocalEntry());
332
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
349
+ process.argv = [
350
+ "bun",
351
+ "cadet",
352
+ "tunnel",
353
+ "--provider",
354
+ "ngrok",
355
+ "--foreground",
356
+ ];
333
357
 
334
358
  await runTunnelCapturingLogs();
335
359
 
@@ -345,7 +369,14 @@ describe("tunnel edge targeting", () => {
345
369
  const cloud = makeCloudEntry();
346
370
  const local = makeLocalEntry();
347
371
  writeLockfile([cloud, local], cloud.assistantId);
348
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
372
+ process.argv = [
373
+ "bun",
374
+ "cadet",
375
+ "tunnel",
376
+ "--provider",
377
+ "ngrok",
378
+ "--foreground",
379
+ ];
349
380
 
350
381
  const logs = await runTunnelCapturingLogs();
351
382
 
@@ -377,6 +408,7 @@ describe("tunnel edge targeting", () => {
377
408
  "docker-1",
378
409
  "--provider",
379
410
  "ngrok",
411
+ "--foreground",
380
412
  ];
381
413
 
382
414
  await runTunnelCapturingLogs();
@@ -397,7 +429,14 @@ describe("tunnel edge targeting", () => {
397
429
  const workspaceDir = useTempDefaultWorkspaceDir();
398
430
  const docker = makeDockerEntry();
399
431
  writeLockfile(docker, docker.assistantId);
400
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
432
+ process.argv = [
433
+ "bun",
434
+ "cadet",
435
+ "tunnel",
436
+ "--provider",
437
+ "ngrok",
438
+ "--foreground",
439
+ ];
401
440
 
402
441
  await runTunnelCapturingLogs();
403
442
 
@@ -417,7 +456,14 @@ describe("tunnel edge targeting", () => {
417
456
  const workspaceDir = useTempDefaultWorkspaceDir();
418
457
  const cloud = makeCloudEntry();
419
458
  writeLockfile([cloud, makeDockerEntry()], cloud.assistantId);
420
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
459
+ process.argv = [
460
+ "bun",
461
+ "cadet",
462
+ "tunnel",
463
+ "--provider",
464
+ "ngrok",
465
+ "--foreground",
466
+ ];
421
467
 
422
468
  const logs = await runTunnelCapturingLogs();
423
469
 
@@ -434,7 +480,14 @@ describe("tunnel edge targeting", () => {
434
480
 
435
481
  test("an active cloud assistant with no local entries exits with an error", async () => {
436
482
  writeLockfile(makeCloudEntry());
437
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
483
+ process.argv = [
484
+ "bun",
485
+ "cadet",
486
+ "tunnel",
487
+ "--provider",
488
+ "ngrok",
489
+ "--foreground",
490
+ ];
438
491
 
439
492
  const { exited, errors } = await runTunnelExpectingExit1();
440
493
 
@@ -453,7 +506,14 @@ describe("tunnel edge targeting", () => {
453
506
  [cloud, makeLocalEntry("assistant-a"), makeLocalEntry("assistant-b")],
454
507
  cloud.assistantId,
455
508
  );
456
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
509
+ process.argv = [
510
+ "bun",
511
+ "cadet",
512
+ "tunnel",
513
+ "--provider",
514
+ "ngrok",
515
+ "--foreground",
516
+ ];
457
517
 
458
518
  const { exited, errors } = await runTunnelExpectingExit1();
459
519
 
@@ -471,7 +531,15 @@ describe("tunnel edge targeting", () => {
471
531
  test("a positional cloud assistant name errors without auto-fallback", async () => {
472
532
  const local = makeLocalEntry();
473
533
  writeLockfile([makeCloudEntry(), local], local.assistantId);
474
- process.argv = ["bun", "cadet", "tunnel", "cloud-1", "--provider", "ngrok"];
534
+ process.argv = [
535
+ "bun",
536
+ "cadet",
537
+ "tunnel",
538
+ "cloud-1",
539
+ "--provider",
540
+ "ngrok",
541
+ "--foreground",
542
+ ];
475
543
 
476
544
  const { exited, errors } = await runTunnelExpectingExit1();
477
545
 
@@ -490,7 +558,15 @@ describe("tunnel edge targeting", () => {
490
558
  const local = makeLocalEntry();
491
559
  local.name = "Ada";
492
560
  writeLockfile([makeCloudEntry(), local], "cloud-1");
493
- process.argv = ["bun", "cadet", "tunnel", "Ada", "--provider", "ngrok"];
561
+ process.argv = [
562
+ "bun",
563
+ "cadet",
564
+ "tunnel",
565
+ "Ada",
566
+ "--provider",
567
+ "ngrok",
568
+ "--foreground",
569
+ ];
494
570
 
495
571
  await runTunnelCapturingLogs();
496
572
 
@@ -514,6 +590,7 @@ describe("tunnel edge targeting", () => {
514
590
  "Lovelace",
515
591
  "--provider",
516
592
  "ngrok",
593
+ "--foreground",
517
594
  ];
518
595
 
519
596
  await runTunnelCapturingLogs();
@@ -530,7 +607,15 @@ describe("tunnel edge targeting", () => {
530
607
  const workspaceDir = useTempDefaultWorkspaceDir();
531
608
  const local = makeLocalEntry();
532
609
  writeLockfile([local, makeAppleContainerEntry()], local.assistantId);
533
- process.argv = ["bun", "cadet", "tunnel", "apple-1", "--provider", "ngrok"];
610
+ process.argv = [
611
+ "bun",
612
+ "cadet",
613
+ "tunnel",
614
+ "apple-1",
615
+ "--provider",
616
+ "ngrok",
617
+ "--foreground",
618
+ ];
534
619
 
535
620
  await runTunnelCapturingLogs();
536
621
 
@@ -550,7 +635,14 @@ describe("tunnel edge targeting", () => {
550
635
  const workspaceDir = useTempDefaultWorkspaceDir();
551
636
  const cloud = makeCloudEntry();
552
637
  writeLockfile([cloud, makeAppleContainerEntry()], cloud.assistantId);
553
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
638
+ process.argv = [
639
+ "bun",
640
+ "cadet",
641
+ "tunnel",
642
+ "--provider",
643
+ "ngrok",
644
+ "--foreground",
645
+ ];
554
646
 
555
647
  const logs = await runTunnelCapturingLogs();
556
648
 
@@ -582,6 +674,7 @@ describe("tunnel edge targeting", () => {
582
674
  "assistant-b",
583
675
  "--provider",
584
676
  "ngrok",
677
+ "--foreground",
585
678
  ];
586
679
 
587
680
  await runTunnelCapturingLogs();
@@ -596,7 +689,15 @@ describe("tunnel edge targeting", () => {
596
689
  cloud.name = "Cloudy";
597
690
  const local = makeLocalEntry();
598
691
  writeLockfile([cloud, local], local.assistantId);
599
- process.argv = ["bun", "cadet", "tunnel", "Cloudy", "--provider", "ngrok"];
692
+ process.argv = [
693
+ "bun",
694
+ "cadet",
695
+ "tunnel",
696
+ "Cloudy",
697
+ "--provider",
698
+ "ngrok",
699
+ "--foreground",
700
+ ];
600
701
 
601
702
  const { exited, errors } = await runTunnelExpectingExit1();
602
703
 
@@ -617,7 +718,15 @@ describe("tunnel edge targeting", () => {
617
718
  const second = makeLocalEntry("assistant-b");
618
719
  second.name = "Ada";
619
720
  writeLockfile([first, second], "assistant-a");
620
- process.argv = ["bun", "cadet", "tunnel", "Ada", "--provider", "ngrok"];
721
+ process.argv = [
722
+ "bun",
723
+ "cadet",
724
+ "tunnel",
725
+ "Ada",
726
+ "--provider",
727
+ "ngrok",
728
+ "--foreground",
729
+ ];
621
730
 
622
731
  const { exited, errors } = await runTunnelExpectingExit1();
623
732
 
@@ -630,7 +739,15 @@ describe("tunnel edge targeting", () => {
630
739
  });
631
740
 
632
741
  test("an unknown positional name errors", async () => {
633
- process.argv = ["bun", "cadet", "tunnel", "nope", "--provider", "ngrok"];
742
+ process.argv = [
743
+ "bun",
744
+ "cadet",
745
+ "tunnel",
746
+ "nope",
747
+ "--provider",
748
+ "ngrok",
749
+ "--foreground",
750
+ ];
634
751
 
635
752
  const { exited, errors } = await runTunnelExpectingExit1();
636
753
 
@@ -643,7 +760,14 @@ describe("tunnel edge targeting", () => {
643
760
  test("targets the edge port for cloudflare", async () => {
644
761
  const entry = makeLocalEntry();
645
762
  writeLockfile(entry);
646
- process.argv = ["bun", "cadet", "tunnel", "--provider", "cloudflare"];
763
+ process.argv = [
764
+ "bun",
765
+ "cadet",
766
+ "tunnel",
767
+ "--provider",
768
+ "cloudflare",
769
+ "--foreground",
770
+ ];
647
771
 
648
772
  await runTunnelCapturingLogs();
649
773
 
@@ -658,7 +782,14 @@ describe("tunnel edge targeting", () => {
658
782
  test("targets the edge port for tailscale and notes a reused webhooks-only edge", async () => {
659
783
  const entry = makeLocalEntry();
660
784
  writeLockfile(entry);
661
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
785
+ process.argv = [
786
+ "bun",
787
+ "cadet",
788
+ "tunnel",
789
+ "--provider",
790
+ "tailscale",
791
+ "--foreground",
792
+ ];
662
793
  ensureTunnelEdgeMock.mockResolvedValue({
663
794
  port: EDGE_PORT,
664
795
  started: false,
@@ -673,13 +804,12 @@ describe("tunnel edge targeting", () => {
673
804
  workspaceDir: join(entry.resources!.instanceDir, "workspace"),
674
805
  });
675
806
  expect(logs).toContain(`Reusing the nginx edge on 127.0.0.1:${EDGE_PORT}`);
676
- expect(logs).toContain("serves webhooks only");
677
807
  });
678
808
 
679
809
  test("a bare invocation tunnels the default provider", async () => {
680
810
  const entry = makeLocalEntry();
681
811
  writeLockfile(entry);
682
- process.argv = ["bun", "cadet", "tunnel"];
812
+ process.argv = ["bun", "cadet", "tunnel", "--foreground"];
683
813
 
684
814
  const logs = await runTunnelCapturingLogs();
685
815
 
@@ -698,7 +828,7 @@ describe("tunnel edge targeting", () => {
698
828
  const entry = makeLocalEntry();
699
829
  entry.name = "Ada";
700
830
  writeLockfile(entry);
701
- process.argv = ["bun", "cadet", "tunnel", "Ada"];
831
+ process.argv = ["bun", "cadet", "tunnel", "Ada", "--foreground"];
702
832
 
703
833
  const logs = await runTunnelCapturingLogs();
704
834
 
@@ -718,7 +848,14 @@ describe("tunnel edge targeting", () => {
718
848
  const workspaceDir = writeEntryWorkspaceConfig(entry, {
719
849
  telegram: { botUsername: "example_bot" },
720
850
  });
721
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
851
+ process.argv = [
852
+ "bun",
853
+ "cadet",
854
+ "tunnel",
855
+ "--provider",
856
+ "tailscale",
857
+ "--foreground",
858
+ ];
722
859
 
723
860
  const warnings: string[] = [];
724
861
  const warnSpy = spyOn(console, "warn").mockImplementation(
@@ -749,7 +886,14 @@ describe("tunnel edge targeting", () => {
749
886
  const entry = makeLocalEntry();
750
887
  writeLockfile(entry);
751
888
  const workspaceDir = writeEntryWorkspaceConfig(entry, {});
752
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
889
+ process.argv = [
890
+ "bun",
891
+ "cadet",
892
+ "tunnel",
893
+ "--provider",
894
+ "tailscale",
895
+ "--foreground",
896
+ ];
753
897
 
754
898
  await runTunnelCapturingLogs();
755
899
 
@@ -766,7 +910,14 @@ describe("tunnel edge targeting", () => {
766
910
  const workspaceDir = writeEntryWorkspaceConfig(entry, {
767
911
  telegram: { botUsername: "example_bot" },
768
912
  });
769
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
913
+ process.argv = [
914
+ "bun",
915
+ "cadet",
916
+ "tunnel",
917
+ "--provider",
918
+ "ngrok",
919
+ "--foreground",
920
+ ];
770
921
 
771
922
  await runTunnelCapturingLogs();
772
923
 
@@ -778,11 +929,18 @@ describe("tunnel edge targeting", () => {
778
929
  });
779
930
 
780
931
  test("missing nginx aborts before any provider spawn", async () => {
781
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
932
+ process.argv = [
933
+ "bun",
934
+ "cadet",
935
+ "tunnel",
936
+ "--provider",
937
+ "ngrok",
938
+ "--foreground",
939
+ ];
782
940
  ensureTunnelEdgeMock.mockRejectedValue(
783
941
  new Error(
784
942
  "nginx is not installed, so the tunnel edge cannot start. " +
785
- "Install it (macOS: `brew install nginx`, Linux: `sudo apt install nginx`) " +
943
+ "See https://nginx.org/en/docs/install.html for how to install, " +
786
944
  "or point NGINX_BIN at an existing binary.",
787
945
  ),
788
946
  );
@@ -790,14 +948,23 @@ describe("tunnel edge targeting", () => {
790
948
  const { exited, errors } = await runTunnelExpectingExit1();
791
949
 
792
950
  expect(exited).toBe(true);
793
- expect(errors).toContain("brew install nginx");
951
+ expect(errors).toContain(
952
+ "See https://nginx.org/en/docs/install.html for how to install",
953
+ );
794
954
  expect(runNgrokTunnelMock).not.toHaveBeenCalled();
795
955
  expect(runCloudflareTunnelMock).not.toHaveBeenCalled();
796
956
  expect(runTailscaleTunnelMock).not.toHaveBeenCalled();
797
957
  });
798
958
 
799
959
  test("rejects an unknown --provider with a stale-CLI hint", async () => {
800
- process.argv = ["bun", "cadet", "tunnel", "--provider", "bogus"];
960
+ process.argv = [
961
+ "bun",
962
+ "cadet",
963
+ "tunnel",
964
+ "--provider",
965
+ "bogus",
966
+ "--foreground",
967
+ ];
801
968
 
802
969
  const { exited, errors } = await runTunnelExpectingExit1();
803
970
 
@@ -814,7 +981,14 @@ describe("tunnel edge targeting", () => {
814
981
  try {
815
982
  for (const provider of TUNNEL_PROVIDERS) {
816
983
  writeLockfile(makeLocalEntry());
817
- process.argv = ["bun", "cadet", "tunnel", "--provider", provider];
984
+ process.argv = [
985
+ "bun",
986
+ "cadet",
987
+ "tunnel",
988
+ "--provider",
989
+ provider,
990
+ "--foreground",
991
+ ];
818
992
 
819
993
  const { exited, errors } = await runTunnelExpectingExit1();
820
994
 
@@ -837,6 +1011,7 @@ describe("tunnel edge targeting", () => {
837
1011
  "ngrok",
838
1012
  "--domain",
839
1013
  "foo.ngrok.app",
1014
+ "--foreground",
840
1015
  ];
841
1016
 
842
1017
  await runTunnelCapturingLogs();
@@ -858,6 +1033,7 @@ describe("tunnel edge targeting", () => {
858
1033
  "cloudflare",
859
1034
  "--domain",
860
1035
  "foo.ngrok.app",
1036
+ "--foreground",
861
1037
  ];
862
1038
 
863
1039
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -883,6 +1059,7 @@ describe("tunnel edge targeting", () => {
883
1059
  "--provider",
884
1060
  "ngrok",
885
1061
  "--clear-domain",
1062
+ "--foreground",
886
1063
  ];
887
1064
 
888
1065
  const logs = await runTunnelCapturingLogs();
@@ -908,6 +1085,7 @@ describe("tunnel edge targeting", () => {
908
1085
  "--domain",
909
1086
  "foo.ngrok.app",
910
1087
  "--clear-domain",
1088
+ "--foreground",
911
1089
  ];
912
1090
 
913
1091
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -925,6 +1103,7 @@ describe("tunnel edge targeting", () => {
925
1103
  "--provider",
926
1104
  "cloudflare",
927
1105
  "--clear-domain",
1106
+ "--foreground",
928
1107
  ];
929
1108
 
930
1109
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -945,6 +1124,7 @@ describe("tunnel edge targeting", () => {
945
1124
  "--provider",
946
1125
  "ngrok",
947
1126
  "--domain",
1127
+ "--foreground",
948
1128
  ];
949
1129
 
950
1130
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -957,7 +1137,14 @@ describe("tunnel edge targeting", () => {
957
1137
  test("a not-yet-implemented provider error carries the stale-CLI hint", async () => {
958
1138
  // `cadet` stays valid-but-unimplemented, so it hits this path rather
959
1139
  // than the unknown-provider one.
960
- process.argv = ["bun", "cadet", "tunnel", "--provider", "cadet"];
1140
+ process.argv = [
1141
+ "bun",
1142
+ "cadet",
1143
+ "tunnel",
1144
+ "--provider",
1145
+ "cadet",
1146
+ "--foreground",
1147
+ ];
961
1148
 
962
1149
  let err: Error | undefined;
963
1150
  try {
@@ -1139,13 +1326,14 @@ describe("ngrok --domain spawn args", () => {
1139
1326
  expect(spawnMock).toHaveBeenCalledTimes(1);
1140
1327
  const [cmd, args] = spawnMock.mock.calls[0];
1141
1328
  expect(cmd).toBe("ngrok");
1329
+ // Plain `ngrok http` so the user's default config — where their
1330
+ // authtoken lives — is what the agent loads.
1142
1331
  expect(args).toEqual([
1143
- "start",
1144
- "cadet",
1145
- `--config=${realNgrok.ngrokConfigPath()}`,
1332
+ "http",
1333
+ "127.0.0.1:7831",
1146
1334
  "--log=stdout",
1335
+ "--domain=foo.ngrok.app",
1147
1336
  ]);
1148
- // The reserved domain rides the XDG config the agent loads, not a flag.
1149
1337
  expect(realNgrok.loadNgrokDomain()).toBe("foo.ngrok.app");
1150
1338
 
1151
1339
  const config = IngressTestConfigSchema.parse(
@@ -1430,11 +1618,13 @@ describe("ngrok --domain spawn args", () => {
1430
1618
  expect(spawnMock).toHaveBeenCalledTimes(1);
1431
1619
  const [cmd, args] = spawnMock.mock.calls[0];
1432
1620
  expect(cmd).toBe("ngrok");
1621
+ // Plain `ngrok http` so the user's default config — where their
1622
+ // authtoken lives — is what the agent loads.
1433
1623
  expect(args).toEqual([
1434
- "start",
1435
- "cadet",
1436
- `--config=${realNgrok.ngrokConfigPath()}`,
1624
+ "http",
1625
+ "127.0.0.1:7830",
1437
1626
  "--log=stdout",
1627
+ "--domain=foo.ngrok.app",
1438
1628
  ]);
1439
1629
 
1440
1630
  const config = IngressTestConfigSchema.parse(
@@ -1469,10 +1659,10 @@ describe("ngrok --domain spawn args", () => {
1469
1659
  expect(spawnMock).toHaveBeenCalledTimes(1);
1470
1660
  const [, args] = spawnMock.mock.calls[0];
1471
1661
  expect(args).toEqual([
1472
- "start",
1473
- "cadet",
1474
- `--config=${realNgrok.ngrokConfigPath()}`,
1662
+ "http",
1663
+ "127.0.0.1:7831",
1475
1664
  "--log=stdout",
1665
+ "--domain=foo.ngrok.app",
1476
1666
  ]);
1477
1667
 
1478
1668
  const config = IngressTestConfigSchema.parse(
@@ -1483,7 +1673,7 @@ describe("ngrok --domain spawn args", () => {
1483
1673
  });
1484
1674
  });
1485
1675
 
1486
- describe("tunnel --detach", () => {
1676
+ describe("tunnel background mode", () => {
1487
1677
  const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
1488
1678
 
1489
1679
  function makeFakeChild(pid: number): ChildProcess {
@@ -1505,13 +1695,22 @@ describe("tunnel --detach", () => {
1505
1695
  ): ChildProcess => makeFakeChild(9999),
1506
1696
  );
1507
1697
 
1508
- // spawnDetachedTunnel() scopes the log filename to its own (parent) pid,
1509
- // which in-process is this test runner's pid.
1510
1698
  function detachedLogPath(): string {
1511
- return join(getLogDir(), `tunnel-${process.pid}.log`);
1699
+ return join(getLogDir(), "tunnel.log");
1512
1700
  }
1513
1701
 
1514
1702
  beforeAll(() => {
1703
+ // The edge-targeting describe's afterAll restores the REAL provider
1704
+ // modules; re-establish the fakes the foreground test here needs.
1705
+ mock.module("../lib/tunnels/ngrok", () => ({
1706
+ ...realNgrok,
1707
+ runNgrokTunnel: runNgrokTunnelMock,
1708
+ }));
1709
+ mock.module("../lib/nginx-ingress.js", () => ({
1710
+ ...realNginxIngress,
1711
+ ensureTunnelEdge: ensureTunnelEdgeMock,
1712
+ getNginxVersion: getNginxVersionMock,
1713
+ }));
1515
1714
  mock.module("node:child_process", () => ({
1516
1715
  ...realChildProcess,
1517
1716
  spawn: spawnMock,
@@ -1520,10 +1719,22 @@ describe("tunnel --detach", () => {
1520
1719
 
1521
1720
  afterAll(() => {
1522
1721
  mock.module("node:child_process", () => realChildProcess);
1722
+ mock.module("../lib/tunnels/ngrok", () => realNgrok);
1723
+ mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
1523
1724
  });
1524
1725
 
1525
1726
  beforeEach(() => {
1526
1727
  spawnMock.mockClear();
1728
+ runNgrokTunnelMock.mockReset();
1729
+ runNgrokTunnelMock.mockResolvedValue(
1730
+ fakeTunnelResult("https://test.example.ngrok.dev"),
1731
+ );
1732
+ ensureTunnelEdgeMock.mockReset();
1733
+ ensureTunnelEdgeMock.mockResolvedValue({
1734
+ port: EDGE_PORT,
1735
+ started: true,
1736
+ includesWebApp: true,
1737
+ });
1527
1738
  const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
1528
1739
  tempDirs.push(configHome);
1529
1740
  process.env.XDG_CONFIG_HOME = configHome;
@@ -1541,15 +1752,8 @@ describe("tunnel --detach", () => {
1541
1752
  }
1542
1753
  });
1543
1754
 
1544
- test("--detach re-invokes without the flag as a detached child and reports readiness", async () => {
1545
- process.argv = [
1546
- "bun",
1547
- "cadet",
1548
- "tunnel",
1549
- "--provider",
1550
- "ngrok",
1551
- "--detach",
1552
- ];
1755
+ test("runs detached by default: re-invokes itself as a detached child and reports readiness", async () => {
1756
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1553
1757
 
1554
1758
  // Simulate the detached child writing its readiness line to the log file
1555
1759
  // the moment it is spawned.
@@ -1568,8 +1772,10 @@ describe("tunnel --detach", () => {
1568
1772
  expect(args).toContain("tunnel");
1569
1773
  expect(args).toContain("--provider");
1570
1774
  expect(args).toContain("ngrok");
1571
- expect(args).not.toContain("--detach");
1572
- expect(args).not.toContain("-d");
1775
+ // The child runs the foreground path (it IS the background daemon); the
1776
+ // parent must not hand the default through or the child would daemonize
1777
+ // again.
1778
+ expect(args).toContain("--foreground");
1573
1779
  expect(opts?.detached).toBe(true);
1574
1780
 
1575
1781
  expect(logs).toContain("Tunnel established: https://foo.ngrok.app");
@@ -1582,32 +1788,25 @@ describe("tunnel --detach", () => {
1582
1788
  expect(ensureTunnelEdgeMock).not.toHaveBeenCalled();
1583
1789
  });
1584
1790
 
1585
- test("-d is equivalent to --detach", async () => {
1586
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale", "-d"];
1587
- spawnMock.mockImplementationOnce(() => {
1588
- appendFileSync(
1589
- detachedLogPath(),
1590
- "Tunnel established: https://tailnet.example.ts.net\n",
1591
- );
1592
- return makeFakeChild(1234);
1593
- });
1594
-
1595
- await runTunnelCapturingLogs();
1596
-
1597
- const [, args] = spawnMock.mock.calls[0];
1598
- expect(args).not.toContain("-d");
1599
- expect(args).not.toContain("--detach");
1600
- });
1601
-
1602
- test("reports an early child exit instead of hanging", async () => {
1791
+ test("--foreground runs the tunnel in this process instead of a detached child", async () => {
1603
1792
  process.argv = [
1604
1793
  "bun",
1605
1794
  "cadet",
1606
1795
  "tunnel",
1607
1796
  "--provider",
1608
1797
  "ngrok",
1609
- "--detach",
1798
+ "--foreground",
1610
1799
  ];
1800
+
1801
+ await runTunnelCapturingLogs();
1802
+
1803
+ expect(spawnMock).not.toHaveBeenCalled();
1804
+ expect(ensureTunnelEdgeMock).toHaveBeenCalled();
1805
+ expect(runNgrokTunnelMock).toHaveBeenCalled();
1806
+ });
1807
+
1808
+ test("reports an early child exit instead of hanging", async () => {
1809
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1611
1810
  spawnMock.mockImplementationOnce(() => {
1612
1811
  const child = makeFakeChild(4242);
1613
1812
  // Listeners attach synchronously right after spawn() returns; defer the
@@ -1624,14 +1823,7 @@ describe("tunnel --detach", () => {
1624
1823
  });
1625
1824
 
1626
1825
  test("an adopted ngrok tunnel gets an accurate stop message, not a false kill claim", async () => {
1627
- process.argv = [
1628
- "bun",
1629
- "cadet",
1630
- "tunnel",
1631
- "--provider",
1632
- "ngrok",
1633
- "--detach",
1634
- ];
1826
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1635
1827
  spawnMock.mockImplementationOnce(() => {
1636
1828
  appendFileSync(
1637
1829
  detachedLogPath(),