@mycadet/cli 0.12.9 → 0.12.11

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 (55) hide show
  1. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  5. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  6. package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  7. package/package.json +4 -3
  8. package/src/__tests__/client-web-config.test.ts +0 -1
  9. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  10. package/src/__tests__/devices.test.ts +62 -0
  11. package/src/__tests__/ensure-binary.test.ts +52 -0
  12. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  13. package/src/__tests__/multi-local.test.ts +3 -3
  14. package/src/__tests__/nginx-ingress.test.ts +58 -26
  15. package/src/__tests__/pair.test.ts +149 -78
  16. package/src/__tests__/ps-cloud-status.test.ts +6 -4
  17. package/src/__tests__/setup.test.ts +14 -4
  18. package/src/__tests__/teleport.test.ts +0 -15
  19. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  20. package/src/__tests__/tunnel-stop.test.ts +226 -0
  21. package/src/__tests__/tunnel.test.ts +265 -70
  22. package/src/__tests__/uninstall.test.ts +140 -8
  23. package/src/commands/client.ts +4 -6
  24. package/src/commands/devices.ts +66 -30
  25. package/src/commands/hatch.ts +14 -53
  26. package/src/commands/pair.ts +147 -38
  27. package/src/commands/ps.ts +10 -20
  28. package/src/commands/teleport.ts +2 -21
  29. package/src/commands/tunnel.ts +67 -28
  30. package/src/commands/uninstall.ts +106 -71
  31. package/src/commands/upgrade.ts +6 -1
  32. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  33. package/src/lib/assistant-config.ts +20 -5
  34. package/src/lib/cloud-client.ts +0 -3
  35. package/src/lib/cloud-login-poll.ts +75 -0
  36. package/src/lib/cloud-login.ts +3 -2
  37. package/src/lib/color.ts +4 -0
  38. package/src/lib/docker.ts +46 -70
  39. package/src/lib/ensure-binary.ts +65 -6
  40. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  41. package/src/lib/environments/paths.ts +16 -8
  42. package/src/lib/environments/resolve.ts +2 -4
  43. package/src/lib/hatch-local.ts +16 -59
  44. package/src/lib/hatch-next-steps.ts +18 -9
  45. package/src/lib/live-voice/tunnel.ts +2 -2
  46. package/src/lib/local.ts +42 -9
  47. package/src/lib/nginx-ingress.ts +124 -24
  48. package/src/lib/step-runner.ts +6 -1
  49. package/src/lib/tunnel.ts +86 -34
  50. package/src/lib/tunnels/cadet.ts +12 -1
  51. package/src/lib/tunnels/cloudflare.ts +36 -1
  52. package/src/lib/tunnels/ngrok.ts +16 -2
  53. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  54. package/src/lib/tunnels/tailscale.ts +79 -1
  55. package/src/lib/xdg-log.ts +11 -0
@@ -53,6 +53,10 @@ const runCloudflareTunnelMock = mock<
53
53
  mock.module("../lib/tunnels/cloudflare.js", () => ({
54
54
  ...realCloudflareTunnel,
55
55
  runCloudflareTunnel: runCloudflareTunnelMock,
56
+ CLOUDFLARE_PROVIDER: {
57
+ ...realCloudflareTunnel.CLOUDFLARE_PROVIDER,
58
+ run: runCloudflareTunnelMock,
59
+ },
56
60
  }));
57
61
 
58
62
  const runNgrokTunnelMock = mock<typeof ngrok.runNgrokTunnel>(async () =>
@@ -61,6 +65,7 @@ const runNgrokTunnelMock = mock<typeof ngrok.runNgrokTunnel>(async () =>
61
65
  mock.module("../lib/tunnels/ngrok", () => ({
62
66
  ...realNgrok,
63
67
  runNgrokTunnel: runNgrokTunnelMock,
68
+ NGROK_PROVIDER: { ...realNgrok.NGROK_PROVIDER, run: runNgrokTunnelMock },
64
69
  }));
65
70
 
66
71
  const runTailscaleTunnelMock = mock<typeof tailscaleTunnel.runTailscaleTunnel>(
@@ -69,6 +74,10 @@ const runTailscaleTunnelMock = mock<typeof tailscaleTunnel.runTailscaleTunnel>(
69
74
  mock.module("../lib/tunnels/tailscale.js", () => ({
70
75
  ...realTailscaleTunnel,
71
76
  runTailscaleTunnel: runTailscaleTunnelMock,
77
+ TAILSCALE_PROVIDER: {
78
+ ...realTailscaleTunnel.TAILSCALE_PROVIDER,
79
+ run: runTailscaleTunnelMock,
80
+ },
72
81
  }));
73
82
 
74
83
  const notifyGatewayOfPublicUrlMock = mock<
@@ -245,7 +254,14 @@ async function runTunnelCapturingLogs(): Promise<string> {
245
254
 
246
255
  describe("tunnel edge targeting", () => {
247
256
  beforeEach(() => {
248
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
257
+ process.argv = [
258
+ "bun",
259
+ "cadet",
260
+ "tunnel",
261
+ "--provider",
262
+ "tailscale",
263
+ "--foreground",
264
+ ];
249
265
  writeLockfile(makeLocalEntry());
250
266
  globalThis.fetch = withPreconnect(async () => {
251
267
  throw new Error("gateway unavailable");
@@ -311,7 +327,14 @@ describe("tunnel edge targeting", () => {
311
327
  const entry = makeLocalEntry();
312
328
  entry.runtimeUrl = "https://stale-tunnel.ngrok-free.dev";
313
329
  writeLockfile(entry);
314
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
330
+ process.argv = [
331
+ "bun",
332
+ "cadet",
333
+ "tunnel",
334
+ "--provider",
335
+ "ngrok",
336
+ "--foreground",
337
+ ];
315
338
 
316
339
  const logs = await runTunnelCapturingLogs();
317
340
 
@@ -332,7 +355,14 @@ describe("tunnel edge targeting", () => {
332
355
 
333
356
  test("the tunnel level notifies the gateway of the provider's public URL", async () => {
334
357
  writeLockfile(makeLocalEntry());
335
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
358
+ process.argv = [
359
+ "bun",
360
+ "cadet",
361
+ "tunnel",
362
+ "--provider",
363
+ "ngrok",
364
+ "--foreground",
365
+ ];
336
366
 
337
367
  await runTunnelCapturingLogs();
338
368
 
@@ -348,7 +378,14 @@ describe("tunnel edge targeting", () => {
348
378
  const cloud = makeCloudEntry();
349
379
  const local = makeLocalEntry();
350
380
  writeLockfile([cloud, local], cloud.assistantId);
351
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
381
+ process.argv = [
382
+ "bun",
383
+ "cadet",
384
+ "tunnel",
385
+ "--provider",
386
+ "ngrok",
387
+ "--foreground",
388
+ ];
352
389
 
353
390
  const logs = await runTunnelCapturingLogs();
354
391
 
@@ -380,6 +417,7 @@ describe("tunnel edge targeting", () => {
380
417
  "docker-1",
381
418
  "--provider",
382
419
  "ngrok",
420
+ "--foreground",
383
421
  ];
384
422
 
385
423
  await runTunnelCapturingLogs();
@@ -400,7 +438,14 @@ describe("tunnel edge targeting", () => {
400
438
  const workspaceDir = useTempDefaultWorkspaceDir();
401
439
  const docker = makeDockerEntry();
402
440
  writeLockfile(docker, docker.assistantId);
403
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
441
+ process.argv = [
442
+ "bun",
443
+ "cadet",
444
+ "tunnel",
445
+ "--provider",
446
+ "ngrok",
447
+ "--foreground",
448
+ ];
404
449
 
405
450
  await runTunnelCapturingLogs();
406
451
 
@@ -420,7 +465,14 @@ describe("tunnel edge targeting", () => {
420
465
  const workspaceDir = useTempDefaultWorkspaceDir();
421
466
  const cloud = makeCloudEntry();
422
467
  writeLockfile([cloud, makeDockerEntry()], cloud.assistantId);
423
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
468
+ process.argv = [
469
+ "bun",
470
+ "cadet",
471
+ "tunnel",
472
+ "--provider",
473
+ "ngrok",
474
+ "--foreground",
475
+ ];
424
476
 
425
477
  const logs = await runTunnelCapturingLogs();
426
478
 
@@ -437,7 +489,14 @@ describe("tunnel edge targeting", () => {
437
489
 
438
490
  test("an active cloud assistant with no local entries exits with an error", async () => {
439
491
  writeLockfile(makeCloudEntry());
440
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
492
+ process.argv = [
493
+ "bun",
494
+ "cadet",
495
+ "tunnel",
496
+ "--provider",
497
+ "ngrok",
498
+ "--foreground",
499
+ ];
441
500
 
442
501
  const { exited, errors } = await runTunnelExpectingExit1();
443
502
 
@@ -456,7 +515,14 @@ describe("tunnel edge targeting", () => {
456
515
  [cloud, makeLocalEntry("assistant-a"), makeLocalEntry("assistant-b")],
457
516
  cloud.assistantId,
458
517
  );
459
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
518
+ process.argv = [
519
+ "bun",
520
+ "cadet",
521
+ "tunnel",
522
+ "--provider",
523
+ "ngrok",
524
+ "--foreground",
525
+ ];
460
526
 
461
527
  const { exited, errors } = await runTunnelExpectingExit1();
462
528
 
@@ -474,7 +540,15 @@ describe("tunnel edge targeting", () => {
474
540
  test("a positional cloud assistant name errors without auto-fallback", async () => {
475
541
  const local = makeLocalEntry();
476
542
  writeLockfile([makeCloudEntry(), local], local.assistantId);
477
- process.argv = ["bun", "cadet", "tunnel", "cloud-1", "--provider", "ngrok"];
543
+ process.argv = [
544
+ "bun",
545
+ "cadet",
546
+ "tunnel",
547
+ "cloud-1",
548
+ "--provider",
549
+ "ngrok",
550
+ "--foreground",
551
+ ];
478
552
 
479
553
  const { exited, errors } = await runTunnelExpectingExit1();
480
554
 
@@ -493,7 +567,15 @@ describe("tunnel edge targeting", () => {
493
567
  const local = makeLocalEntry();
494
568
  local.name = "Ada";
495
569
  writeLockfile([makeCloudEntry(), local], "cloud-1");
496
- process.argv = ["bun", "cadet", "tunnel", "Ada", "--provider", "ngrok"];
570
+ process.argv = [
571
+ "bun",
572
+ "cadet",
573
+ "tunnel",
574
+ "Ada",
575
+ "--provider",
576
+ "ngrok",
577
+ "--foreground",
578
+ ];
497
579
 
498
580
  await runTunnelCapturingLogs();
499
581
 
@@ -517,6 +599,7 @@ describe("tunnel edge targeting", () => {
517
599
  "Lovelace",
518
600
  "--provider",
519
601
  "ngrok",
602
+ "--foreground",
520
603
  ];
521
604
 
522
605
  await runTunnelCapturingLogs();
@@ -533,7 +616,15 @@ describe("tunnel edge targeting", () => {
533
616
  const workspaceDir = useTempDefaultWorkspaceDir();
534
617
  const local = makeLocalEntry();
535
618
  writeLockfile([local, makeAppleContainerEntry()], local.assistantId);
536
- process.argv = ["bun", "cadet", "tunnel", "apple-1", "--provider", "ngrok"];
619
+ process.argv = [
620
+ "bun",
621
+ "cadet",
622
+ "tunnel",
623
+ "apple-1",
624
+ "--provider",
625
+ "ngrok",
626
+ "--foreground",
627
+ ];
537
628
 
538
629
  await runTunnelCapturingLogs();
539
630
 
@@ -553,7 +644,14 @@ describe("tunnel edge targeting", () => {
553
644
  const workspaceDir = useTempDefaultWorkspaceDir();
554
645
  const cloud = makeCloudEntry();
555
646
  writeLockfile([cloud, makeAppleContainerEntry()], cloud.assistantId);
556
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
647
+ process.argv = [
648
+ "bun",
649
+ "cadet",
650
+ "tunnel",
651
+ "--provider",
652
+ "ngrok",
653
+ "--foreground",
654
+ ];
557
655
 
558
656
  const logs = await runTunnelCapturingLogs();
559
657
 
@@ -585,6 +683,7 @@ describe("tunnel edge targeting", () => {
585
683
  "assistant-b",
586
684
  "--provider",
587
685
  "ngrok",
686
+ "--foreground",
588
687
  ];
589
688
 
590
689
  await runTunnelCapturingLogs();
@@ -599,7 +698,15 @@ describe("tunnel edge targeting", () => {
599
698
  cloud.name = "Cloudy";
600
699
  const local = makeLocalEntry();
601
700
  writeLockfile([cloud, local], local.assistantId);
602
- process.argv = ["bun", "cadet", "tunnel", "Cloudy", "--provider", "ngrok"];
701
+ process.argv = [
702
+ "bun",
703
+ "cadet",
704
+ "tunnel",
705
+ "Cloudy",
706
+ "--provider",
707
+ "ngrok",
708
+ "--foreground",
709
+ ];
603
710
 
604
711
  const { exited, errors } = await runTunnelExpectingExit1();
605
712
 
@@ -620,7 +727,15 @@ describe("tunnel edge targeting", () => {
620
727
  const second = makeLocalEntry("assistant-b");
621
728
  second.name = "Ada";
622
729
  writeLockfile([first, second], "assistant-a");
623
- process.argv = ["bun", "cadet", "tunnel", "Ada", "--provider", "ngrok"];
730
+ process.argv = [
731
+ "bun",
732
+ "cadet",
733
+ "tunnel",
734
+ "Ada",
735
+ "--provider",
736
+ "ngrok",
737
+ "--foreground",
738
+ ];
624
739
 
625
740
  const { exited, errors } = await runTunnelExpectingExit1();
626
741
 
@@ -633,7 +748,15 @@ describe("tunnel edge targeting", () => {
633
748
  });
634
749
 
635
750
  test("an unknown positional name errors", async () => {
636
- process.argv = ["bun", "cadet", "tunnel", "nope", "--provider", "ngrok"];
751
+ process.argv = [
752
+ "bun",
753
+ "cadet",
754
+ "tunnel",
755
+ "nope",
756
+ "--provider",
757
+ "ngrok",
758
+ "--foreground",
759
+ ];
637
760
 
638
761
  const { exited, errors } = await runTunnelExpectingExit1();
639
762
 
@@ -646,7 +769,14 @@ describe("tunnel edge targeting", () => {
646
769
  test("targets the edge port for cloudflare", async () => {
647
770
  const entry = makeLocalEntry();
648
771
  writeLockfile(entry);
649
- process.argv = ["bun", "cadet", "tunnel", "--provider", "cloudflare"];
772
+ process.argv = [
773
+ "bun",
774
+ "cadet",
775
+ "tunnel",
776
+ "--provider",
777
+ "cloudflare",
778
+ "--foreground",
779
+ ];
650
780
 
651
781
  await runTunnelCapturingLogs();
652
782
 
@@ -661,7 +791,14 @@ describe("tunnel edge targeting", () => {
661
791
  test("targets the edge port for tailscale and notes a reused webhooks-only edge", async () => {
662
792
  const entry = makeLocalEntry();
663
793
  writeLockfile(entry);
664
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
794
+ process.argv = [
795
+ "bun",
796
+ "cadet",
797
+ "tunnel",
798
+ "--provider",
799
+ "tailscale",
800
+ "--foreground",
801
+ ];
665
802
  ensureTunnelEdgeMock.mockResolvedValue({
666
803
  port: EDGE_PORT,
667
804
  started: false,
@@ -681,7 +818,7 @@ describe("tunnel edge targeting", () => {
681
818
  test("a bare invocation tunnels the default provider", async () => {
682
819
  const entry = makeLocalEntry();
683
820
  writeLockfile(entry);
684
- process.argv = ["bun", "cadet", "tunnel"];
821
+ process.argv = ["bun", "cadet", "tunnel", "--foreground"];
685
822
 
686
823
  const logs = await runTunnelCapturingLogs();
687
824
 
@@ -700,7 +837,7 @@ describe("tunnel edge targeting", () => {
700
837
  const entry = makeLocalEntry();
701
838
  entry.name = "Ada";
702
839
  writeLockfile(entry);
703
- process.argv = ["bun", "cadet", "tunnel", "Ada"];
840
+ process.argv = ["bun", "cadet", "tunnel", "Ada", "--foreground"];
704
841
 
705
842
  const logs = await runTunnelCapturingLogs();
706
843
 
@@ -720,7 +857,14 @@ describe("tunnel edge targeting", () => {
720
857
  const workspaceDir = writeEntryWorkspaceConfig(entry, {
721
858
  telegram: { botUsername: "example_bot" },
722
859
  });
723
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
860
+ process.argv = [
861
+ "bun",
862
+ "cadet",
863
+ "tunnel",
864
+ "--provider",
865
+ "tailscale",
866
+ "--foreground",
867
+ ];
724
868
 
725
869
  const warnings: string[] = [];
726
870
  const warnSpy = spyOn(console, "warn").mockImplementation(
@@ -751,7 +895,14 @@ describe("tunnel edge targeting", () => {
751
895
  const entry = makeLocalEntry();
752
896
  writeLockfile(entry);
753
897
  const workspaceDir = writeEntryWorkspaceConfig(entry, {});
754
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale"];
898
+ process.argv = [
899
+ "bun",
900
+ "cadet",
901
+ "tunnel",
902
+ "--provider",
903
+ "tailscale",
904
+ "--foreground",
905
+ ];
755
906
 
756
907
  await runTunnelCapturingLogs();
757
908
 
@@ -768,7 +919,14 @@ describe("tunnel edge targeting", () => {
768
919
  const workspaceDir = writeEntryWorkspaceConfig(entry, {
769
920
  telegram: { botUsername: "example_bot" },
770
921
  });
771
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
922
+ process.argv = [
923
+ "bun",
924
+ "cadet",
925
+ "tunnel",
926
+ "--provider",
927
+ "ngrok",
928
+ "--foreground",
929
+ ];
772
930
 
773
931
  await runTunnelCapturingLogs();
774
932
 
@@ -780,7 +938,14 @@ describe("tunnel edge targeting", () => {
780
938
  });
781
939
 
782
940
  test("missing nginx aborts before any provider spawn", async () => {
783
- process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
941
+ process.argv = [
942
+ "bun",
943
+ "cadet",
944
+ "tunnel",
945
+ "--provider",
946
+ "ngrok",
947
+ "--foreground",
948
+ ];
784
949
  ensureTunnelEdgeMock.mockRejectedValue(
785
950
  new Error(
786
951
  "nginx is not installed, so the tunnel edge cannot start. " +
@@ -801,7 +966,14 @@ describe("tunnel edge targeting", () => {
801
966
  });
802
967
 
803
968
  test("rejects an unknown --provider with a stale-CLI hint", async () => {
804
- process.argv = ["bun", "cadet", "tunnel", "--provider", "bogus"];
969
+ process.argv = [
970
+ "bun",
971
+ "cadet",
972
+ "tunnel",
973
+ "--provider",
974
+ "bogus",
975
+ "--foreground",
976
+ ];
805
977
 
806
978
  const { exited, errors } = await runTunnelExpectingExit1();
807
979
 
@@ -818,7 +990,14 @@ describe("tunnel edge targeting", () => {
818
990
  try {
819
991
  for (const provider of TUNNEL_PROVIDERS) {
820
992
  writeLockfile(makeLocalEntry());
821
- process.argv = ["bun", "cadet", "tunnel", "--provider", provider];
993
+ process.argv = [
994
+ "bun",
995
+ "cadet",
996
+ "tunnel",
997
+ "--provider",
998
+ provider,
999
+ "--foreground",
1000
+ ];
822
1001
 
823
1002
  const { exited, errors } = await runTunnelExpectingExit1();
824
1003
 
@@ -841,6 +1020,7 @@ describe("tunnel edge targeting", () => {
841
1020
  "ngrok",
842
1021
  "--domain",
843
1022
  "foo.ngrok.app",
1023
+ "--foreground",
844
1024
  ];
845
1025
 
846
1026
  await runTunnelCapturingLogs();
@@ -862,6 +1042,7 @@ describe("tunnel edge targeting", () => {
862
1042
  "cloudflare",
863
1043
  "--domain",
864
1044
  "foo.ngrok.app",
1045
+ "--foreground",
865
1046
  ];
866
1047
 
867
1048
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -887,6 +1068,7 @@ describe("tunnel edge targeting", () => {
887
1068
  "--provider",
888
1069
  "ngrok",
889
1070
  "--clear-domain",
1071
+ "--foreground",
890
1072
  ];
891
1073
 
892
1074
  const logs = await runTunnelCapturingLogs();
@@ -912,6 +1094,7 @@ describe("tunnel edge targeting", () => {
912
1094
  "--domain",
913
1095
  "foo.ngrok.app",
914
1096
  "--clear-domain",
1097
+ "--foreground",
915
1098
  ];
916
1099
 
917
1100
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -929,6 +1112,7 @@ describe("tunnel edge targeting", () => {
929
1112
  "--provider",
930
1113
  "cloudflare",
931
1114
  "--clear-domain",
1115
+ "--foreground",
932
1116
  ];
933
1117
 
934
1118
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -949,6 +1133,7 @@ describe("tunnel edge targeting", () => {
949
1133
  "--provider",
950
1134
  "ngrok",
951
1135
  "--domain",
1136
+ "--foreground",
952
1137
  ];
953
1138
 
954
1139
  const { exited, errors } = await runTunnelExpectingExit1();
@@ -961,7 +1146,14 @@ describe("tunnel edge targeting", () => {
961
1146
  test("a not-yet-implemented provider error carries the stale-CLI hint", async () => {
962
1147
  // `cadet` stays valid-but-unimplemented, so it hits this path rather
963
1148
  // than the unknown-provider one.
964
- process.argv = ["bun", "cadet", "tunnel", "--provider", "cadet"];
1149
+ process.argv = [
1150
+ "bun",
1151
+ "cadet",
1152
+ "tunnel",
1153
+ "--provider",
1154
+ "cadet",
1155
+ "--foreground",
1156
+ ];
965
1157
 
966
1158
  let err: Error | undefined;
967
1159
  try {
@@ -1490,7 +1682,7 @@ describe("ngrok --domain spawn args", () => {
1490
1682
  });
1491
1683
  });
1492
1684
 
1493
- describe("tunnel --detach", () => {
1685
+ describe("tunnel background mode", () => {
1494
1686
  const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
1495
1687
 
1496
1688
  function makeFakeChild(pid: number): ChildProcess {
@@ -1512,13 +1704,23 @@ describe("tunnel --detach", () => {
1512
1704
  ): ChildProcess => makeFakeChild(9999),
1513
1705
  );
1514
1706
 
1515
- // spawnDetachedTunnel() scopes the log filename to its own (parent) pid,
1516
- // which in-process is this test runner's pid.
1517
1707
  function detachedLogPath(): string {
1518
- return join(getLogDir(), `tunnel-${process.pid}.log`);
1708
+ return join(getLogDir(), "tunnel.log");
1519
1709
  }
1520
1710
 
1521
1711
  beforeAll(() => {
1712
+ // The edge-targeting describe's afterAll restores the REAL provider
1713
+ // modules; re-establish the fakes the foreground test here needs.
1714
+ mock.module("../lib/tunnels/ngrok", () => ({
1715
+ ...realNgrok,
1716
+ runNgrokTunnel: runNgrokTunnelMock,
1717
+ NGROK_PROVIDER: { ...realNgrok.NGROK_PROVIDER, run: runNgrokTunnelMock },
1718
+ }));
1719
+ mock.module("../lib/nginx-ingress.js", () => ({
1720
+ ...realNginxIngress,
1721
+ ensureTunnelEdge: ensureTunnelEdgeMock,
1722
+ getNginxVersion: getNginxVersionMock,
1723
+ }));
1522
1724
  mock.module("node:child_process", () => ({
1523
1725
  ...realChildProcess,
1524
1726
  spawn: spawnMock,
@@ -1527,10 +1729,22 @@ describe("tunnel --detach", () => {
1527
1729
 
1528
1730
  afterAll(() => {
1529
1731
  mock.module("node:child_process", () => realChildProcess);
1732
+ mock.module("../lib/tunnels/ngrok", () => realNgrok);
1733
+ mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
1530
1734
  });
1531
1735
 
1532
1736
  beforeEach(() => {
1533
1737
  spawnMock.mockClear();
1738
+ runNgrokTunnelMock.mockReset();
1739
+ runNgrokTunnelMock.mockResolvedValue(
1740
+ fakeTunnelResult("https://test.example.ngrok.dev"),
1741
+ );
1742
+ ensureTunnelEdgeMock.mockReset();
1743
+ ensureTunnelEdgeMock.mockResolvedValue({
1744
+ port: EDGE_PORT,
1745
+ started: true,
1746
+ includesWebApp: true,
1747
+ });
1534
1748
  const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
1535
1749
  tempDirs.push(configHome);
1536
1750
  process.env.XDG_CONFIG_HOME = configHome;
@@ -1548,15 +1762,8 @@ describe("tunnel --detach", () => {
1548
1762
  }
1549
1763
  });
1550
1764
 
1551
- test("--detach re-invokes without the flag as a detached child and reports readiness", async () => {
1552
- process.argv = [
1553
- "bun",
1554
- "cadet",
1555
- "tunnel",
1556
- "--provider",
1557
- "ngrok",
1558
- "--detach",
1559
- ];
1765
+ test("runs detached by default: re-invokes itself as a detached child and reports readiness", async () => {
1766
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1560
1767
 
1561
1768
  // Simulate the detached child writing its readiness line to the log file
1562
1769
  // the moment it is spawned.
@@ -1575,13 +1782,15 @@ describe("tunnel --detach", () => {
1575
1782
  expect(args).toContain("tunnel");
1576
1783
  expect(args).toContain("--provider");
1577
1784
  expect(args).toContain("ngrok");
1578
- expect(args).not.toContain("--detach");
1579
- expect(args).not.toContain("-d");
1785
+ // The child runs the foreground path (it IS the background daemon); the
1786
+ // parent must not hand the default through or the child would daemonize
1787
+ // again.
1788
+ expect(args).toContain("--foreground");
1580
1789
  expect(opts?.detached).toBe(true);
1581
1790
 
1582
1791
  expect(logs).toContain("Tunnel established: https://foo.ngrok.app");
1583
1792
  expect(logs).toContain("Running in background (pid 9999)");
1584
- expect(logs).toContain("Stop with: kill 9999");
1793
+ expect(logs).toContain("Stop with: cadet tunnel stop");
1585
1794
 
1586
1795
  // The parent hands the actual tunnel workflow off to the child: it never
1587
1796
  // runs the provider or edge setup itself.
@@ -1589,32 +1798,25 @@ describe("tunnel --detach", () => {
1589
1798
  expect(ensureTunnelEdgeMock).not.toHaveBeenCalled();
1590
1799
  });
1591
1800
 
1592
- test("-d is equivalent to --detach", async () => {
1593
- process.argv = ["bun", "cadet", "tunnel", "--provider", "tailscale", "-d"];
1594
- spawnMock.mockImplementationOnce(() => {
1595
- appendFileSync(
1596
- detachedLogPath(),
1597
- "Tunnel established: https://tailnet.example.ts.net\n",
1598
- );
1599
- return makeFakeChild(1234);
1600
- });
1601
-
1602
- await runTunnelCapturingLogs();
1603
-
1604
- const [, args] = spawnMock.mock.calls[0];
1605
- expect(args).not.toContain("-d");
1606
- expect(args).not.toContain("--detach");
1607
- });
1608
-
1609
- test("reports an early child exit instead of hanging", async () => {
1801
+ test("--foreground runs the tunnel in this process instead of a detached child", async () => {
1610
1802
  process.argv = [
1611
1803
  "bun",
1612
1804
  "cadet",
1613
1805
  "tunnel",
1614
1806
  "--provider",
1615
1807
  "ngrok",
1616
- "--detach",
1808
+ "--foreground",
1617
1809
  ];
1810
+
1811
+ await runTunnelCapturingLogs();
1812
+
1813
+ expect(spawnMock).not.toHaveBeenCalled();
1814
+ expect(ensureTunnelEdgeMock).toHaveBeenCalled();
1815
+ expect(runNgrokTunnelMock).toHaveBeenCalled();
1816
+ });
1817
+
1818
+ test("reports an early child exit instead of hanging", async () => {
1819
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1618
1820
  spawnMock.mockImplementationOnce(() => {
1619
1821
  const child = makeFakeChild(4242);
1620
1822
  // Listeners attach synchronously right after spawn() returns; defer the
@@ -1626,19 +1828,12 @@ describe("tunnel --detach", () => {
1626
1828
  const { exited, errors } = await runTunnelExpectingExit1();
1627
1829
 
1628
1830
  expect(exited).toBe(true);
1629
- expect(errors).toContain("exited during startup (exit code 1)");
1831
+ expect(errors).toContain("Error (1): tunnel process exited during startup");
1630
1832
  expect(runNgrokTunnelMock).not.toHaveBeenCalled();
1631
1833
  });
1632
1834
 
1633
1835
  test("an adopted ngrok tunnel gets an accurate stop message, not a false kill claim", async () => {
1634
- process.argv = [
1635
- "bun",
1636
- "cadet",
1637
- "tunnel",
1638
- "--provider",
1639
- "ngrok",
1640
- "--detach",
1641
- ];
1836
+ process.argv = ["bun", "cadet", "tunnel", "--provider", "ngrok"];
1642
1837
  spawnMock.mockImplementationOnce(() => {
1643
1838
  appendFileSync(
1644
1839
  detachedLogPath(),