@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +2 -2
  2. package/knip.json +1 -1
  3. package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
  4. package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
  5. package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
  6. package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
  7. package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
  8. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
  9. package/node_modules/@mycadet/assistant-api/index.ts +11 -11
  10. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
  11. package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  12. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  13. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  14. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  15. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  16. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  17. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  18. package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
  19. package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
  20. package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  21. package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  22. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  23. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  24. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  25. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  26. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  27. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  28. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  29. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
  30. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
  31. package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  32. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  33. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  34. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  35. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  36. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  37. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  38. package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
  39. package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
  40. package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
  41. package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
  42. package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
  43. package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
  44. package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
  45. package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
  46. package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
  47. package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
  48. package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
  49. package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
  50. package/node_modules/@mycadet/service-contracts/package.json +2 -2
  51. package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
  52. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  53. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  54. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  55. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  56. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  57. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  58. package/package.json +6 -4
  59. package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
  60. package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
  61. package/src/__tests__/assistant-client-refresh.test.ts +2 -2
  62. package/src/__tests__/backup.test.ts +86 -91
  63. package/src/__tests__/client-token.test.ts +1 -1
  64. package/src/__tests__/client-tui-refresh.test.ts +1 -1
  65. package/src/__tests__/client-web-config.test.ts +1 -2
  66. package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
  67. package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
  68. package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
  69. package/src/__tests__/connect-import.test.ts +55 -39
  70. package/src/__tests__/debug.test.ts +38 -38
  71. package/src/__tests__/ensure-binary.test.ts +182 -0
  72. package/src/__tests__/guardian-token.test.ts +149 -56
  73. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  74. package/src/__tests__/helpers/guardian-token.ts +22 -0
  75. package/src/__tests__/helpers/http.ts +10 -0
  76. package/src/__tests__/nginx-ingress.test.ts +15 -20
  77. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  78. package/src/__tests__/orphan-detection.test.ts +1 -1
  79. package/src/__tests__/pair.test.ts +149 -85
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
  81. package/src/__tests__/ps-table.test.ts +79 -0
  82. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  83. package/src/__tests__/retire.test.ts +58 -199
  84. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  85. package/src/__tests__/sleep.test.ts +19 -19
  86. package/src/__tests__/statefulset.test.ts +8 -8
  87. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
  88. package/src/__tests__/teleport-backup.test.ts +3 -3
  89. package/src/__tests__/teleport.test.ts +180 -211
  90. package/src/__tests__/tunnel.test.ts +490 -270
  91. package/src/__tests__/uninstall.test.ts +150 -9
  92. package/src/__tests__/upgrade-local.test.ts +3 -3
  93. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  94. package/src/__tests__/wake.test.ts +260 -43
  95. package/src/__tests__/workos-pkce.test.ts +2 -2
  96. package/src/commands/backup.ts +24 -24
  97. package/src/commands/client.ts +76 -78
  98. package/src/commands/completions.test.ts +104 -0
  99. package/src/commands/completions.ts +87 -0
  100. package/src/commands/debug.ts +20 -20
  101. package/src/commands/exec.ts +12 -12
  102. package/src/commands/flags.ts +2 -2
  103. package/src/commands/hatch.ts +27 -24
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/pair.ts +140 -37
  107. package/src/commands/ps.ts +232 -86
  108. package/src/commands/restart.ts +30 -0
  109. package/src/commands/restore.ts +140 -22
  110. package/src/commands/retire.ts +48 -150
  111. package/src/commands/rollback.ts +20 -26
  112. package/src/commands/setup.ts +2 -2
  113. package/src/commands/sleep.ts +2 -2
  114. package/src/commands/teleport.ts +110 -129
  115. package/src/commands/terminal.ts +158 -18
  116. package/src/commands/tunnel.ts +156 -175
  117. package/src/commands/uninstall.ts +137 -54
  118. package/src/commands/unpair.ts +3 -3
  119. package/src/commands/upgrade.ts +30 -54
  120. package/src/commands/use.ts +1 -1
  121. package/src/commands/wake.ts +166 -50
  122. package/src/index.ts +18 -9
  123. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  124. package/src/lib/__tests__/docker.test.ts +168 -20
  125. package/src/lib/__tests__/job-polling.test.ts +2 -2
  126. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  127. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  128. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  129. package/src/lib/assistant-api-key-resolution.ts +5 -5
  130. package/src/lib/assistant-client.ts +8 -8
  131. package/src/lib/assistant-config.ts +58 -12
  132. package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
  133. package/src/lib/cloud-login.ts +23 -24
  134. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  135. package/src/lib/cloud-values.ts +3 -0
  136. package/src/lib/color.ts +4 -0
  137. package/src/lib/config-utils.ts +1 -1
  138. package/src/lib/detached-process.ts +1 -1
  139. package/src/lib/docker.ts +300 -111
  140. package/src/lib/ensure-binary.ts +171 -0
  141. package/src/lib/environments/__tests__/paths.test.ts +29 -3
  142. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  143. package/src/lib/environments/paths.ts +16 -8
  144. package/src/lib/environments/resolve.ts +10 -12
  145. package/src/lib/flag-args.test.ts +1 -1
  146. package/src/lib/flag-args.ts +1 -1
  147. package/src/lib/guardian-token.ts +37 -5
  148. package/src/lib/hatch-local.ts +32 -29
  149. package/src/lib/hatch-next-steps.ts +5 -9
  150. package/src/lib/health-check.ts +6 -6
  151. package/src/lib/ingress-config.ts +8 -35
  152. package/src/lib/job-polling.ts +2 -2
  153. package/src/lib/live-voice/connection.ts +12 -12
  154. package/src/lib/live-voice/tunnel.test.ts +11 -11
  155. package/src/lib/live-voice/tunnel.ts +15 -15
  156. package/src/lib/local-runtime-client.ts +20 -20
  157. package/src/lib/local.ts +46 -34
  158. package/src/lib/nginx-ingress.ts +40 -15
  159. package/src/lib/pair.ts +1 -1
  160. package/src/lib/process.ts +1 -1
  161. package/src/lib/provider-secrets.ts +3 -3
  162. package/src/lib/retire-apple-container.ts +1 -1
  163. package/src/lib/runtime-url.ts +8 -8
  164. package/src/lib/statefulset.ts +2 -2
  165. package/src/lib/step-runner.ts +6 -1
  166. package/src/lib/sync-cloud-assistants.ts +19 -19
  167. package/src/lib/teleport-backup.ts +4 -4
  168. package/src/lib/terminal-client.ts +17 -17
  169. package/src/lib/terminal-session.ts +24 -24
  170. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  171. package/src/lib/tunnels/cadet.ts +29 -0
  172. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  173. package/src/lib/tunnels/gateway-notify.ts +61 -0
  174. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  175. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  176. package/src/lib/upgrade-lifecycle.ts +8 -21
  177. package/src/lib/upgrade-preflight.ts +2 -2
  178. package/src/lib/workos-pkce.ts +5 -5
  179. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  180. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  181. package/src/commands/recover.ts +0 -131
  182. package/src/commands/roadmap.ts +0 -459
  183. package/src/commands/ssh.ts +0 -144
  184. package/src/commands/workflows.ts +0 -305
  185. package/src/lib/managed-cloud.ts +0 -3
@@ -2,12 +2,12 @@
2
2
  * Platform terminal API client.
3
3
  *
4
4
  * Wraps the Django terminal session endpoints that proxy through vembda to
5
- * open K8s exec streams into managed assistant containers. Same transport
5
+ * open K8s exec streams into cloud assistant containers. Same transport
6
6
  * the web UI's xterm.js terminal uses.
7
7
  */
8
8
 
9
9
  import { loopbackSafeFetch } from "./loopback-fetch.js";
10
- import { authHeaders, getPlatformUrl } from "./platform-client.js";
10
+ import { authHeaders, getCloudUrl } from "./cloud-client.js";
11
11
  import { z } from "zod";
12
12
 
13
13
  const TerminalSessionResponseSchema = z.object({ session_id: z.string() });
@@ -29,10 +29,10 @@ export async function createTerminalSession(
29
29
  assistantId: string,
30
30
  cols: number,
31
31
  rows: number,
32
- platformUrl?: string,
32
+ cloudUrl?: string,
33
33
  service?: string,
34
34
  ): Promise<{ session_id: string }> {
35
- const baseUrl = platformUrl || getPlatformUrl();
35
+ const baseUrl = cloudUrl || getCloudUrl();
36
36
  const body: Record<string, unknown> = { cols, rows };
37
37
  if (service) {
38
38
  body.service = service;
@@ -41,7 +41,7 @@ export async function createTerminalSession(
41
41
  `${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/`,
42
42
  {
43
43
  method: "POST",
44
- headers: await authHeaders(token, platformUrl),
44
+ headers: await authHeaders(token, cloudUrl),
45
45
  body: JSON.stringify(body),
46
46
  },
47
47
  );
@@ -62,14 +62,14 @@ export async function closeTerminalSession(
62
62
  token: string,
63
63
  assistantId: string,
64
64
  sessionId: string,
65
- platformUrl?: string,
65
+ cloudUrl?: string,
66
66
  ): Promise<void> {
67
- const baseUrl = platformUrl || getPlatformUrl();
67
+ const baseUrl = cloudUrl || getCloudUrl();
68
68
  const response = await loopbackSafeFetch(
69
69
  `${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/${sessionId}/`,
70
70
  {
71
71
  method: "DELETE",
72
- headers: await authHeaders(token, platformUrl),
72
+ headers: await authHeaders(token, cloudUrl),
73
73
  },
74
74
  );
75
75
  // 404 = already closed, treat as success
@@ -89,14 +89,14 @@ export async function sendTerminalInput(
89
89
  assistantId: string,
90
90
  sessionId: string,
91
91
  data: string,
92
- platformUrl?: string,
92
+ cloudUrl?: string,
93
93
  ): Promise<void> {
94
- const baseUrl = platformUrl || getPlatformUrl();
94
+ const baseUrl = cloudUrl || getCloudUrl();
95
95
  const response = await loopbackSafeFetch(
96
96
  `${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/${sessionId}/input/`,
97
97
  {
98
98
  method: "POST",
99
- headers: await authHeaders(token, platformUrl),
99
+ headers: await authHeaders(token, cloudUrl),
100
100
  body: JSON.stringify({ data }),
101
101
  },
102
102
  );
@@ -113,14 +113,14 @@ export async function resizeTerminalSession(
113
113
  sessionId: string,
114
114
  cols: number,
115
115
  rows: number,
116
- platformUrl?: string,
116
+ cloudUrl?: string,
117
117
  ): Promise<void> {
118
- const baseUrl = platformUrl || getPlatformUrl();
118
+ const baseUrl = cloudUrl || getCloudUrl();
119
119
  const response = await loopbackSafeFetch(
120
120
  `${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/${sessionId}/resize/`,
121
121
  {
122
122
  method: "POST",
123
- headers: await authHeaders(token, platformUrl),
123
+ headers: await authHeaders(token, cloudUrl),
124
124
  body: JSON.stringify({ cols, rows }),
125
125
  },
126
126
  );
@@ -143,14 +143,14 @@ export async function* subscribeTerminalEvents(
143
143
  token: string,
144
144
  assistantId: string,
145
145
  sessionId: string,
146
- platformUrl?: string,
146
+ cloudUrl?: string,
147
147
  signal?: AbortSignal,
148
148
  ): AsyncGenerator<TerminalOutputEvent> {
149
- const baseUrl = platformUrl || getPlatformUrl();
149
+ const baseUrl = cloudUrl || getCloudUrl();
150
150
  const response = await loopbackSafeFetch(
151
151
  `${baseUrl}/v1/assistants/${assistantId}/terminal/sessions/${sessionId}/events/`,
152
152
  {
153
- headers: await authHeaders(token, platformUrl),
153
+ headers: await authHeaders(token, cloudUrl),
154
154
  signal,
155
155
  },
156
156
  );
@@ -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,15 @@
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 { ensureBinaryInstalled } from "../ensure-binary.js";
6
+
7
+ import { GATEWAY_PORT } from "../constants.js";
4
8
  import {
5
9
  clearIngressUrl,
6
10
  getDefaultWorkspaceDir,
7
11
  saveIngressUrl,
8
- } from "./ingress-config.js";
12
+ } from "../ingress-config.js";
9
13
 
10
14
  // ── Cloudflare Tunnel ─────────────────────────────────────────────────────────
11
15
 
@@ -135,19 +139,27 @@ export interface RunCloudflareTunnelOptions {
135
139
 
136
140
  export async function runCloudflareTunnel(
137
141
  opts: RunCloudflareTunnelOptions = {},
138
- ): Promise<void> {
139
- const version = getCloudflareTunnelVersion();
142
+ ): Promise<ProviderTunnelResult> {
143
+ const version = await ensureBinaryInstalled({
144
+ binary: "cloudflared",
145
+ purpose: "the cloudflare tunnel",
146
+ packages: {
147
+ brew: ["brew", "install", "cloudflare/cloudflare/cloudflared"],
148
+ winget: [
149
+ "winget",
150
+ "install",
151
+ "--silent",
152
+ "--accept-package-agreements",
153
+ "--accept-source-agreements",
154
+ "Cloudflare.cloudflared",
155
+ ],
156
+ choco: ["choco", "install", "-y", "cloudflared"],
157
+ },
158
+ manualHint:
159
+ "See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ for how to install",
160
+ probe: getCloudflareTunnelVersion,
161
+ });
140
162
  if (!version) {
141
- console.error("Error: cloudflared is not installed.");
142
- console.error("");
143
- console.error("Install cloudflared:");
144
- console.error(" macOS: brew install cloudflare/cloudflare/cloudflared");
145
- console.error(" Linux: https://pkg.cloudflare.com/index.html");
146
- console.error(
147
- " Windows: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
148
- );
149
- console.error("");
150
- console.error("No Cloudflare account is required for quick tunnels.");
151
163
  process.exit(1);
152
164
  }
153
165
 
@@ -226,7 +238,16 @@ export async function runCloudflareTunnel(
226
238
  console.log("Press Ctrl+C to stop the tunnel and clear the ingress URL.");
227
239
 
228
240
  // Keep running until cloudflared exits (e.g., network error or user Ctrl+C)
229
- await new Promise<void>((resolve) => {
241
+ const waitForExit = new Promise<void>((resolve) => {
230
242
  child.on("exit", () => resolve());
231
243
  });
244
+ return { publicUrl, waitForExit };
232
245
  }
246
+
247
+ /** Per-provider copy `cadet tunnel`'s help renders for cloudflare. */
248
+ export const CLOUDFLARE_TUNNEL_META: ProviderMeta = {
249
+ reach: "Public tunnel that webhook integrations can reach",
250
+ install: "install: brew install cloudflare/cloudflare/cloudflared",
251
+ notes: ["No Cloudflare account required for quick tunnels."],
252
+ supportsReservedDomain: false,
253
+ };