@mycadet/cli 0.12.7 → 0.12.9

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 (179) 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 +3 -2
  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 +2 -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 +21 -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 +1 -8
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -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 +181 -197
  90. package/src/__tests__/tunnel.test.ts +257 -222
  91. package/src/__tests__/uninstall.test.ts +13 -4
  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 -76
  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 +23 -23
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/ps.ts +244 -86
  107. package/src/commands/restart.ts +30 -0
  108. package/src/commands/restore.ts +140 -22
  109. package/src/commands/retire.ts +48 -150
  110. package/src/commands/rollback.ts +20 -26
  111. package/src/commands/setup.ts +2 -2
  112. package/src/commands/sleep.ts +2 -2
  113. package/src/commands/teleport.ts +109 -109
  114. package/src/commands/terminal.ts +158 -18
  115. package/src/commands/tunnel.ts +138 -157
  116. package/src/commands/uninstall.ts +57 -9
  117. package/src/commands/unpair.ts +3 -3
  118. package/src/commands/upgrade.ts +30 -54
  119. package/src/commands/use.ts +1 -1
  120. package/src/commands/wake.ts +166 -50
  121. package/src/index.ts +18 -9
  122. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  123. package/src/lib/__tests__/docker.test.ts +168 -20
  124. package/src/lib/__tests__/job-polling.test.ts +2 -2
  125. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  126. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  127. package/src/lib/assistant-api-key-resolution.ts +5 -5
  128. package/src/lib/assistant-client.ts +8 -8
  129. package/src/lib/assistant-config.ts +58 -12
  130. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  131. package/src/lib/cloud-login.ts +24 -24
  132. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  133. package/src/lib/cloud-values.ts +3 -0
  134. package/src/lib/config-utils.ts +1 -1
  135. package/src/lib/detached-process.ts +1 -1
  136. package/src/lib/docker.ts +260 -53
  137. package/src/lib/ensure-binary.ts +171 -0
  138. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  139. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  140. package/src/lib/environments/resolve.ts +8 -8
  141. package/src/lib/flag-args.test.ts +1 -1
  142. package/src/lib/flag-args.ts +1 -1
  143. package/src/lib/guardian-token.ts +37 -5
  144. package/src/lib/hatch-local.ts +19 -23
  145. package/src/lib/health-check.ts +6 -6
  146. package/src/lib/ingress-config.ts +8 -35
  147. package/src/lib/job-polling.ts +2 -2
  148. package/src/lib/live-voice/connection.ts +12 -12
  149. package/src/lib/live-voice/tunnel.test.ts +11 -11
  150. package/src/lib/live-voice/tunnel.ts +15 -15
  151. package/src/lib/local-runtime-client.ts +20 -20
  152. package/src/lib/local.ts +10 -31
  153. package/src/lib/nginx-ingress.ts +42 -15
  154. package/src/lib/pair.ts +1 -1
  155. package/src/lib/process.ts +1 -1
  156. package/src/lib/provider-secrets.ts +3 -3
  157. package/src/lib/retire-apple-container.ts +1 -1
  158. package/src/lib/runtime-url.ts +8 -8
  159. package/src/lib/statefulset.ts +2 -2
  160. package/src/lib/sync-cloud-assistants.ts +19 -19
  161. package/src/lib/teleport-backup.ts +4 -4
  162. package/src/lib/terminal-client.ts +17 -17
  163. package/src/lib/terminal-session.ts +24 -24
  164. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  165. package/src/lib/tunnels/cadet.ts +29 -0
  166. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  167. package/src/lib/tunnels/gateway-notify.ts +61 -0
  168. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  169. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  170. package/src/lib/upgrade-lifecycle.ts +8 -21
  171. package/src/lib/upgrade-preflight.ts +2 -2
  172. package/src/lib/workos-pkce.ts +5 -5
  173. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  174. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  175. package/src/commands/recover.ts +0 -131
  176. package/src/commands/roadmap.ts +0 -459
  177. package/src/commands/ssh.ts +0 -144
  178. package/src/commands/workflows.ts +0 -305
  179. 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,41 +14,37 @@ 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
50
  detach: boolean;
@@ -58,7 +54,7 @@ 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
60
  let detach = false;
@@ -76,42 +72,44 @@ 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(", ")}`,
99
+ ` --provider <provider> Tunnel provider: ${TUNNEL_PROVIDERS.join(", ")} — default ${DEFAULT_TUNNEL_PROVIDER}`,
103
100
  );
104
101
  console.log(
105
- " --domain <domain> Reserved ngrok domain to bind (ngrok provider only).",
102
+ " --domain <domain> Reserved domain to bind, for providers that support",
106
103
  );
107
104
  console.log(
108
- " Saved to the workspace config so `cadet wake` restores reuse it.",
105
+ " one. Saved to the workspace config so `cadet wake` restores",
109
106
  );
107
+ console.log(" reuse it.");
110
108
  console.log(
111
- " --clear-domain Clear the saved ngrok domain (ngrok provider only) and tunnel",
109
+ " --clear-domain Clear the saved reserved domain (same providers) and",
112
110
  );
113
111
  console.log(
114
- " without one. Cannot be combined with --domain.",
112
+ " tunnel without one. Cannot be combined with --domain.",
115
113
  );
116
114
  console.log(
117
115
  ` -d, --detach Run the tunnel in the background. Waits up to ${TUNNEL_BACKGROUND_START_TIMEOUT_MS / 1000}s for the`,
@@ -123,72 +121,66 @@ function parseArgs(): TunnelArgs {
123
121
  " the CLI log directory. Stop it with `kill <pid>`, except when it",
124
122
  );
125
123
  console.log(
126
- " adopted an already-running ngrok tunnel (started outside this",
124
+ " adopted an already-running tunnel (started outside this",
127
125
  );
128
126
  console.log(
129
127
  " command): killing the supervisor then leaves that tunnel running.",
130
128
  );
131
129
  console.log("");
132
130
  console.log("Providers:");
133
- console.log(
134
- " cadet Managed tunnel via Cadet Cloud (not yet available)",
135
- );
136
- console.log(
137
- " ngrok ngrok tunnel — install: brew install ngrok/ngrok/ngrok",
138
- );
139
- console.log(
140
- " cloudflare Cloudflare quick tunnel — install: brew install cloudflare/cloudflare/cloudflared",
141
- );
142
- console.log(
143
- " No Cloudflare account required for quick tunnels.",
144
- );
145
- console.log(
146
- " tailscale Tailscale serve. Install: https://tailscale.com/download, then run `tailscale up`",
147
- );
148
- console.log(
149
- " Reachable only from your own tailnet (private; LetsEncrypt cert).",
150
- );
151
- console.log(
152
- " When webhook integrations are configured it records its URL for device",
153
- );
154
- console.log(
155
- " pairing only, leaving the saved webhook callback base as it is.",
156
- );
131
+ for (const name of TUNNEL_PROVIDERS) {
132
+ const meta = PROVIDER_META[name];
133
+ console.log(` ${name.padEnd(12)}${meta.reach}`);
134
+ console.log(` ${" ".repeat(12)}${meta.install}`);
135
+ for (const note of meta.notes ?? []) {
136
+ console.log(` ${" ".repeat(12)}${note}`);
137
+ }
138
+ }
157
139
  console.log("");
158
140
  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");
141
+ console.log(` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER}`);
142
+ const domainExample =
143
+ PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
144
+ if (domainExample) {
145
+ console.log(
146
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --domain ${domainExample}`,
147
+ );
148
+ console.log(
149
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --clear-domain`,
150
+ );
151
+ }
164
152
  console.log(" $ cadet tunnel --provider cloudflare");
165
153
  console.log(" $ cadet tunnel my-assistant --provider tailscale");
166
- console.log(" $ cadet tunnel --provider ngrok --detach");
154
+ console.log(
155
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --detach`,
156
+ );
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
  }
@@ -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
  }
@@ -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,16 +339,15 @@ 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
348
  * Re-invoke `cadet tunnel` (without `-d`/`--detach`) as a detached background
370
349
  * process via `relaunchDetached`, waiting for the child to either print its
371
- * "Tunnel established"/"Found existing ngrok tunnel" line (readiness) or
350
+ * "Tunnel established" or adopted-tunnel line (readiness) or
372
351
  * exit early (failure), so a misconfigured provider still fails loudly in
373
352
  * the terminal that ran `-d`.
374
353
  *
@@ -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, detach } =
421
+ parseArgs();
446
422
 
447
- if (detach) {
448
- await spawnDetachedTunnel();
449
- return;
450
- }
423
+ if (detach) {
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
-
@@ -2,11 +2,16 @@ import { existsSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
2
2
  import { basename, dirname, join } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
 
5
- import { loadAllAssistants } from "../lib/assistant-config.js";
5
+ import {
6
+ getDaemonPidPath,
7
+ loadAllAssistants,
8
+ type AssistantEntry,
9
+ } from "../lib/assistant-config.js";
6
10
  import {
7
11
  canPromptForConfirmation,
8
12
  confirmAction,
9
13
  } from "../lib/confirm-action.js";
14
+ import { retireDocker } from "../lib/docker.js";
10
15
  import {
11
16
  getConfigDirs,
12
17
  getInputHistoryPath,
@@ -14,6 +19,28 @@ import {
14
19
  getStateDir,
15
20
  } from "../lib/environments/paths.js";
16
21
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
22
+ import { stopProcessByPidFile } from "../lib/process";
23
+
24
+ /**
25
+ * The per-assistant teardown uninstall performs before deleting the data:
26
+ * docker assistants keep containers, volumes, and a network in docker that
27
+ * the filesystem sweep never reaches, and local assistants keep processes.
28
+ * Everything on-disk is deleted by the sweep regardless — this is the part
29
+ * uninstall could NOT do before it refused.
30
+ */
31
+ async function retireAssistantData(entry: AssistantEntry): Promise<void> {
32
+ if (entry.cloud === "docker") {
33
+ await retireDocker(entry.assistantId);
34
+ return;
35
+ }
36
+ const resources = entry.resources;
37
+ if (!resources) return;
38
+ await stopProcessByPidFile(
39
+ join(resources.instanceDir, "gateway.pid"),
40
+ "gateway",
41
+ );
42
+ await stopProcessByPidFile(getDaemonPidPath(resources), "assistant");
43
+ }
17
44
 
18
45
  /** A directory (or file) scheduled for deletion, with what it holds. */
19
46
  interface DeleteTarget {
@@ -114,7 +141,7 @@ function selfDelete(): void {
114
141
  "Cadet is running from a source checkout — skipping self-removal.",
115
142
  );
116
143
  console.log(
117
- "Remove the installed copy with `bun remove -g mycadet` (or `npm uninstall -g mycadet`).",
144
+ "Remove the installed copy with `bun remove -g mycadet` or `npm uninstall -g mycadet`.",
118
145
  );
119
146
  return;
120
147
  }
@@ -169,13 +196,34 @@ export const uninstall = Object.assign(
169
196
  }
170
197
  const yes = args.includes("--yes");
171
198
 
172
- // Retire tears down an assistant AND archives its data. Uninstall assumes
173
- // an empty rack: retiring first is the way to keep an archive.
174
- if (loadAllAssistants().length > 0) {
175
- console.error(
176
- "Error: You still have assistants. Retire them first (`cadet retire <name>`) — retiring archives their data before removing it.",
199
+ // Assistants still on the rack are retired — with their data deleted —
200
+ // as part of the uninstall. Uninstall deletes every location cadet uses
201
+ // INCLUDING the retired archives, so retiring first (which archives)
202
+ // only matters for the teardown it performs: docker assistants keep
203
+ // containers, volumes, and a network in docker that the filesystem
204
+ // sweep below never reaches. A warning, not a refusal (v0.0.10 run
205
+ // feedback: the hard error just blocked the uninstall).
206
+ const existing = loadAllAssistants();
207
+ if (existing.length > 0) {
208
+ console.warn(
209
+ `\n⚠️ ${existing.length} ${existing.length === 1 ? "assistant still exists" : "assistants still exist"}: ${existing.map((a) => a.assistantId).join(", ")}`,
210
+ );
211
+ console.warn(
212
+ " They will be retired now — their data (and any retired archives) will be permanently deleted by this uninstall.",
177
213
  );
178
- process.exit(1);
214
+ for (const entry of existing) {
215
+ if (entry.cloud === "apple-container" || entry.cloud === "paired") {
216
+ console.log(` Skipping '${entry.assistantId}' on ${entry.cloud} — its lifecycle is managed elsewhere.`);
217
+ continue;
218
+ }
219
+ try {
220
+ await retireAssistantData(entry);
221
+ } catch (err) {
222
+ console.error(
223
+ ` Warning: could not fully retire '${entry.assistantId}': ${err instanceof Error ? err.message : String(err)}`,
224
+ );
225
+ }
226
+ }
179
227
  }
180
228
 
181
229
  const env = getCurrentEnvironment();
@@ -191,7 +239,7 @@ export const uninstall = Object.assign(
191
239
  } else {
192
240
  console.log("It will permanently delete:");
193
241
  for (const t of targets) {
194
- console.log(` - ${t.path} (${t.description})`);
242
+ console.log(` - ${t.path} — ${t.description}`);
195
243
  }
196
244
  }
197
245
  const execBase = basename(execPath);
@@ -4,7 +4,7 @@
4
4
  * Forget a pairing imported from another machine via `cadet connect import`:
5
5
  * remove its lockfile entry and stored guardian token from THIS machine. Only
6
6
  * paired assistants (`cloud: "paired"`) can be unpaired — `cadet retire` owns
7
- * local and managed assistants.
7
+ * local and cloud assistants.
8
8
  *
9
9
  * This is client-side only: it forgets the connection here but does not revoke
10
10
  * the device on the host. (Host-side revocation is `cadet devices`.)
@@ -37,7 +37,7 @@ OPTIONS:
37
37
 
38
38
  Removes the local connection (lockfile entry + stored token). Only paired
39
39
  assistants (imported via 'cadet connect import') can be unpaired; use
40
- 'cadet retire' for local or managed assistants.
40
+ 'cadet retire' for local or cloud assistants.
41
41
 
42
42
  EXAMPLES:
43
43
  cadet unpair paired-desk
@@ -72,7 +72,7 @@ export const unpair = Object.assign(
72
72
 
73
73
  if (entry.cloud !== "paired") {
74
74
  console.error(
75
- `Error: '${name}' is not a paired assistant. Use \`cadet retire\` to remove a local or managed assistant.`,
75
+ `Error: '${name}' is not a paired assistant. Use \`cadet retire\` to remove a local or cloud assistant.`,
76
76
  );
77
77
  process.exit(1);
78
78
  }