@mycadet/cli 0.12.7 → 0.12.8

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 (165) 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-platform-login.ts → show-cloud-login.ts} +3 -5
  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 +1 -1
  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 +1 -1
  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 +1 -1
  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} +5 -5
  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__/guardian-token.test.ts +149 -56
  72. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  73. package/src/__tests__/helpers/guardian-token.ts +22 -0
  74. package/src/__tests__/helpers/http.ts +10 -0
  75. package/src/__tests__/nginx-ingress.test.ts +18 -18
  76. package/src/__tests__/orphan-detection.test.ts +1 -1
  77. package/src/__tests__/pair.test.ts +1 -8
  78. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
  79. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  80. package/src/__tests__/retire.test.ts +58 -199
  81. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  82. package/src/__tests__/sleep.test.ts +19 -19
  83. package/src/__tests__/statefulset.test.ts +8 -8
  84. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
  85. package/src/__tests__/teleport-backup.test.ts +3 -3
  86. package/src/__tests__/teleport.test.ts +180 -196
  87. package/src/__tests__/tunnel.test.ts +255 -227
  88. package/src/__tests__/uninstall.test.ts +13 -4
  89. package/src/__tests__/upgrade-local.test.ts +3 -3
  90. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  91. package/src/__tests__/wake.test.ts +258 -41
  92. package/src/__tests__/workos-pkce.test.ts +2 -2
  93. package/src/commands/backup.ts +22 -22
  94. package/src/commands/client.ts +76 -76
  95. package/src/commands/debug.ts +20 -20
  96. package/src/commands/exec.ts +5 -5
  97. package/src/commands/hatch.ts +22 -22
  98. package/src/commands/login.ts +22 -22
  99. package/src/commands/logs.ts +1 -1
  100. package/src/commands/ps.ts +188 -79
  101. package/src/commands/restart.ts +30 -0
  102. package/src/commands/restore.ts +138 -20
  103. package/src/commands/retire.ts +46 -148
  104. package/src/commands/rollback.ts +18 -24
  105. package/src/commands/setup.ts +2 -2
  106. package/src/commands/teleport.ts +93 -93
  107. package/src/commands/terminal.ts +157 -17
  108. package/src/commands/tunnel.ts +80 -122
  109. package/src/commands/uninstall.ts +55 -7
  110. package/src/commands/unpair.ts +3 -3
  111. package/src/commands/upgrade.ts +22 -46
  112. package/src/commands/wake.ts +162 -46
  113. package/src/index.ts +3 -9
  114. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  115. package/src/lib/__tests__/job-polling.test.ts +1 -1
  116. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  117. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  118. package/src/lib/assistant-api-key-resolution.ts +5 -5
  119. package/src/lib/assistant-client.ts +8 -8
  120. package/src/lib/assistant-config.ts +58 -12
  121. package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
  122. package/src/lib/cloud-login.ts +24 -24
  123. package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
  124. package/src/lib/cloud-values.ts +3 -0
  125. package/src/lib/config-utils.ts +1 -1
  126. package/src/lib/detached-process.ts +1 -1
  127. package/src/lib/docker.ts +111 -10
  128. package/src/lib/environments/__tests__/paths.test.ts +2 -2
  129. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  130. package/src/lib/environments/resolve.ts +8 -8
  131. package/src/lib/guardian-token.ts +36 -4
  132. package/src/lib/hatch-local.ts +19 -23
  133. package/src/lib/health-check.ts +6 -6
  134. package/src/lib/ingress-config.ts +8 -35
  135. package/src/lib/job-polling.ts +1 -1
  136. package/src/lib/live-voice/connection.ts +12 -12
  137. package/src/lib/live-voice/tunnel.test.ts +11 -11
  138. package/src/lib/live-voice/tunnel.ts +15 -15
  139. package/src/lib/local-runtime-client.ts +20 -20
  140. package/src/lib/local.ts +9 -30
  141. package/src/lib/nginx-ingress.ts +41 -14
  142. package/src/lib/process.ts +1 -1
  143. package/src/lib/provider-secrets.ts +3 -3
  144. package/src/lib/retire-apple-container.ts +1 -1
  145. package/src/lib/runtime-url.ts +8 -8
  146. package/src/lib/statefulset.ts +2 -2
  147. package/src/lib/sync-cloud-assistants.ts +18 -18
  148. package/src/lib/teleport-backup.ts +4 -4
  149. package/src/lib/terminal-client.ts +17 -17
  150. package/src/lib/terminal-session.ts +24 -24
  151. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  152. package/src/lib/tunnels/cadet.ts +29 -0
  153. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
  154. package/src/lib/tunnels/gateway-notify.ts +61 -0
  155. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
  156. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
  157. package/src/lib/upgrade-lifecycle.ts +3 -17
  158. package/src/lib/upgrade-preflight.ts +2 -2
  159. package/src/lib/workos-pkce.ts +5 -5
  160. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  161. package/src/commands/recover.ts +0 -131
  162. package/src/commands/roadmap.ts +0 -459
  163. package/src/commands/ssh.ts +0 -144
  164. package/src/commands/workflows.ts +0 -305
  165. package/src/lib/managed-cloud.ts +0 -3
@@ -1,9 +1,10 @@
1
1
  /**
2
- * `cadet terminal` — Interactive shell into a managed assistant container.
2
+ * `cadet terminal` — Interactive shell into an assistant.
3
3
  *
4
- * Bridges the local tty to a platform terminal session (K8s exec) so the
5
- * user can interact with their assistant's sandbox from iTerm2 or any
6
- * local terminal emulator.
4
+ * Dispatches by cloud type: a cloud terminal session (K8s exec) for
5
+ * cloud assistants, `docker exec` for docker assistants, `gcloud compute
6
+ * ssh` for GCP, raw ssh for custom hosts, and the container management
7
+ * socket for apple-container assistants.
7
8
  *
8
9
  * Subcommands:
9
10
  * cadet terminal — Interactive shell
@@ -11,6 +12,13 @@
11
12
  * cadet terminal list — List tmux sessions
12
13
  */
13
14
 
15
+ import { isCloudHome } from "../lib/cloud-values.js";
16
+ import { spawn } from "child_process";
17
+
18
+ import { extractHostFromUrl, resolveAssistant } from "../lib/assistant-config";
19
+ import { dockerResourceNames } from "../lib/docker";
20
+ import { getCloudUrl, readCloudToken } from "../lib/cloud-client";
21
+ import { sshAppleContainer } from "../lib/ssh-apple-container";
14
22
  import {
15
23
  closeTerminalSession,
16
24
  createTerminalSession,
@@ -22,6 +30,18 @@ import {
22
30
  resolveManagedAssistant,
23
31
  } from "../lib/terminal-session.js";
24
32
  import type { ResolvedManagedAssistant } from "../lib/terminal-session.js";
33
+ import type { ChildProcess } from "child_process";
34
+
35
+ const SSH_OPTS = [
36
+ "-o",
37
+ "StrictHostKeyChecking=no",
38
+ "-o",
39
+ "UserKnownHostsFile=/dev/null",
40
+ "-o",
41
+ "ConnectTimeout=10",
42
+ "-o",
43
+ "LogLevel=ERROR",
44
+ ];
25
45
 
26
46
  // ---------------------------------------------------------------------------
27
47
  // Helpers
@@ -30,9 +50,15 @@ import type { ResolvedManagedAssistant } from "../lib/terminal-session.js";
30
50
  function printHelp(): void {
31
51
  console.log("Usage: cadet terminal [subcommand] [options]");
32
52
  console.log("");
53
+ console.log("Open an interactive terminal session into an assistant.");
54
+ console.log("");
33
55
  console.log(
34
- "Open an interactive terminal session into a managed assistant container.",
56
+ "Managed (cloud-hosted) assistants get a cloud terminal session; docker",
35
57
  );
58
+ console.log(
59
+ "assistants shell in via docker exec, GCP assistants via gcloud, custom",
60
+ );
61
+ console.log("hosts via ssh, and apple-container assistants via their management socket.");
36
62
  console.log("");
37
63
  console.log("Subcommands:");
38
64
  console.log(" (none) Interactive shell");
@@ -58,6 +84,19 @@ function printHelp(): void {
58
84
  console.log(" cadet terminal --assistant my-assistant");
59
85
  }
60
86
 
87
+ function waitForExit(child: ChildProcess): Promise<void> {
88
+ return new Promise<void>((resolve, reject) => {
89
+ child.on("close", (code) => {
90
+ if (code === 0) {
91
+ resolve();
92
+ } else {
93
+ reject(new Error(`terminal exited with code ${code}`));
94
+ }
95
+ });
96
+ child.on("error", reject);
97
+ });
98
+ }
99
+
61
100
  // ---------------------------------------------------------------------------
62
101
  // List tmux sessions
63
102
  // ---------------------------------------------------------------------------
@@ -73,7 +112,7 @@ async function listTmuxSessions(
73
112
  assistant.assistantId,
74
113
  cols,
75
114
  rows,
76
- assistant.platformUrl,
115
+ assistant.cloudUrl,
77
116
  );
78
117
 
79
118
  const abortController = new AbortController();
@@ -88,7 +127,7 @@ async function listTmuxSessions(
88
127
  assistant.token,
89
128
  assistant.assistantId,
90
129
  sessionId,
91
- assistant.platformUrl,
130
+ assistant.cloudUrl,
92
131
  abortController.signal,
93
132
  )) {
94
133
  const text = Buffer.from(event.data, "base64").toString("utf-8");
@@ -108,7 +147,7 @@ async function listTmuxSessions(
108
147
  assistant.assistantId,
109
148
  sessionId,
110
149
  'tmux list-sessions 2>/dev/null || echo "No tmux sessions found"; exit\r',
111
- assistant.platformUrl,
150
+ assistant.cloudUrl,
112
151
  );
113
152
  }
114
153
  }
@@ -125,7 +164,7 @@ async function listTmuxSessions(
125
164
  assistant.token,
126
165
  assistant.assistantId,
127
166
  sessionId,
128
- assistant.platformUrl,
167
+ assistant.cloudUrl,
129
168
  ).catch(() => {});
130
169
  }
131
170
 
@@ -200,10 +239,9 @@ export const terminal = Object.assign(
200
239
  }
201
240
  }
202
241
 
203
- const assistant = resolveManagedAssistant(assistantName);
204
-
242
+ // tmux subcommands are managed-cloud features
205
243
  if (subcommand === "list") {
206
- await listTmuxSessions(assistant);
244
+ await listTmuxSessions(resolveManagedAssistant(assistantName));
207
245
  return;
208
246
  }
209
247
 
@@ -217,13 +255,115 @@ export const terminal = Object.assign(
217
255
  }
218
256
  // Shell-escape the session name to handle spaces/metacharacters
219
257
  const escaped = tmuxSessionName.replace(/'/g, "'\\''");
220
- await interactiveSession(assistant, `tmux attach -t '${escaped}'`);
258
+ await interactiveSession(
259
+ resolveManagedAssistant(assistantName),
260
+ `tmux attach -t '${escaped}'`,
261
+ );
262
+ return;
263
+ }
264
+
265
+ // Default: interactive shell, dispatched by the assistant's cloud type.
266
+ const entry = resolveAssistant(assistantName);
267
+
268
+ if (!entry) {
269
+ if (assistantName) {
270
+ console.error(`No assistant instance found with name '${assistantName}'.`);
271
+ } else {
272
+ console.error("No assistant instance found. Run `cadet hatch` first.");
273
+ }
274
+ process.exit(1);
275
+ }
276
+
277
+ const cloud = entry.cloud;
278
+
279
+ if (cloud === "local") {
280
+ console.error(
281
+ "Cannot open a terminal into a local assistant. Local assistants run directly on this machine.\n" +
282
+ `Use 'cadet ps ${entry.assistantId}' to check its processes instead.`,
283
+ );
284
+ process.exit(1);
285
+ }
286
+
287
+ if (cloud === "aws") {
288
+ console.error("Terminals on AWS instances are not yet supported.");
289
+ process.exit(1);
290
+ }
291
+
292
+ // Apple container: connect to the management socket for an interactive shell.
293
+ if (cloud === "apple-container") {
294
+ await sshAppleContainer(entry);
221
295
  return;
222
296
  }
223
297
 
224
- // Default: interactive shell
225
- await interactiveSession(assistant);
298
+ if (cloud === "docker") {
299
+ const res = dockerResourceNames(entry.assistantId);
300
+ console.log(`🔗 Connecting to ${entry.assistantId} via docker exec...\n`);
301
+
302
+ const child = spawn(
303
+ "docker",
304
+ ["exec", "-it", res.assistantContainer, "/bin/sh"],
305
+ { stdio: "inherit", windowsHide: true },
306
+ );
307
+ await waitForExit(child);
308
+ return;
309
+ }
310
+
311
+ if (cloud === "gcp") {
312
+ const project = entry.project;
313
+ const zone = entry.zone;
314
+ if (!project || !zone) {
315
+ console.error("Error: GCP project and zone not found in assistant config.");
316
+ process.exit(1);
317
+ }
318
+
319
+ const sshTarget = entry.sshUser
320
+ ? `${entry.sshUser}@${entry.assistantId}`
321
+ : entry.assistantId;
322
+
323
+ console.log(`🔗 Connecting to ${entry.assistantId} via gcloud...\n`);
324
+
325
+ const child = spawn(
326
+ "gcloud",
327
+ ["compute", "ssh", sshTarget, `--project=${project}`, `--zone=${zone}`],
328
+ { stdio: "inherit", windowsHide: true },
329
+ );
330
+ await waitForExit(child);
331
+ return;
332
+ }
333
+
334
+ if (isCloudHome(cloud)) {
335
+ const token = readCloudToken();
336
+ if (!token) {
337
+ console.error(
338
+ "Not logged in. Run `cadet login` first to authenticate with Cadet Cloud.",
339
+ );
340
+ process.exit(1);
341
+ }
342
+ await interactiveSession({
343
+ assistantId: entry.assistantId,
344
+ token,
345
+ cloudUrl: getCloudUrl(),
346
+ });
347
+ return;
348
+ }
349
+
350
+ if (cloud === "custom") {
351
+ const host = extractHostFromUrl(entry.runtimeUrl);
352
+ const sshUser = entry.sshUser ?? "root";
353
+ const sshTarget = `${sshUser}@${host}`;
354
+
355
+ console.log(`🔗 Connecting to ${entry.assistantId} via ssh...\n`);
356
+
357
+ const child = spawn("ssh", [...SSH_OPTS, sshTarget], {
358
+ stdio: "inherit",
359
+ windowsHide: true,
360
+ });
361
+ await waitForExit(child);
362
+ return;
363
+ }
364
+
365
+ console.error(`Error: Unknown cloud type '${cloud}'.`);
366
+ process.exit(1);
226
367
  },
227
- { description: "Open a terminal into a managed assistant container" },
368
+ { description: "Open a terminal into an assistant" },
228
369
  );
229
-
@@ -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,36 @@ 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
25
  formatEdgeMode,
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";
32
28
  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
- };
29
+ import {
30
+ ADOPTED_TUNNEL_RE,
31
+ DEFAULT_TUNNEL_PROVIDER,
32
+ PROVIDER_META,
33
+ adoptedTunnelStopNotice,
34
+ cadetTunnelComingSoonError,
35
+ clearReservedDomain,
36
+ hasWebhookIntegrationsConfigured,
37
+ providerSupportsReservedDomain,
38
+ runProviderTunnel,
39
+ } from "../lib/tunnel.js";
40
+
41
+ // The accepted `--provider` values ARE the shared registry, so validation
42
+ // here cannot drift from what the workspace config accepts.
48
43
 
49
44
  interface TunnelArgs {
50
45
  assistantName: string | null;
51
- provider: TunnelProvider;
46
+ provider: TunnelProviderName;
52
47
  domain: string | null;
53
48
  clearDomain: boolean;
54
49
  detach: boolean;
@@ -58,7 +53,7 @@ const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
58
53
 
59
54
  function parseArgs(): TunnelArgs {
60
55
  const args = process.argv.slice(3);
61
- let provider: TunnelProvider | null = null;
56
+ let provider: TunnelProviderName | null = null;
62
57
  let domain: string | null = null;
63
58
  let clearDomain = false;
64
59
  let detach = false;
@@ -76,14 +71,13 @@ function parseArgs(): TunnelArgs {
76
71
  );
77
72
  console.log("");
78
73
  console.log(
79
- "How far that URL reaches depends on the provider, so --provider is required.",
80
- );
81
- console.log(
82
- "A tailscale tunnel is reachable only from your own tailnet; webhook",
74
+ "How far that URL reaches depends on the provider; omit --provider for",
83
75
  );
76
+ console.log("the default. A tailscale tunnel is reachable only from your");
84
77
  console.log(
85
- "integrations need a public tunnel: --provider ngrok or --provider cloudflare.",
78
+ "own tailnet; webhook integrations need a public tunnel (see Providers",
86
79
  );
80
+ console.log("below).");
87
81
  console.log("");
88
82
  console.log(
89
83
  "The tunnel always fronts the local nginx edge, which is started automatically.",
@@ -99,19 +93,21 @@ function parseArgs(): TunnelArgs {
99
93
  console.log("");
100
94
  console.log("Options:");
101
95
  console.log(
102
- ` --provider <provider> Required. Tunnel provider: ${VALID_PROVIDERS.join(", ")}`,
96
+ ` --provider <provider> Tunnel provider: ${TUNNEL_PROVIDERS.join(", ")}`,
103
97
  );
98
+ console.log(` (default: ${DEFAULT_TUNNEL_PROVIDER})`);
104
99
  console.log(
105
- " --domain <domain> Reserved ngrok domain to bind (ngrok provider only).",
100
+ " --domain <domain> Reserved domain to bind, for providers that support",
106
101
  );
107
102
  console.log(
108
- " Saved to the workspace config so `cadet wake` restores reuse it.",
103
+ " one. Saved to the workspace config so `cadet wake` restores",
109
104
  );
105
+ console.log(" reuse it.");
110
106
  console.log(
111
- " --clear-domain Clear the saved ngrok domain (ngrok provider only) and tunnel",
107
+ " --clear-domain Clear the saved reserved domain (same providers) and",
112
108
  );
113
109
  console.log(
114
- " without one. Cannot be combined with --domain.",
110
+ " tunnel without one. Cannot be combined with --domain.",
115
111
  );
116
112
  console.log(
117
113
  ` -d, --detach Run the tunnel in the background. Waits up to ${TUNNEL_BACKGROUND_START_TIMEOUT_MS / 1000}s for the`,
@@ -123,72 +119,64 @@ function parseArgs(): TunnelArgs {
123
119
  " the CLI log directory. Stop it with `kill <pid>`, except when it",
124
120
  );
125
121
  console.log(
126
- " adopted an already-running ngrok tunnel (started outside this",
122
+ " adopted an already-running tunnel (started outside this",
127
123
  );
128
124
  console.log(
129
125
  " command): killing the supervisor then leaves that tunnel running.",
130
126
  );
131
127
  console.log("");
132
128
  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
- );
129
+ for (const name of TUNNEL_PROVIDERS) {
130
+ const meta = PROVIDER_META[name];
131
+ console.log(` ${name.padEnd(12)}${meta.reach}`);
132
+ console.log(` ${" ".repeat(12)}${meta.install}`);
133
+ for (const note of meta.notes ?? []) {
134
+ console.log(` ${" ".repeat(12)}${note}`);
135
+ }
136
+ }
157
137
  console.log("");
158
138
  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");
139
+ console.log(` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER}`);
140
+ const domainExample = PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
141
+ if (domainExample) {
142
+ console.log(
143
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --domain ${domainExample}`,
144
+ );
145
+ console.log(
146
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --clear-domain`,
147
+ );
148
+ }
164
149
  console.log(" $ cadet tunnel --provider cloudflare");
165
150
  console.log(" $ cadet tunnel my-assistant --provider tailscale");
166
- console.log(" $ cadet tunnel --provider ngrok --detach");
151
+ console.log(
152
+ ` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --detach`,
153
+ );
167
154
  process.exit(0);
168
155
  } else if (arg === "--provider") {
169
156
  const next = args[i + 1];
170
157
  if (!next || next.startsWith("-")) {
171
158
  console.error(
172
- `Error: --provider requires one of: ${VALID_PROVIDERS.join(", ")}`,
159
+ `Error: --provider requires one of: ${TUNNEL_PROVIDERS.join(", ")}`,
173
160
  );
174
161
  process.exit(1);
175
162
  }
176
- if (!VALID_PROVIDERS.includes(next as TunnelProvider)) {
163
+ if (!TUNNEL_PROVIDERS.includes(next as TunnelProviderName)) {
177
164
  console.error(
178
- `Error: unknown tunnel provider '${next}'. Valid providers: ${VALID_PROVIDERS.join(", ")}.`,
165
+ `Error: unknown tunnel provider '${next}'. Valid providers: ${TUNNEL_PROVIDERS.join(", ")}.`,
179
166
  );
180
167
  console.error(
181
168
  `If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
182
169
  );
183
170
  process.exit(1);
184
171
  }
185
- provider = next as TunnelProvider;
172
+ provider = next as TunnelProviderName;
186
173
  i++;
187
174
  } else if (arg === "--domain") {
188
175
  const next = args[i + 1];
189
176
  if (!next || next.startsWith("-")) {
177
+ const domainExample = PROVIDER_META[DEFAULT_TUNNEL_PROVIDER].domainExample;
190
178
  console.error(
191
- "Error: --domain requires a value, e.g. --domain my-assistant.ngrok.app",
179
+ `Error: --domain requires a value${domainExample ? `, e.g. --domain ${domainExample}` : ""}`,
192
180
  );
193
181
  process.exit(1);
194
182
  }
@@ -210,32 +198,22 @@ function parseArgs(): TunnelArgs {
210
198
  parseAssistantTargetArg(args, FLAGS_WITH_VALUES) ?? null;
211
199
 
212
200
  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.",
201
+ provider = DEFAULT_TUNNEL_PROVIDER;
202
+ console.log(
203
+ `Using the default provider: ${provider} (pass --provider to choose another).`,
215
204
  );
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);
227
205
  }
228
206
 
229
- if (domain && provider !== "ngrok") {
207
+ if (domain && !providerSupportsReservedDomain(provider)) {
230
208
  console.error(
231
- `Error: --domain is only supported with --provider ngrok (got '${provider}').`,
209
+ `Error: --domain is not supported with --provider ${provider} (reserved domains have no equivalent there).`,
232
210
  );
233
211
  process.exit(1);
234
212
  }
235
213
 
236
- if (clearDomain && provider !== "ngrok") {
214
+ if (clearDomain && !providerSupportsReservedDomain(provider)) {
237
215
  console.error(
238
- `Error: --clear-domain is only supported with --provider ngrok (got '${provider}').`,
216
+ `Error: --clear-domain is not supported with --provider ${provider} (reserved domains have no equivalent there).`,
239
217
  );
240
218
  process.exit(1);
241
219
  }
@@ -259,7 +237,7 @@ interface LocalTunnelTarget {
259
237
 
260
238
  /**
261
239
  * 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
240
+ * reachable gateway (e.g. cloud-hosted). Entries with `resources` carry
263
241
  * their own gateway port and instance workspace. Local container entries
264
242
  * (docker, apple-container) run locally without host resources: their gateway
265
243
  * port comes from localUrl/runtimeUrl and their ingress state lives in the
@@ -284,7 +262,7 @@ function toLocalTunnelTarget(entry: AssistantEntry): LocalTunnelTarget | null {
284
262
 
285
263
  function describeUntunnelableEntry(entry: AssistantEntry): string {
286
264
  const reference = formatAssistantReference(entry);
287
- return isManagedCloud(entry.cloud)
265
+ return isCloudHome(entry.cloud)
288
266
  ? `Assistant '${reference}' runs on Cadet Cloud and needs no tunnel.`
289
267
  : `Assistant '${reference}' has no locally managed runtime to tunnel.`;
290
268
  }
@@ -292,7 +270,7 @@ function describeUntunnelableEntry(entry: AssistantEntry): string {
292
270
  /**
293
271
  * Resolve the assistant whose gateway and workspace the tunnel edge fronts.
294
272
  * 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
273
+ * resolved entry has none (e.g. the active assistant is cloud-hosted) and
296
274
  * no name was given, fall back to the sole local target, otherwise exit with
297
275
  * an error naming the local assistants to pass explicitly.
298
276
  */
@@ -350,8 +328,8 @@ function guardTailnetOnlyWebhookIngress(workspaceDir: string): boolean {
350
328
  console.warn(
351
329
  "⚠ Webhook integrations are configured, and the tailscale tunnel is reachable only from your own tailnet, " +
352
330
  "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.",
331
+ "recorded for device pairing only. For a tunnel those integrations can reach too, run `cadet tunnel --help` " +
332
+ "to see the public providers.",
355
333
  );
356
334
  return true;
357
335
  }
@@ -359,16 +337,15 @@ function guardTailnetOnlyWebhookIngress(workspaceDir: string): boolean {
359
337
  // Generous cap so a slow provider handshake (e.g. cloudflared's own ~30s
360
338
  // quick-tunnel timeout) still counts as a successful, if slow, start.
361
339
  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).
340
+ // Matches the "just established" line every provider prints, or the adoption
341
+ // line printed when a run adopts an already-running agent's tunnel (captured
342
+ // separately since that case does not own the tunnel it adopted).
365
343
  const TUNNEL_ESTABLISHED_RE = /Tunnel established: (\S+)/;
366
- const TUNNEL_ADOPTED_RE = /Found existing ngrok tunnel: (\S+)/;
367
344
 
368
345
  /**
369
346
  * Re-invoke `cadet tunnel` (without `-d`/`--detach`) as a detached background
370
347
  * process via `relaunchDetached`, waiting for the child to either print its
371
- * "Tunnel established"/"Found existing ngrok tunnel" line (readiness) or
348
+ * "Tunnel established" or adopted-tunnel line (readiness) or
372
349
  * exit early (failure), so a misconfigured provider still fails loudly in
373
350
  * the terminal that ran `-d`.
374
351
  *
@@ -404,7 +381,7 @@ async function spawnDetachedTunnel(): Promise<void> {
404
381
  publicUrl = established[1];
405
382
  return true;
406
383
  }
407
- const adoptedMatch = TUNNEL_ADOPTED_RE.exec(logText);
384
+ const adoptedMatch = ADOPTED_TUNNEL_RE.exec(logText);
408
385
  if (adoptedMatch) {
409
386
  publicUrl = adoptedMatch[1];
410
387
  adopted = true;
@@ -430,11 +407,7 @@ async function spawnDetachedTunnel(): Promise<void> {
430
407
  }
431
408
  console.log(`Running in background (pid ${child.pid}). Logs: ${logPath}`);
432
409
  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
- );
410
+ console.log(adoptedTunnelStopNotice(child.pid));
438
411
  } else {
439
412
  console.log(`Stop with: kill ${child.pid}`);
440
413
  }
@@ -449,11 +422,8 @@ export const tunnel = Object.assign(
449
422
  return;
450
423
  }
451
424
 
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
- );
425
+ if (PROVIDER_META[provider].comingSoon) {
426
+ throw cadetTunnelComingSoonError(provider);
457
427
  }
458
428
 
459
429
  const { entry, gatewayPort, workspaceDir } =
@@ -463,8 +433,8 @@ export const tunnel = Object.assign(
463
433
  provider === "tailscale" && guardTailnetOnlyWebhookIngress(workspaceDir);
464
434
 
465
435
  if (clearDomain) {
466
- saveNgrokDomain(workspaceDir, null);
467
- console.log("Cleared the saved ngrok domain from the workspace config.");
436
+ clearReservedDomain();
437
+ console.log("Cleared the saved reserved domain.");
468
438
  }
469
439
 
470
440
  let edge: TunnelEdge;
@@ -490,21 +460,9 @@ export const tunnel = Object.assign(
490
460
  workspaceDir,
491
461
  };
492
462
 
493
- if (provider === "ngrok") {
494
- await runNgrokTunnel({
495
- ...baseTunnelOpts,
496
- ...(domain ? { domain } : {}),
497
- });
498
- return;
499
- }
500
-
501
- if (provider === "cloudflare") {
502
- await runCloudflareTunnel(baseTunnelOpts);
503
- return;
504
- }
505
-
506
- await runTailscaleTunnel({
463
+ await runProviderTunnel(provider, {
507
464
  ...baseTunnelOpts,
465
+ ...(domain ? { domain } : {}),
508
466
  ...(preserveIngressUrl ? { preserveIngressUrl } : {}),
509
467
  });
510
468
  },