@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,14 +1,14 @@
1
1
  /**
2
2
  * Shared terminal session primitives for managed (cloud-hosted) assistants.
3
3
  *
4
- * Extracted from commands/terminal.ts so that ssh.ts, exec.ts, and
5
- * terminal.ts can all use the same interactive session and assistant
6
- * resolver without cross-importing commands (per cli/CONTRIBUTING.md).
4
+ * Extracted from commands/terminal.ts so that exec.ts and terminal.ts can
5
+ * use the same interactive session and assistant resolver without
6
+ * cross-importing commands (per cli/CONTRIBUTING.md).
7
7
  */
8
8
 
9
- import { isManagedCloud } from "./managed-cloud.js";
9
+ import { isCloudHome } from "./cloud-values.js";
10
10
  import { resolveAssistant } from "./assistant-config.js";
11
- import { getPlatformUrl, readPlatformToken } from "./platform-client.js";
11
+ import { getCloudUrl, readCloudToken } from "./cloud-client.js";
12
12
  import {
13
13
  closeTerminalSession,
14
14
  createTerminalSession,
@@ -24,7 +24,7 @@ import {
24
24
  export interface ResolvedManagedAssistant {
25
25
  assistantId: string;
26
26
  token: string;
27
- platformUrl: string;
27
+ cloudUrl: string;
28
28
  }
29
29
 
30
30
  // ---------------------------------------------------------------------------
@@ -32,7 +32,7 @@ export interface ResolvedManagedAssistant {
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
34
  /**
35
- * Resolve a managed (cloud-hosted) assistant from the lockfile. Exits with
35
+ * Resolve a cloud (cloud-hosted) assistant from the lockfile. Exits with
36
36
  * an error if the assistant is not found, not managed, or the user isn't
37
37
  * logged in.
38
38
  */
@@ -51,14 +51,14 @@ export function resolveManagedAssistant(
51
51
  }
52
52
 
53
53
  const cloud = entry.cloud;
54
- if (!isManagedCloud(cloud)) {
54
+ if (!isCloudHome(cloud)) {
55
55
  if (cloud === "local") {
56
56
  console.error(
57
57
  "This assistant runs locally on your machine. You can access it directly.",
58
58
  );
59
59
  } else if (cloud === "docker") {
60
60
  console.error(
61
- `Use 'cadet exec -it -- /bin/bash' or 'cadet ssh' for ${cloud} instances.`,
61
+ `Use 'cadet exec -it -- /bin/bash' or 'cadet terminal' for ${cloud} instances.`,
62
62
  );
63
63
  } else {
64
64
  console.error(
@@ -68,7 +68,7 @@ export function resolveManagedAssistant(
68
68
  process.exit(1);
69
69
  }
70
70
 
71
- const token = readPlatformToken();
71
+ const token = readCloudToken();
72
72
  if (!token) {
73
73
  console.error(
74
74
  "Not logged in. Run `cadet login` first to authenticate with Cadet Cloud.",
@@ -79,7 +79,7 @@ export function resolveManagedAssistant(
79
79
  return {
80
80
  assistantId: entry.assistantId,
81
81
  token,
82
- platformUrl: getPlatformUrl(),
82
+ cloudUrl: getCloudUrl(),
83
83
  };
84
84
  }
85
85
 
@@ -88,8 +88,8 @@ export function resolveManagedAssistant(
88
88
  // ---------------------------------------------------------------------------
89
89
 
90
90
  /**
91
- * Open an interactive raw-tty terminal session to a managed assistant.
92
- * Bridges local stdin/stdout to the platform terminal session API.
91
+ * Open an interactive raw-tty terminal session to a cloud assistant.
92
+ * Bridges local stdin/stdout to the cloud terminal session API.
93
93
  */
94
94
  export async function interactiveSession(
95
95
  assistant: ResolvedManagedAssistant,
@@ -106,7 +106,7 @@ export async function interactiveSession(
106
106
  assistant.assistantId,
107
107
  cols,
108
108
  rows,
109
- assistant.platformUrl,
109
+ assistant.cloudUrl,
110
110
  service,
111
111
  );
112
112
 
@@ -142,7 +142,7 @@ export async function interactiveSession(
142
142
  assistant.token,
143
143
  assistant.assistantId,
144
144
  sessionId,
145
- assistant.platformUrl,
145
+ assistant.cloudUrl,
146
146
  );
147
147
  } catch {
148
148
  // Best-effort cleanup
@@ -169,7 +169,7 @@ export async function interactiveSession(
169
169
  sessionId,
170
170
  newCols,
171
171
  newRows,
172
- assistant.platformUrl,
172
+ assistant.cloudUrl,
173
173
  ).catch(() => {
174
174
  // Resize failures are non-fatal
175
175
  });
@@ -190,7 +190,7 @@ export async function interactiveSession(
190
190
  assistant.assistantId,
191
191
  sessionId,
192
192
  data,
193
- assistant.platformUrl,
193
+ assistant.cloudUrl,
194
194
  ).catch((err) => {
195
195
  if (!exiting) {
196
196
  console.error(`\r\nInput error: ${err.message}\r\n`);
@@ -214,7 +214,7 @@ export async function interactiveSession(
214
214
  assistant.assistantId,
215
215
  sessionId,
216
216
  initialCommand + "\r",
217
- assistant.platformUrl,
217
+ assistant.cloudUrl,
218
218
  );
219
219
  }
220
220
 
@@ -224,7 +224,7 @@ export async function interactiveSession(
224
224
  assistant.token,
225
225
  assistant.assistantId,
226
226
  sessionId,
227
- assistant.platformUrl,
227
+ assistant.cloudUrl,
228
228
  abortController.signal,
229
229
  )) {
230
230
  if (exiting) break;
@@ -271,7 +271,7 @@ export function shellEscapeArgs(args: string[]): string {
271
271
  // ---------------------------------------------------------------------------
272
272
 
273
273
  /**
274
- * Run a command non-interactively in a managed assistant service. Creates
274
+ * Run a command non-interactively in a cloud assistant service. Creates
275
275
  * an ephemeral terminal session, sends the command wrapped in sentinels for
276
276
  * reliable output extraction, captures the result, and exits with the
277
277
  * remote command's exit code.
@@ -300,7 +300,7 @@ export async function nonInteractiveExec(
300
300
  assistant.assistantId,
301
301
  120,
302
302
  24,
303
- assistant.platformUrl,
303
+ assistant.cloudUrl,
304
304
  options?.service,
305
305
  );
306
306
 
@@ -333,7 +333,7 @@ export async function nonInteractiveExec(
333
333
  assistant.token,
334
334
  assistant.assistantId,
335
335
  sessionId,
336
- assistant.platformUrl,
336
+ assistant.cloudUrl,
337
337
  abortController.signal,
338
338
  )) {
339
339
  eventCount++;
@@ -369,7 +369,7 @@ export async function nonInteractiveExec(
369
369
  assistant.assistantId,
370
370
  sessionId,
371
371
  fullCmd,
372
- assistant.platformUrl,
372
+ assistant.cloudUrl,
373
373
  );
374
374
  }
375
375
  }
@@ -403,7 +403,7 @@ export async function nonInteractiveExec(
403
403
  assistant.token,
404
404
  assistant.assistantId,
405
405
  sessionId,
406
- assistant.platformUrl,
406
+ assistant.cloudUrl,
407
407
  ).catch(() => {});
408
408
  }
409
409
 
@@ -1,11 +1,10 @@
1
- import type { ChildProcess } from "child_process";
2
-
3
- import { selectRestartTunnelRecord } from "@mycadet/service-contracts/ingress";
4
-
5
1
  import {
2
+ clearRootProcess,
6
3
  loadAllAssistants,
7
4
  loadAllAssistantsAcrossEnvs,
5
+ readRootProcesses,
8
6
  type AssistantEntry,
7
+ type RootProcessRecord,
9
8
  } from "./assistant-config.js";
10
9
 
11
10
  import {
@@ -14,7 +13,6 @@ import {
14
13
  loadRawConfig,
15
14
  parseGatewayPortFromEntryUrls,
16
15
  } from "./ingress-config.js";
17
- import { IngressConfigSchema } from "./ngrok.js";
18
16
  import {
19
17
  ensureTunnelEdge,
20
18
  formatEdgeMode,
@@ -23,8 +21,187 @@ import {
23
21
  type TunnelEdge,
24
22
  } from "./nginx-ingress.js";
25
23
  import { waitForDaemonReady } from "./http-client.js";
26
- import { hasWebhookIntegrations, maybeStartNgrokTunnel } from "./ngrok.js";
24
+ import {
25
+ hasWebhookIntegrations,
26
+ IngressConfigSchema,
27
+ isNgrokProcess,
28
+ maybeStartNgrokTunnel,
29
+ NGROK_TUNNEL_META,
30
+ runNgrokTunnel,
31
+ saveNgrokDomain,
32
+ } from "./tunnels/ngrok.js";
33
+ import { stopProcess } from "./process.js";
27
34
  import { shellArg } from "./shell-arg.js";
35
+ import { notifyGatewayOfPublicUrl } from "./tunnels/gateway-notify.js";
36
+ import {
37
+ selectRestartTunnelRecord,
38
+ type TunnelProviderName,
39
+ } from "@mycadet/service-contracts/ingress";
40
+ import { runCadetTunnel, CADET_TUNNEL_META } from "./tunnels/cadet.js";
41
+ import {
42
+ runCloudflareTunnel,
43
+ CLOUDFLARE_TUNNEL_META,
44
+ } from "./tunnels/cloudflare.js";
45
+ import {
46
+ runTailscaleTunnel,
47
+ TAILSCALE_TUNNEL_META,
48
+ } from "./tunnels/tailscale.js";
49
+
50
+ /**
51
+ * The provider the automatic tunnel paths start when no provider is named.
52
+ * ngrok today; the north star has the managed `cadet` tunnel as the eventual
53
+ * default, at which point only this constant flips.
54
+ */
55
+ export const DEFAULT_TUNNEL_PROVIDER: TunnelProviderName = "ngrok";
56
+
57
+ /** Per-provider copy `cadet tunnel`'s help renders. */
58
+ export interface ProviderMeta {
59
+ /** How far the tunnel URL reaches. */
60
+ reach: string;
61
+ /** Install instruction line, rendered under the provider entry. */
62
+ install: string;
63
+ /** Extra behavior notes, rendered under the install line. */
64
+ notes?: string[];
65
+ /** Example reserved-domain value, for providers that support `--domain`. */
66
+ domainExample?: string;
67
+ /** Whether `--domain`/`--clear-domain` can bind a reserved domain. */
68
+ supportsReservedDomain: boolean;
69
+ /**
70
+ * Listed but not yet implemented: the command refuses it before starting
71
+ * any infrastructure, so `--provider <name>` fails fast with the
72
+ * coming-soon copy instead of building an edge nothing will use.
73
+ */
74
+ comingSoon?: boolean;
75
+ }
76
+
77
+ /**
78
+ * The registry: each provider's copy lives in its own `tunnels/<provider>.ts`
79
+ * module and is assembled here, so `cadet tunnel`'s help, `--provider`
80
+ * validation, and dispatch all read one shape.
81
+ */
82
+ export const PROVIDER_META: Record<TunnelProviderName, ProviderMeta> = {
83
+ cadet: CADET_TUNNEL_META,
84
+ ngrok: NGROK_TUNNEL_META,
85
+ cloudflare: CLOUDFLARE_TUNNEL_META,
86
+ tailscale: TAILSCALE_TUNNEL_META,
87
+ };
88
+
89
+ /**
90
+ * Registry check that tolerates any provider spelling: only providers whose
91
+ * meta claims reserved-domain support accept `--domain`/`--clear-domain`.
92
+ */
93
+ export function providerSupportsReservedDomain(provider: string): boolean {
94
+ for (const [name, meta] of Object.entries(PROVIDER_META)) {
95
+ if (name === provider) return meta.supportsReservedDomain;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ /** Options every provider tunnel accepts; `runProviderTunnel` maps them. */
101
+ export interface ProviderTunnelOptions {
102
+ port: number;
103
+ assistantId?: string;
104
+ workspaceDir: string;
105
+ /** Reserved domain to bind (providers that support one only). */
106
+ domain?: string;
107
+ /** Tailnet-only runs keep the webhook callback base as it is. */
108
+ preserveIngressUrl?: boolean;
109
+ }
110
+
111
+ /**
112
+ * What every tunnel provider's run hands back: the public URL it established
113
+ * or adopted, and the promise that settles when the run ends. Conformance is
114
+ * the point — the tunnel level can notify the gateway the same way no matter
115
+ * which provider ran.
116
+ */
117
+ export interface ProviderTunnelResult {
118
+ publicUrl: string;
119
+ waitForExit: Promise<void>;
120
+ }
121
+
122
+ /**
123
+ * Run a tunnel provider by name — the dispatch `cadet tunnel` uses, so the
124
+ * command layer never names a provider implementation directly. Notifies the
125
+ * gateway of the resulting public URL identically for every provider, then
126
+ * holds until the run ends.
127
+ */
128
+ export async function runProviderTunnel(
129
+ provider: TunnelProviderName,
130
+ opts: ProviderTunnelOptions,
131
+ ): Promise<{ publicUrl: string }> {
132
+ const result = await runProvider(provider, opts);
133
+ await notifyGatewayOfPublicUrl(result.publicUrl, opts.assistantId);
134
+ await result.waitForExit;
135
+ return { publicUrl: result.publicUrl };
136
+ }
137
+
138
+ /** Map the provider name onto its run — no gateway notification here. */
139
+ function runProvider(
140
+ provider: TunnelProviderName,
141
+ opts: ProviderTunnelOptions,
142
+ ): Promise<ProviderTunnelResult> {
143
+ const base = {
144
+ port: opts.port,
145
+ assistantId: opts.assistantId,
146
+ workspaceDir: opts.workspaceDir,
147
+ };
148
+ if (provider === "ngrok") {
149
+ return runNgrokTunnel({
150
+ ...base,
151
+ ...(opts.domain ? { domain: opts.domain } : {}),
152
+ });
153
+ }
154
+ if (provider === "cadet") {
155
+ return runCadetTunnel(opts);
156
+ }
157
+ if (provider === "cloudflare") {
158
+ return runCloudflareTunnel(base);
159
+ }
160
+ return runTailscaleTunnel({
161
+ ...base,
162
+ ...(opts.preserveIngressUrl
163
+ ? { preserveIngressUrl: opts.preserveIngressUrl }
164
+ : {}),
165
+ });
166
+ }
167
+
168
+ /** Drop the saved reserved-domain record (providers that keep one). */
169
+ export function clearReservedDomain(): void {
170
+ saveNgrokDomain(null);
171
+ }
172
+
173
+ /**
174
+ * Stop a tunnel agent this CLI started, per the root lockfile's `tunnel`
175
+ * entry. The pid is verified against the provider's process identity before
176
+ * signaling — a stale entry after PID reuse must not kill an unrelated
177
+ * process.
178
+ */
179
+ export async function stopTunnelProcess(): Promise<void> {
180
+ const recorded: RootProcessRecord | undefined = readRootProcesses().tunnel;
181
+ if (!recorded) return;
182
+ if (isNgrokProcess(recorded.pid)) {
183
+ await stopProcess(recorded.pid, "tunnel");
184
+ }
185
+ clearRootProcess("tunnel");
186
+ }
187
+
188
+ /**
189
+ * The `cadet tunnel --detach` copy for a run that adopted an already-running
190
+ * agent: killing the supervisor does not stop a tunnel it did not spawn.
191
+ */
192
+ export function adoptedTunnelStopNotice(pid: number | undefined): string {
193
+ return (
194
+ `That ngrok agent was already running outside this command, so \`kill ${pid}\` only ` +
195
+ "stops this supervisor, not the ngrok tunnel or its saved ingress URL. Stop the ngrok agent " +
196
+ "directly to end the tunnel."
197
+ );
198
+ }
199
+
200
+ export {
201
+ ADOPTED_TUNNEL_RE,
202
+ hasWebhookIntegrationsConfigured,
203
+ } from "./tunnels/ngrok.js";
204
+ export { cadetTunnelComingSoonError } from "./tunnels/cadet.js";
28
205
 
29
206
  /** Matches the Docker hatch path's service-readiness allowance. */
30
207
  export const DOCKER_GATEWAY_READY_TIMEOUT_MS = 5 * 60_000;
@@ -221,8 +398,9 @@ function ownsSharedIngress(
221
398
  * survived across sleep (a tailnet serve, a reserved ngrok domain) reaches the
222
399
  * gateway again without a manual `cadet tunnel`.
223
400
  *
224
- * No webhook auto-tunnel here. Container wakes have never tracked a spawned
225
- * ngrok PID, so starting one would leak it past the next sleep.
401
+ * The auto-tunnel runs here too, matching the local wake path: the spawned
402
+ * agent records itself in the root lockfile under `tunnel`, so `cadet ps` and
403
+ * the stop paths can see it instead of it leaking past the next sleep.
226
404
  */
227
405
  export async function restoreContainerTunnelEdge(
228
406
  entry: AssistantEntry,
@@ -257,20 +435,30 @@ export async function restoreContainerTunnelEdge(
257
435
  );
258
436
  return;
259
437
  }
260
- await restoreTunnelEdge(entry.assistantId, gatewayPort, workspaceDir, false);
438
+ await restoreTunnelEdgeAndAutoTunnel(
439
+ entry.assistantId,
440
+ gatewayPort,
441
+ workspaceDir,
442
+ );
261
443
  }
262
444
 
445
+ /**
446
+ * Restore the nginx edge, then start the default provider's tunnel at it
447
+ * (the gateway port when the edge is down, keeping webhook delivery alive).
448
+ * The spawned agent records itself in the root lockfile under `tunnel`;
449
+ * adopting an already-running one clears any stale entry.
450
+ */
263
451
  export async function restoreTunnelEdgeAndAutoTunnel(
264
452
  assistantId: string,
265
453
  gatewayPort: number,
266
454
  workspaceDir: string,
267
- ): Promise<ChildProcess | null> {
455
+ ): Promise<void> {
268
456
  const edgePort = await restoreTunnelEdge(
269
457
  assistantId,
270
458
  gatewayPort,
271
459
  workspaceDir,
272
460
  );
273
- return maybeStartNgrokTunnel(
461
+ await maybeStartNgrokTunnel(
274
462
  edgePort ?? gatewayPort,
275
463
  workspaceDir,
276
464
  assistantId,
@@ -0,0 +1,29 @@
1
+ import { STALE_CLI_UPDATE_HINT } from "../stale-cli-hint.js";
2
+ import type { ProviderMeta, ProviderTunnelOptions } from "../tunnel.js";
3
+
4
+ /**
5
+ * The managed `cadet` tunnel. It is listed in the shared registry so the
6
+ * command's help and `--provider` validation speak its name, but the tunnel
7
+ * itself ships with Cadet Cloud — until then every run refuses loudly.
8
+ */
9
+ export const CADET_TUNNEL_META: ProviderMeta = {
10
+ reach: "Managed tunnel via Cadet Cloud (coming soon)",
11
+ install: "no install needed — included with Cadet Cloud",
12
+ supportsReservedDomain: false,
13
+ comingSoon: true,
14
+ };
15
+
16
+ /** Copy every refusing path shares, so the wording cannot drift apart. */
17
+ export function cadetTunnelComingSoonError(provider: string): Error {
18
+ return new Error(
19
+ `Tunnel provider '${provider}' is not yet implemented. ` +
20
+ `If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
21
+ );
22
+ }
23
+
24
+ /** Refuse the run: the managed tunnel is not something the CLI can start. */
25
+ export async function runCadetTunnel(
26
+ _opts: ProviderTunnelOptions,
27
+ ): Promise<never> {
28
+ throw cadetTunnelComingSoonError("cadet");
29
+ }
@@ -1,11 +1,13 @@
1
+ import type { ProviderMeta, ProviderTunnelResult } from "../tunnel.js";
2
+
1
3
  import { execFileSync, spawn, type ChildProcess } from "node:child_process";
2
4
 
3
- import { GATEWAY_PORT } from "./constants.js";
5
+ import { GATEWAY_PORT } from "../constants.js";
4
6
  import {
5
7
  clearIngressUrl,
6
8
  getDefaultWorkspaceDir,
7
9
  saveIngressUrl,
8
- } from "./ingress-config.js";
10
+ } from "../ingress-config.js";
9
11
 
10
12
  // ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
11
13
 
@@ -135,7 +137,7 @@ export interface RunCloudflareTunnelOptions {
135
137
 
136
138
  export async function runCloudflareTunnel(
137
139
  opts: RunCloudflareTunnelOptions = {},
138
- ): Promise<void> {
140
+ ): Promise<ProviderTunnelResult> {
139
141
  const version = getCloudflareTunnelVersion();
140
142
  if (!version) {
141
143
  console.error("Error: cloudflared is not installed.");
@@ -226,7 +228,16 @@ export async function runCloudflareTunnel(
226
228
  console.log("Press Ctrl+C to stop the tunnel and clear the ingress URL.");
227
229
 
228
230
  // Keep running until cloudflared exits (e.g., network error or user Ctrl+C)
229
- await new Promise<void>((resolve) => {
231
+ const waitForExit = new Promise<void>((resolve) => {
230
232
  child.on("exit", () => resolve());
231
233
  });
234
+ return { publicUrl, waitForExit };
232
235
  }
236
+
237
+ /** Per-provider copy `cadet tunnel`'s help renders for cloudflare. */
238
+ export const CLOUDFLARE_TUNNEL_META: ProviderMeta = {
239
+ reach: "Public tunnel that webhook integrations can reach",
240
+ install: "install: brew install cloudflare/cloudflare/cloudflared",
241
+ notes: ["No Cloudflare account required for quick tunnels."],
242
+ supportsReservedDomain: false,
243
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tunnel-level gateway notification: once ANY tunnel provider has a public
3
+ * URL, the gateway learns it here — ngrok-agnostic on purpose, so every
4
+ * provider conforms by returning its `publicUrl` and the dispatch (`cadet
5
+ * tunnel`'s tunnel level, or the daemon's auto-tunnel) calls this.
6
+ */
7
+
8
+ import { lookupAssistantByIdentifier } from "../assistant-config.js";
9
+ import { GATEWAY_PORT } from "../constants.js";
10
+ import { loadGuardianToken } from "../guardian-token.js";
11
+ import { loopbackSafeFetch } from "../loopback-fetch.js";
12
+
13
+ /**
14
+ * Where the gateway for this run's assistant answers. The entry's recorded
15
+ * loopback URL when one resolves; the global gateway port otherwise.
16
+ */
17
+ function gatewayUrlFor(assistantId: string | undefined): string {
18
+ if (assistantId) {
19
+ const result = lookupAssistantByIdentifier(assistantId);
20
+ if (result.status === "found") {
21
+ const url = result.entry.localUrl ?? result.entry.runtimeUrl;
22
+ if (url) return url;
23
+ }
24
+ }
25
+ return `http://127.0.0.1:${GATEWAY_PORT}`;
26
+ }
27
+
28
+ /**
29
+ * Tell the gateway the tunnel's public URL so it records the ingress domain
30
+ * in its own config, the same way its own tunnel machinery does. Best-effort:
31
+ * a gateway that is down never fails the tunnel that fronts it.
32
+ */
33
+ export async function notifyGatewayOfPublicUrl(
34
+ publicUrl: string,
35
+ assistantId: string | undefined,
36
+ ): Promise<void> {
37
+ try {
38
+ const token = assistantId
39
+ ? loadGuardianToken(assistantId)?.accessToken
40
+ : undefined;
41
+ const res = await loopbackSafeFetch(
42
+ `${gatewayUrlFor(assistantId)}/v1/ingress/domains`,
43
+ {
44
+ method: "POST",
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
48
+ },
49
+ body: JSON.stringify({ publicBaseUrl: publicUrl }),
50
+ signal: AbortSignal.timeout(5_000),
51
+ },
52
+ );
53
+ if (!res.ok) {
54
+ throw new Error(`gateway responded ${res.status}`);
55
+ }
56
+ } catch (err) {
57
+ console.warn(
58
+ ` ⚠ Could not record the ingress domain with the gateway: ${err instanceof Error ? err.message : String(err)}. The tunnel still runs.`,
59
+ );
60
+ }
61
+ }