@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
@@ -1,4 +1,4 @@
1
- import { isManagedCloud } from "../lib/managed-cloud.js";
1
+ import { isCloudHome } from "../lib/cloud-values.js";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "path";
4
4
 
@@ -14,54 +14,50 @@ import {
14
14
  type AssistantEntry,
15
15
  } from "../lib/assistant-config";
16
16
  import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
17
- import { runCloudflareTunnel } from "../lib/cloudflare-tunnel.js";
18
17
  import { relaunchDetached } from "../lib/detached-process.js";
19
18
  import {
20
19
  getDefaultWorkspaceDir,
21
20
  isLocalContainerEntry,
22
21
  parseGatewayPortFromEntryUrls,
23
- saveNgrokDomain,
24
22
  } from "../lib/ingress-config.js";
25
23
  import {
26
24
  ensureTunnelEdge,
27
- formatEdgeMode,
25
+ getNginxVersion,
28
26
  type TunnelEdge,
29
27
  } from "../lib/nginx-ingress.js";
30
- import { hasWebhookIntegrationsConfigured, runNgrokTunnel } from "../lib/ngrok";
31
- import { shellArg } from "../lib/shell-arg.js";
28
+ import { ensureBinaryInstalled } from "../lib/ensure-binary.js";
32
29
  import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
33
- import { runTailscaleTunnel } from "../lib/tailscale-tunnel.js";
34
-
35
- // `cadet` is the managed option this command owns; the local providers come
36
- // from the shared registry so validation here cannot drift from what the
37
- // workspace config accepts.
38
- const VALID_PROVIDERS = ["cadet", ...TUNNEL_PROVIDERS] as const;
39
- type TunnelProvider = (typeof VALID_PROVIDERS)[number];
40
-
41
- // How far each provider's URL reaches, for the chooser a missing `--provider`
42
- // prints. Keyed off the registry so a new provider cannot ship unoffered.
43
- const PROVIDER_REACH: Record<TunnelProviderName, string> = {
44
- ngrok: "Public tunnel that webhook integrations can reach",
45
- cloudflare: "Public tunnel that webhook integrations can reach",
46
- tailscale: "Tailnet-only tunnel, reachable from your own devices",
47
- };
30
+ import {
31
+ ADOPTED_TUNNEL_RE,
32
+ DEFAULT_TUNNEL_PROVIDER,
33
+ PROVIDER_META,
34
+ adoptedTunnelStopNotice,
35
+ cadetTunnelComingSoonError,
36
+ clearReservedDomain,
37
+ hasWebhookIntegrationsConfigured,
38
+ providerSupportsReservedDomain,
39
+ runProviderTunnel,
40
+ } from "../lib/tunnel.js";
41
+
42
+ // The accepted `--provider` values ARE the shared registry, so validation
43
+ // here cannot drift from what the workspace config accepts.
48
44
 
49
45
  interface TunnelArgs {
50
46
  assistantName: string | null;
51
- provider: TunnelProvider;
47
+ provider: TunnelProviderName;
52
48
  domain: string | null;
53
49
  clearDomain: boolean;
54
- detach: boolean;
50
+ foreground: boolean;
55
51
  }
56
52
 
57
53
  const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
58
54
 
59
55
  function parseArgs(): TunnelArgs {
60
56
  const args = process.argv.slice(3);
61
- let provider: TunnelProvider | null = null;
57
+ let provider: TunnelProviderName | null = null;
62
58
  let domain: string | null = null;
63
59
  let clearDomain = false;
64
- let detach = false;
60
+ let foreground = false;
65
61
 
66
62
  for (let i = 0; i < args.length; i++) {
67
63
  const arg = args[i];
@@ -76,119 +72,115 @@ function parseArgs(): TunnelArgs {
76
72
  );
77
73
  console.log("");
78
74
  console.log(
79
- "How far that URL reaches depends on the provider, so --provider is required.",
75
+ "How far that URL reaches depends on the provider; omit --provider for",
80
76
  );
81
77
  console.log(
82
- "A tailscale tunnel is reachable only from your own tailnet; webhook",
78
+ "the default. A tailscale tunnel is reachable only from your",
83
79
  );
84
80
  console.log(
85
- "integrations need a public tunnel: --provider ngrok or --provider cloudflare.",
81
+ "own tailnet; webhook integrations need a public tunnel. See Providers",
86
82
  );
83
+ console.log("below.");
87
84
  console.log("");
88
85
  console.log(
89
86
  "The tunnel always fronts the local nginx edge, which is started automatically.",
90
87
  );
91
88
  console.log(
92
- "nginx must be installed (macOS: brew install nginx, Linux: sudo apt install nginx).",
89
+ "nginx must be installed; if it is missing, cadet offers to install it.",
93
90
  );
94
91
  console.log("");
95
92
  console.log("Arguments:");
96
93
  console.log(
97
- " <name> Name of the assistant (defaults to active or only local)",
94
+ " <name> Name of the assistant, defaults to active or only local",
98
95
  );
99
96
  console.log("");
100
97
  console.log("Options:");
101
98
  console.log(
102
- ` --provider <provider> Required. Tunnel provider: ${VALID_PROVIDERS.join(", ")}`,
103
- );
104
- console.log(
105
- " --domain <domain> Reserved ngrok domain to bind (ngrok provider only).",
106
- );
107
- console.log(
108
- " Saved to the workspace config so `cadet wake` restores reuse it.",
109
- );
110
- console.log(
111
- " --clear-domain Clear the saved ngrok domain (ngrok provider only) and tunnel",
112
- );
113
- console.log(
114
- " without one. Cannot be combined with --domain.",
115
- );
116
- console.log(
117
- ` -d, --detach Run the tunnel in the background. Waits up to ${TUNNEL_BACKGROUND_START_TIMEOUT_MS / 1000}s for the`,
118
- );
119
- console.log(
120
- " initial established/failed status before returning; logs go to",
121
- );
122
- console.log(
123
- " the CLI log directory. Stop it with `kill <pid>`, except when it",
99
+ ` --provider <provider> Tunnel provider: ${TUNNEL_PROVIDERS.join(", ")} — default ${DEFAULT_TUNNEL_PROVIDER}`,
124
100
  );
125
101
  console.log(
126
- " adopted an already-running ngrok tunnel (started outside this",
102
+ " --domain <domain> Reserved domain to bind, for providers that support",
127
103
  );
128
104
  console.log(
129
- " command): killing the supervisor then leaves that tunnel running.",
105
+ " one. Saved to the workspace config so `cadet wake` restores",
130
106
  );
131
- console.log("");
132
- console.log("Providers:");
107
+ console.log(" reuse it.");
133
108
  console.log(
134
- " cadet Managed tunnel via Cadet Cloud (not yet available)",
109
+ " --clear-domain Clear the saved reserved domain (same providers) and",
135
110
  );
136
111
  console.log(
137
- " ngrok ngrok tunnel — install: brew install ngrok/ngrok/ngrok",
112
+ " tunnel without one. Cannot be combined with --domain.",
138
113
  );
139
114
  console.log(
140
- " cloudflare Cloudflare quick tunnel — install: brew install cloudflare/cloudflare/cloudflared",
115
+ ` --foreground Stream the tunnel's logs to this terminal. By default the tunnel`,
141
116
  );
142
117
  console.log(
143
- " No Cloudflare account required for quick tunnels.",
118
+ " runs in the background: the command returns after printing the public",
144
119
  );
145
120
  console.log(
146
- " tailscale Tailscale serve. Install: https://tailscale.com/download, then run `tailscale up`",
121
+ ` URL, and logs stream to tunnel.log in the CLI log directory. A background`,
147
122
  );
148
123
  console.log(
149
- " Reachable only from your own tailnet (private; LetsEncrypt cert).",
124
+ " tunnel stops with `kill <pid>`, except when it adopted an already-running",
150
125
  );
151
126
  console.log(
152
- " When webhook integrations are configured it records its URL for device",
127
+ " tunnel (started outside this command): killing the supervisor then",
153
128
  );
154
129
  console.log(
155
- " pairing only, leaving the saved webhook callback base as it is.",
130
+ " leaves that tunnel running.",
156
131
  );
157
132
  console.log("");
133
+ console.log("Providers:");
134
+ for (const name of TUNNEL_PROVIDERS) {
135
+ const meta = PROVIDER_META[name];
136
+ console.log(` ${name.padEnd(12)}${meta.reach}`);
137
+ console.log(` ${" ".repeat(12)}${meta.install}`);
138
+ for (const note of meta.notes ?? []) {
139
+ console.log(` ${" ".repeat(12)}${note}`);
140
+ }
141
+ }
142
+ console.log("");
158
143
  console.log("Examples:");
159
- console.log(" $ cadet tunnel --provider ngrok");
160
- console.log(
161
- " $ cadet tunnel --provider ngrok --domain my-assistant.ngrok.app",
162
- );
163
- console.log(" $ cadet tunnel --provider ngrok --clear-domain");
144
+ console.log(` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER}`);
145
+ const domainExample =
146
+ PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
147
+ if (domainExample) {
148
+ console.log(
149
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --domain ${domainExample}`,
150
+ );
151
+ console.log(
152
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --clear-domain`,
153
+ );
154
+ }
164
155
  console.log(" $ cadet tunnel --provider cloudflare");
165
156
  console.log(" $ cadet tunnel my-assistant --provider tailscale");
166
- console.log(" $ cadet tunnel --provider ngrok --detach");
167
157
  process.exit(0);
168
158
  } else if (arg === "--provider") {
169
159
  const next = args[i + 1];
170
160
  if (!next || next.startsWith("-")) {
171
161
  console.error(
172
- `Error: --provider requires one of: ${VALID_PROVIDERS.join(", ")}`,
162
+ `Error: --provider requires one of: ${TUNNEL_PROVIDERS.join(", ")}`,
173
163
  );
174
164
  process.exit(1);
175
165
  }
176
- if (!VALID_PROVIDERS.includes(next as TunnelProvider)) {
166
+ if (!TUNNEL_PROVIDERS.includes(next as TunnelProviderName)) {
177
167
  console.error(
178
- `Error: unknown tunnel provider '${next}'. Valid providers: ${VALID_PROVIDERS.join(", ")}.`,
168
+ `Error: unknown tunnel provider '${next}'. Valid providers: ${TUNNEL_PROVIDERS.join(", ")}.`,
179
169
  );
180
170
  console.error(
181
171
  `If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
182
172
  );
183
173
  process.exit(1);
184
174
  }
185
- provider = next as TunnelProvider;
175
+ provider = next as TunnelProviderName;
186
176
  i++;
187
177
  } else if (arg === "--domain") {
188
178
  const next = args[i + 1];
189
179
  if (!next || next.startsWith("-")) {
180
+ const domainExample =
181
+ PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
190
182
  console.error(
191
- "Error: --domain requires a value, e.g. --domain my-assistant.ngrok.app",
183
+ `Error: --domain requires a value${domainExample ? `, e.g. --domain ${domainExample}` : ""}`,
192
184
  );
193
185
  process.exit(1);
194
186
  }
@@ -196,8 +188,8 @@ function parseArgs(): TunnelArgs {
196
188
  i++;
197
189
  } else if (arg === "--clear-domain") {
198
190
  clearDomain = true;
199
- } else if (arg === "-d" || arg === "--detach") {
200
- detach = true;
191
+ } else if (arg === "--foreground") {
192
+ foreground = true;
201
193
  } else if (arg.startsWith("-")) {
202
194
  console.error(`Error: Unknown option '${arg}'.`);
203
195
  process.exit(1);
@@ -210,32 +202,20 @@ function parseArgs(): TunnelArgs {
210
202
  parseAssistantTargetArg(args, FLAGS_WITH_VALUES) ?? null;
211
203
 
212
204
  if (provider === null) {
213
- console.error(
214
- "Error: --provider is required. How far the tunnel URL reaches depends on it, so there is no default.",
215
- );
216
- console.error("");
217
- console.error("Run one of:");
218
- // Carries the assistant through: dropping it would suggest a command that
219
- // tunnels the active assistant instead of the one that was asked for.
220
- const target = assistantName ? ` ${shellArg(assistantName)}` : "";
221
- for (const name of TUNNEL_PROVIDERS) {
222
- console.error(
223
- ` cadet tunnel${target} --provider ${name.padEnd(13)}${PROVIDER_REACH[name]}`,
224
- );
225
- }
226
- process.exit(1);
205
+ provider = DEFAULT_TUNNEL_PROVIDER;
206
+ console.log(`Using the default provider: ${provider}.`);
227
207
  }
228
208
 
229
- if (domain && provider !== "ngrok") {
209
+ if (domain && !providerSupportsReservedDomain(provider)) {
230
210
  console.error(
231
- `Error: --domain is only supported with --provider ngrok (got '${provider}').`,
211
+ `Error: --domain is not supported with --provider ${provider} (reserved domains have no equivalent there).`,
232
212
  );
233
213
  process.exit(1);
234
214
  }
235
215
 
236
- if (clearDomain && provider !== "ngrok") {
216
+ if (clearDomain && !providerSupportsReservedDomain(provider)) {
237
217
  console.error(
238
- `Error: --clear-domain is only supported with --provider ngrok (got '${provider}').`,
218
+ `Error: --clear-domain is not supported with --provider ${provider} (reserved domains have no equivalent there).`,
239
219
  );
240
220
  process.exit(1);
241
221
  }
@@ -247,7 +227,7 @@ function parseArgs(): TunnelArgs {
247
227
  process.exit(1);
248
228
  }
249
229
 
250
- return { assistantName, provider, domain, clearDomain, detach };
230
+ return { assistantName, provider, domain, clearDomain, foreground };
251
231
  }
252
232
 
253
233
  /** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
@@ -259,7 +239,7 @@ interface LocalTunnelTarget {
259
239
 
260
240
  /**
261
241
  * Map an entry to its local tunnel target, or null when it has no locally
262
- * reachable gateway (e.g. platform-hosted). Entries with `resources` carry
242
+ * reachable gateway (e.g. cloud-hosted). Entries with `resources` carry
263
243
  * their own gateway port and instance workspace. Local container entries
264
244
  * (docker, apple-container) run locally without host resources: their gateway
265
245
  * port comes from localUrl/runtimeUrl and their ingress state lives in the
@@ -284,7 +264,7 @@ function toLocalTunnelTarget(entry: AssistantEntry): LocalTunnelTarget | null {
284
264
 
285
265
  function describeUntunnelableEntry(entry: AssistantEntry): string {
286
266
  const reference = formatAssistantReference(entry);
287
- return isManagedCloud(entry.cloud)
267
+ return isCloudHome(entry.cloud)
288
268
  ? `Assistant '${reference}' runs on Cadet Cloud and needs no tunnel.`
289
269
  : `Assistant '${reference}' has no locally managed runtime to tunnel.`;
290
270
  }
@@ -292,7 +272,7 @@ function describeUntunnelableEntry(entry: AssistantEntry): string {
292
272
  /**
293
273
  * Resolve the assistant whose gateway and workspace the tunnel edge fronts.
294
274
  * Tunnels only make sense for assistants with a local gateway; when the
295
- * resolved entry has none (e.g. the active assistant is platform-hosted) and
275
+ * resolved entry has none (e.g. the active assistant is cloud-hosted) and
296
276
  * no name was given, fall back to the sole local target, otherwise exit with
297
277
  * an error naming the local assistants to pass explicitly.
298
278
  */
@@ -350,8 +330,8 @@ function guardTailnetOnlyWebhookIngress(workspaceDir: string): boolean {
350
330
  console.warn(
351
331
  "⚠ Webhook integrations are configured, and the tailscale tunnel is reachable only from your own tailnet, " +
352
332
  "so it cannot serve their callbacks. The saved ingress base URL stays as it is and the tailnet URL is " +
353
- "recorded for device pairing only. For a tunnel those integrations can reach too, use --provider ngrok " +
354
- "or --provider cloudflare.",
333
+ "recorded for device pairing only. For a tunnel those integrations can reach too, run `cadet tunnel --help` " +
334
+ "to see the public providers.",
355
335
  );
356
336
  return true;
357
337
  }
@@ -359,38 +339,37 @@ function guardTailnetOnlyWebhookIngress(workspaceDir: string): boolean {
359
339
  // Generous cap so a slow provider handshake (e.g. cloudflared's own ~30s
360
340
  // quick-tunnel timeout) still counts as a successful, if slow, start.
361
341
  const TUNNEL_BACKGROUND_START_TIMEOUT_MS = 35_000;
362
- // Matches the "just established" line every provider prints, or ngrok's
363
- // "found an already-running agent" adoption line (captured separately since
364
- // that case does not own the tunnel it adopted).
342
+ // Matches the "just established" line every provider prints, or the adoption
343
+ // line printed when a run adopts an already-running agent's tunnel (captured
344
+ // separately since that case does not own the tunnel it adopted).
365
345
  const TUNNEL_ESTABLISHED_RE = /Tunnel established: (\S+)/;
366
- const TUNNEL_ADOPTED_RE = /Found existing ngrok tunnel: (\S+)/;
367
346
 
368
347
  /**
369
- * Re-invoke `cadet tunnel` (without `-d`/`--detach`) as a detached background
370
- * process via `relaunchDetached`, waiting for the child to either print its
371
- * "Tunnel established"/"Found existing ngrok tunnel" line (readiness) or
372
- * exit early (failure), so a misconfigured provider still fails loudly in
373
- * the terminal that ran `-d`.
348
+ * Re-invoke `cadet tunnel --foreground` as a detached background process via
349
+ * `relaunchDetached`, waiting for the child to either print its
350
+ * "Tunnel established" or adopted-tunnel line (readiness) or exit early
351
+ * (failure), so a misconfigured provider still fails loudly in the terminal
352
+ * that ran `cadet tunnel`.
374
353
  *
375
- * The log filename is scoped to this (parent) process's pid so concurrent
376
- * `-d` runs (e.g. tunneling two different local assistants at once) don't
377
- * truncate and read each other's readiness output.
354
+ * The log file is the fixed `tunnel.log` so there is one stable path to tail;
355
+ * concurrent tunnel runs share (and truncate) it.
378
356
  */
379
357
  async function spawnDetachedTunnel(): Promise<void> {
380
358
  const childArgs: string[] = ["tunnel"];
381
359
  for (const arg of process.argv.slice(3)) {
382
- if (arg === "-d" || arg === "--detach") {
360
+ if (arg === "--foreground") {
383
361
  continue;
384
362
  }
385
363
  childArgs.push(arg);
386
364
  }
365
+ childArgs.push("--foreground");
387
366
 
388
367
  let publicUrl: string | undefined;
389
368
  let adopted = false;
390
369
 
391
370
  const { child, logPath, ready, exitCode } = await relaunchDetached({
392
371
  args: childArgs,
393
- logFile: `tunnel-${process.pid}.log`,
372
+ logFile: "tunnel.log",
394
373
  timeoutMs: TUNNEL_BACKGROUND_START_TIMEOUT_MS,
395
374
  isReady: (logPath) => {
396
375
  let logText = "";
@@ -404,7 +383,7 @@ async function spawnDetachedTunnel(): Promise<void> {
404
383
  publicUrl = established[1];
405
384
  return true;
406
385
  }
407
- const adoptedMatch = TUNNEL_ADOPTED_RE.exec(logText);
386
+ const adoptedMatch = ADOPTED_TUNNEL_RE.exec(logText);
408
387
  if (adoptedMatch) {
409
388
  publicUrl = adoptedMatch[1];
410
389
  adopted = true;
@@ -430,11 +409,7 @@ async function spawnDetachedTunnel(): Promise<void> {
430
409
  }
431
410
  console.log(`Running in background (pid ${child.pid}). Logs: ${logPath}`);
432
411
  if (adopted) {
433
- console.log(
434
- `That ngrok agent was already running outside this command, so \`kill ${child.pid}\` only ` +
435
- "stops this supervisor, not the ngrok tunnel or its saved ingress URL. Stop the ngrok agent " +
436
- "directly to end the tunnel.",
437
- );
412
+ console.log(adoptedTunnelStopNotice(child.pid));
438
413
  } else {
439
414
  console.log(`Stop with: kill ${child.pid}`);
440
415
  }
@@ -442,72 +417,78 @@ async function spawnDetachedTunnel(): Promise<void> {
442
417
 
443
418
  export const tunnel = Object.assign(
444
419
  async function tunnel(): Promise<void> {
445
- const { assistantName, provider, domain, clearDomain, detach } = parseArgs();
420
+ const { assistantName, provider, domain, clearDomain, foreground } =
421
+ parseArgs();
446
422
 
447
- if (detach) {
448
- await spawnDetachedTunnel();
449
- return;
450
- }
423
+ if (!foreground) {
424
+ await spawnDetachedTunnel();
425
+ return;
426
+ }
451
427
 
452
- if (provider === "cadet") {
453
- throw new Error(
454
- `Tunnel provider '${provider}' is not yet implemented. ` +
455
- `If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
456
- );
457
- }
428
+ if (PROVIDER_META[provider].comingSoon) {
429
+ throw cadetTunnelComingSoonError(provider);
430
+ }
458
431
 
459
- const { entry, gatewayPort, workspaceDir } =
460
- resolveLocalTunnelTarget(assistantName);
432
+ const { entry, gatewayPort, workspaceDir } =
433
+ resolveLocalTunnelTarget(assistantName);
461
434
 
462
- const preserveIngressUrl =
463
- provider === "tailscale" && guardTailnetOnlyWebhookIngress(workspaceDir);
435
+ const preserveIngressUrl =
436
+ provider === "tailscale" && guardTailnetOnlyWebhookIngress(workspaceDir);
464
437
 
465
- if (clearDomain) {
466
- saveNgrokDomain(workspaceDir, null);
467
- console.log("Cleared the saved ngrok domain from the workspace config.");
468
- }
438
+ if (clearDomain) {
439
+ clearReservedDomain();
440
+ console.log("Cleared the saved reserved domain.");
441
+ }
469
442
 
470
- let edge: TunnelEdge;
471
- try {
472
- edge = await ensureTunnelEdge({
473
- assistantId: entry.assistantId,
474
- workspaceDir,
475
- gatewayPort,
443
+ const nginxVersion = await ensureBinaryInstalled({
444
+ binary: "nginx",
445
+ purpose: "the tunnel edge",
446
+ packages: {
447
+ brew: ["brew", "install", "nginx"],
448
+ "apt-get": ["sudo", "apt-get", "install", "-y", "nginx"],
449
+ dnf: ["sudo", "dnf", "install", "-y", "nginx"],
450
+ pacman: ["sudo", "pacman", "-S", "--noconfirm", "nginx"],
451
+ zypper: ["sudo", "zypper", "install", "-y", "nginx"],
452
+ choco: ["choco", "install", "-y", "nginx"],
453
+ },
454
+ manualHint:
455
+ "See https://nginx.org/en/docs/install.html for how to install, or point NGINX_BIN at an existing binary.",
456
+ probe: getNginxVersion,
476
457
  });
477
- } catch (err) {
478
- console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
479
- process.exit(1);
480
- }
458
+ if (!nginxVersion) {
459
+ console.error("Error: nginx is required to run the tunnel edge.");
460
+ process.exit(1);
461
+ }
481
462
 
482
- console.log(
483
- `${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port} ` +
484
- `(serves ${formatEdgeMode(edge.includesWebApp)}).`,
485
- );
463
+ let edge: TunnelEdge;
464
+ try {
465
+ edge = await ensureTunnelEdge({
466
+ assistantId: entry.assistantId,
467
+ workspaceDir,
468
+ gatewayPort,
469
+ });
470
+ } catch (err) {
471
+ console.error(
472
+ `Error: ${err instanceof Error ? err.message : String(err)}`,
473
+ );
474
+ process.exit(1);
475
+ }
486
476
 
487
- const baseTunnelOpts = {
488
- port: edge.port,
489
- assistantId: entry.assistantId,
490
- workspaceDir,
491
- };
477
+ console.log(
478
+ `${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port}.`,
479
+ );
492
480
 
493
- if (provider === "ngrok") {
494
- await runNgrokTunnel({
481
+ const baseTunnelOpts = {
482
+ port: edge.port,
483
+ assistantId: entry.assistantId,
484
+ workspaceDir,
485
+ };
486
+
487
+ await runProviderTunnel(provider, {
495
488
  ...baseTunnelOpts,
496
489
  ...(domain ? { domain } : {}),
490
+ ...(preserveIngressUrl ? { preserveIngressUrl } : {}),
497
491
  });
498
- return;
499
- }
500
-
501
- if (provider === "cloudflare") {
502
- await runCloudflareTunnel(baseTunnelOpts);
503
- return;
504
- }
505
-
506
- await runTailscaleTunnel({
507
- ...baseTunnelOpts,
508
- ...(preserveIngressUrl ? { preserveIngressUrl } : {}),
509
- });
510
- },
492
+ },
511
493
  { description: "Create a tunnel for a locally hosted assistant" },
512
494
  );
513
-