@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.
- package/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/{show-platform-login.ts → show-cloud-login.ts} +3 -5
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
- 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
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +3 -2
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +2 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +5 -5
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +18 -18
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -196
- package/src/__tests__/tunnel.test.ts +255 -227
- package/src/__tests__/uninstall.test.ts +13 -4
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +258 -41
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +22 -22
- package/src/commands/client.ts +76 -76
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +5 -5
- package/src/commands/hatch.ts +22 -22
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +1 -1
- package/src/commands/ps.ts +188 -79
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +138 -20
- package/src/commands/retire.ts +46 -148
- package/src/commands/rollback.ts +18 -24
- package/src/commands/setup.ts +2 -2
- package/src/commands/teleport.ts +93 -93
- package/src/commands/terminal.ts +157 -17
- package/src/commands/tunnel.ts +80 -122
- package/src/commands/uninstall.ts +55 -7
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +22 -46
- package/src/commands/wake.ts +162 -46
- package/src/index.ts +3 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/job-polling.test.ts +1 -1
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +111 -10
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/guardian-token.ts +36 -4
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +9 -30
- package/src/lib/nginx-ingress.ts +41 -14
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/sync-cloud-assistants.ts +18 -18
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
- package/src/lib/upgrade-lifecycle.ts +3 -17
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
3
10
|
import { dirname, join } from "node:path";
|
|
4
11
|
|
|
5
|
-
import {
|
|
12
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
13
|
+
|
|
14
|
+
import { GATEWAY_PORT } from "../constants.js";
|
|
15
|
+
import { clearRootProcess, stampRootProcess } from "../assistant-config.js";
|
|
6
16
|
import {
|
|
7
17
|
clearIngressUrl,
|
|
8
18
|
getDefaultWorkspaceDir,
|
|
9
|
-
loadNgrokDomain,
|
|
10
19
|
loadRawConfig,
|
|
11
20
|
saveIngressUrl,
|
|
12
|
-
|
|
13
|
-
} from "
|
|
14
|
-
import {
|
|
21
|
+
} from "../ingress-config.js";
|
|
22
|
+
import { getConfigDir } from "../environments/paths.js";
|
|
23
|
+
import { getCurrentEnvironment } from "../environments/resolve.js";
|
|
24
|
+
import { loopbackSafeFetch } from "../loopback-fetch.js";
|
|
25
|
+
import { readProcessCommandLine } from "../process.js";
|
|
15
26
|
import { z } from "zod";
|
|
27
|
+
import type { ProviderMeta, ProviderTunnelResult } from "../tunnel.js";
|
|
28
|
+
import { notifyGatewayOfPublicUrl } from "./gateway-notify.js";
|
|
16
29
|
|
|
17
30
|
const NGROK_API_URL = "http://127.0.0.1:4040/api/tunnels";
|
|
18
31
|
const NGROK_POLL_INTERVAL_MS = 500;
|
|
@@ -42,6 +55,14 @@ export const IngressConfigSchema = z.object({
|
|
|
42
55
|
publicBaseUrl: z.string().optional(),
|
|
43
56
|
});
|
|
44
57
|
|
|
58
|
+
/** Per-provider copy `cadet tunnel`'s help renders for ngrok. */
|
|
59
|
+
export const NGROK_TUNNEL_META: ProviderMeta = {
|
|
60
|
+
reach: "Public tunnel that webhook integrations can reach",
|
|
61
|
+
install: "install: brew install ngrok/ngrok/ngrok",
|
|
62
|
+
domainExample: "my-assistant.ngrok.app",
|
|
63
|
+
supportsReservedDomain: true,
|
|
64
|
+
};
|
|
65
|
+
|
|
45
66
|
/**
|
|
46
67
|
* Check whether ngrok is installed and accessible on the PATH.
|
|
47
68
|
* Returns the version string if installed, null otherwise.
|
|
@@ -118,9 +139,106 @@ function staleAgentDiagnostic(tunnels: NgrokTunnel[], port: number): string {
|
|
|
118
139
|
|
|
119
140
|
/** Recovery copy for a saved domain whose reservation may have lapsed. */
|
|
120
141
|
function savedDomainRecoveryHint(domain: string): string {
|
|
121
|
-
return `The saved ngrok domain '${domain}' (
|
|
142
|
+
return `The saved ngrok domain '${domain}' (${ngrokConfigPath()}) may no longer be reserved. Run \`cadet tunnel --provider ngrok --clear-domain\` to drop it and tunnel without one.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const NGROK_TUNNEL_NAME = "cadet";
|
|
146
|
+
|
|
147
|
+
const NgrokTunnelDefSchema = z.looseObject({
|
|
148
|
+
proto: z.string().optional(),
|
|
149
|
+
addr: z.string().optional(),
|
|
150
|
+
domain: z.string().optional(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const NgrokConfigFileSchema = z.looseObject({
|
|
154
|
+
version: z.string().optional(),
|
|
155
|
+
tunnels: z.record(z.string(), NgrokTunnelDefSchema).optional(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
type NgrokConfigFile = z.infer<typeof NgrokConfigFileSchema>;
|
|
159
|
+
type NgrokTunnelDef = z.infer<typeof NgrokTunnelDefSchema>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The ngrok-format config file this CLI owns in the XDG config dir, layered
|
|
163
|
+
* over the user's root ngrok config at spawn time (`--config` files merge in
|
|
164
|
+
* order, later wins; the user's authtoken keeps living in their root config).
|
|
165
|
+
* It is also the source of truth for the reserved domain at this layer — the
|
|
166
|
+
* workspace config no longer records one.
|
|
167
|
+
*/
|
|
168
|
+
export function ngrokConfigPath(): string {
|
|
169
|
+
return join(getConfigDir(getCurrentEnvironment()), "tunnels", "ngrok.yaml");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readNgrokConfigFile(): NgrokConfigFile {
|
|
173
|
+
const path = ngrokConfigPath();
|
|
174
|
+
if (!existsSync(path)) return {};
|
|
175
|
+
try {
|
|
176
|
+
const parsed = NgrokConfigFileSchema.safeParse(
|
|
177
|
+
parseYaml(readFileSync(path, "utf-8")),
|
|
178
|
+
);
|
|
179
|
+
return parsed.success ? parsed.data : {};
|
|
180
|
+
} catch {
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeNgrokConfigFile(config: NgrokConfigFile): void {
|
|
186
|
+
const path = ngrokConfigPath();
|
|
187
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
188
|
+
writeFileSync(path, stringifyYaml(config));
|
|
122
189
|
}
|
|
123
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Persist a reserved ngrok domain in the XDG ngrok config; null clears it.
|
|
193
|
+
*
|
|
194
|
+
* The reserved domain is an ngrok reservation concept (cloudflare quick
|
|
195
|
+
* tunnels and tailscale serve have no equivalent), which is why this lives in
|
|
196
|
+
* the ngrok provider module rather than the shared ingress-config helpers.
|
|
197
|
+
*/
|
|
198
|
+
export function saveNgrokDomain(domain: string | null): void {
|
|
199
|
+
const config = readNgrokConfigFile();
|
|
200
|
+
const cadet: NgrokTunnelDef = {
|
|
201
|
+
...config.tunnels?.[NGROK_TUNNEL_NAME],
|
|
202
|
+
proto: "http",
|
|
203
|
+
};
|
|
204
|
+
if (domain) {
|
|
205
|
+
cadet.domain = domain;
|
|
206
|
+
} else {
|
|
207
|
+
delete cadet.domain;
|
|
208
|
+
}
|
|
209
|
+
const tunnels: Record<string, NgrokTunnelDef> = { ...config.tunnels };
|
|
210
|
+
tunnels[NGROK_TUNNEL_NAME] = cadet;
|
|
211
|
+
const next: NgrokConfigFile = { ...config, version: config.version ?? "2" };
|
|
212
|
+
next.tunnels = tunnels;
|
|
213
|
+
writeNgrokConfigFile(next);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Read the reserved ngrok domain from the XDG ngrok config, if saved. */
|
|
217
|
+
export function loadNgrokDomain(): string | null {
|
|
218
|
+
const domain = readNgrokConfigFile().tunnels?.[NGROK_TUNNEL_NAME]?.domain;
|
|
219
|
+
return domain?.trim() ? domain : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check whether a PID belongs to an ngrok process via its command line, so a
|
|
224
|
+
* recorded pid is verified before it is signaled (PID reuse guard).
|
|
225
|
+
*/
|
|
226
|
+
export function isNgrokProcess(pid: number): boolean {
|
|
227
|
+
try {
|
|
228
|
+
return /ngrok/i.test(readProcessCommandLine(pid));
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Matches the adoption line the tunnel workflows print when they find an
|
|
236
|
+
* already-running tunnel, so the detached-tunnel supervisor can detect
|
|
237
|
+
* adoption without this module's implementation details leaking into the
|
|
238
|
+
* command layer.
|
|
239
|
+
*/
|
|
240
|
+
export const ADOPTED_TUNNEL_RE = /Found existing ngrok tunnel: (\S+)/;
|
|
241
|
+
|
|
124
242
|
/** Whether a tunnel public URL's host equals the given reserved domain. */
|
|
125
243
|
function urlMatchesDomain(publicUrl: string, domain: string): boolean {
|
|
126
244
|
try {
|
|
@@ -133,15 +251,42 @@ function urlMatchesDomain(publicUrl: string, domain: string): boolean {
|
|
|
133
251
|
}
|
|
134
252
|
|
|
135
253
|
/**
|
|
136
|
-
*
|
|
254
|
+
* Write the cadet-owned XDG ngrok config for this run: the tunnel definition
|
|
255
|
+
* `ngrok start` reads, carrying the loopback target and — when reserved — the
|
|
256
|
+
* bound domain. The file is layered over the user's root ngrok config at
|
|
257
|
+
* spawn, so their authtoken and settings keep applying untouched.
|
|
258
|
+
*/
|
|
259
|
+
function writeNgrokConfig(targetPort: number, domain?: string): void {
|
|
260
|
+
const config = readNgrokConfigFile();
|
|
261
|
+
const cadet: NgrokTunnelDef = {
|
|
262
|
+
...config.tunnels?.[NGROK_TUNNEL_NAME],
|
|
263
|
+
proto: "http",
|
|
264
|
+
// Explicit over a bare port, which ngrok expands to `localhost`, whose
|
|
265
|
+
// ::1 answer resolves first on macOS and races the edge's IPv4 bind.
|
|
266
|
+
addr: `127.0.0.1:${targetPort}`,
|
|
267
|
+
};
|
|
268
|
+
if (domain) {
|
|
269
|
+
cadet.domain = domain;
|
|
270
|
+
}
|
|
271
|
+
const tunnels: Record<string, NgrokTunnelDef> = { ...config.tunnels };
|
|
272
|
+
tunnels[NGROK_TUNNEL_NAME] = cadet;
|
|
273
|
+
const next: NgrokConfigFile = { ...config, version: config.version ?? "2" };
|
|
274
|
+
next.tunnels = tunnels;
|
|
275
|
+
writeNgrokConfigFile(next);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Start an ngrok process tunneling HTTP traffic to the given local port, via
|
|
280
|
+
* the cadet-owned XDG config (`ngrok start cadet`) so the agent never collides
|
|
281
|
+
* with the host machine's root ngrok config.
|
|
137
282
|
*
|
|
138
283
|
* When `logFilePath` is provided, stdout/stderr are redirected to that file
|
|
139
284
|
* instead of being piped. This avoids keeping pipe handles open in the
|
|
140
285
|
* parent process — which would either prevent the CLI from exiting (if
|
|
141
286
|
* handles are left open) or send SIGPIPE to ngrok (if destroyed).
|
|
142
287
|
*
|
|
143
|
-
* When `domain` is set, the tunnel binds that reserved ngrok domain
|
|
144
|
-
*
|
|
288
|
+
* When `domain` is set, the spawned tunnel binds that reserved ngrok domain,
|
|
289
|
+
* recorded in the XDG config alongside the loopback target.
|
|
145
290
|
*
|
|
146
291
|
* Returns the spawned child process.
|
|
147
292
|
*/
|
|
@@ -159,12 +304,13 @@ export function startNgrokProcess(
|
|
|
159
304
|
stdio = ["ignore", fd, fd];
|
|
160
305
|
}
|
|
161
306
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
307
|
+
writeNgrokConfig(targetPort, domain);
|
|
308
|
+
const args = [
|
|
309
|
+
"start",
|
|
310
|
+
NGROK_TUNNEL_NAME,
|
|
311
|
+
`--config=${ngrokConfigPath()}`,
|
|
312
|
+
"--log=stdout",
|
|
313
|
+
];
|
|
168
314
|
|
|
169
315
|
const child = spawn("ngrok", args, {
|
|
170
316
|
detached: true,
|
|
@@ -225,10 +371,7 @@ export function hasWebhookIntegrations(
|
|
|
225
371
|
const telegram = TelegramIntegrationSchema.safeParse(config.telegram);
|
|
226
372
|
if (telegram.success && telegram.data.botUsername) return true;
|
|
227
373
|
const twilio = TwilioIntegrationSchema.safeParse(config.twilio);
|
|
228
|
-
if (
|
|
229
|
-
twilio.success &&
|
|
230
|
-
(twilio.data.accountSid || twilio.data.phoneNumber)
|
|
231
|
-
) {
|
|
374
|
+
if (twilio.success && (twilio.data.accountSid || twilio.data.phoneNumber)) {
|
|
232
375
|
return true;
|
|
233
376
|
}
|
|
234
377
|
return false;
|
|
@@ -282,7 +425,7 @@ export async function maybeStartNgrokTunnel(
|
|
|
282
425
|
workspaceDir: string,
|
|
283
426
|
assistantId?: string,
|
|
284
427
|
): Promise<ChildProcess | null> {
|
|
285
|
-
// Managed/containerized deployments route webhooks through the
|
|
428
|
+
// Managed/containerized deployments route webhooks through the cloud's
|
|
286
429
|
// callback proxy. ngrok is not needed and would not be reachable from the
|
|
287
430
|
// platform anyway — skip it entirely.
|
|
288
431
|
const isContainerized =
|
|
@@ -295,7 +438,7 @@ export async function maybeStartNgrokTunnel(
|
|
|
295
438
|
const version = getNgrokVersion();
|
|
296
439
|
if (!version) return null;
|
|
297
440
|
|
|
298
|
-
const savedDomain = loadNgrokDomain(
|
|
441
|
+
const savedDomain = loadNgrokDomain() ?? undefined;
|
|
299
442
|
|
|
300
443
|
// Reuse an existing tunnel if one is already running
|
|
301
444
|
const runningTunnels = (await queryNgrokTunnels()) ?? [];
|
|
@@ -312,6 +455,10 @@ export async function maybeStartNgrokTunnel(
|
|
|
312
455
|
}
|
|
313
456
|
console.log(` Found existing ngrok tunnel: ${existingUrl}`);
|
|
314
457
|
saveIngressUrl(workspaceDir, existingUrl, assistantId, "ngrok");
|
|
458
|
+
await notifyGatewayOfPublicUrl(existingUrl, assistantId);
|
|
459
|
+
// The adopted tunnel belongs to some other agent we do not own a pid for,
|
|
460
|
+
// so any recorded tunnel process entry would be stale from here on.
|
|
461
|
+
clearRootProcess("tunnel");
|
|
315
462
|
return null;
|
|
316
463
|
}
|
|
317
464
|
if (runningTunnels.length > 0) {
|
|
@@ -339,6 +486,15 @@ export async function maybeStartNgrokTunnel(
|
|
|
339
486
|
try {
|
|
340
487
|
const publicUrl = await waitForNgrokUrl(targetPort, savedDomain);
|
|
341
488
|
saveIngressUrl(workspaceDir, publicUrl, assistantId, "ngrok");
|
|
489
|
+
await notifyGatewayOfPublicUrl(publicUrl, assistantId);
|
|
490
|
+
if (ngrokProcess.pid) {
|
|
491
|
+
stampRootProcess("tunnel", {
|
|
492
|
+
pid: ngrokProcess.pid,
|
|
493
|
+
provider: "ngrok",
|
|
494
|
+
startedAt: new Date().toISOString(),
|
|
495
|
+
assistantId,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
342
498
|
console.log(` Tunnel established: ${publicUrl}`);
|
|
343
499
|
|
|
344
500
|
return ngrokProcess;
|
|
@@ -350,6 +506,7 @@ export async function maybeStartNgrokTunnel(
|
|
|
350
506
|
console.warn(` ⚠ ${savedDomainRecoveryHint(savedDomain)}`);
|
|
351
507
|
}
|
|
352
508
|
if (!ngrokProcess.killed) ngrokProcess.kill("SIGTERM");
|
|
509
|
+
clearRootProcess("tunnel");
|
|
353
510
|
return null;
|
|
354
511
|
}
|
|
355
512
|
}
|
|
@@ -370,11 +527,13 @@ export interface RunNgrokTunnelOptions {
|
|
|
370
527
|
|
|
371
528
|
/**
|
|
372
529
|
* Run the ngrok tunnel workflow: check installation, find or start a tunnel,
|
|
373
|
-
* save the public URL to config
|
|
530
|
+
* and save the public URL to config. Hands the URL back so the tunnel level
|
|
531
|
+
* can notify the gateway of it; `waitForExit` settles when the run ends
|
|
532
|
+
* (process exit, signal, or detach).
|
|
374
533
|
*/
|
|
375
534
|
export async function runNgrokTunnel(
|
|
376
535
|
opts: RunNgrokTunnelOptions = {},
|
|
377
|
-
): Promise<
|
|
536
|
+
): Promise<ProviderTunnelResult> {
|
|
378
537
|
const version = getNgrokVersion();
|
|
379
538
|
if (!version) {
|
|
380
539
|
console.error("Error: ngrok is not installed.");
|
|
@@ -397,7 +556,7 @@ export async function runNgrokTunnel(
|
|
|
397
556
|
|
|
398
557
|
// The saved domain is standing intent: a run without --domain reuses it,
|
|
399
558
|
// and only an explicit --domain rewrites it.
|
|
400
|
-
const domain = opts.domain ?? loadNgrokDomain(
|
|
559
|
+
const domain = opts.domain ?? loadNgrokDomain() ?? undefined;
|
|
401
560
|
if (domain && !opts.domain) {
|
|
402
561
|
console.log(`Using saved ngrok domain: ${domain}`);
|
|
403
562
|
}
|
|
@@ -426,8 +585,10 @@ export async function runNgrokTunnel(
|
|
|
426
585
|
}
|
|
427
586
|
console.log(`Found existing ngrok tunnel: ${existingUrl}`);
|
|
428
587
|
saveIngressUrl(workspaceDir, existingUrl, opts.assistantId, "ngrok");
|
|
588
|
+
// Not our child process: a recorded entry would be stale or foreign.
|
|
589
|
+
clearRootProcess("tunnel");
|
|
429
590
|
if (opts.domain) {
|
|
430
|
-
saveNgrokDomain(
|
|
591
|
+
saveNgrokDomain(opts.domain);
|
|
431
592
|
}
|
|
432
593
|
console.log("Ingress URL saved to config.");
|
|
433
594
|
console.log("");
|
|
@@ -436,11 +597,11 @@ export async function runNgrokTunnel(
|
|
|
436
597
|
);
|
|
437
598
|
|
|
438
599
|
// Block until SIGINT/SIGTERM
|
|
439
|
-
|
|
600
|
+
const waitForExit = new Promise<void>((resolve) => {
|
|
440
601
|
process.on("SIGINT", () => resolve());
|
|
441
602
|
process.on("SIGTERM", () => resolve());
|
|
442
603
|
});
|
|
443
|
-
return;
|
|
604
|
+
return { publicUrl: existingUrl, waitForExit };
|
|
444
605
|
}
|
|
445
606
|
|
|
446
607
|
console.log(`Starting ngrok tunnel to localhost:${port}...`);
|
|
@@ -453,6 +614,7 @@ export async function runNgrokTunnel(
|
|
|
453
614
|
if (!ngrokProcess.killed) {
|
|
454
615
|
ngrokProcess.kill("SIGTERM");
|
|
455
616
|
}
|
|
617
|
+
clearRootProcess("tunnel");
|
|
456
618
|
if (publicUrl) {
|
|
457
619
|
console.log("\nClearing ingress URL from config...");
|
|
458
620
|
clearIngressUrl(workspaceDir, opts.assistantId);
|
|
@@ -510,15 +672,24 @@ export async function runNgrokTunnel(
|
|
|
510
672
|
// The domain is standing intent, not tunnel state: cleanup clears the
|
|
511
673
|
// ingress URL but leaves the domain saved for wake/daemon restores.
|
|
512
674
|
saveIngressUrl(workspaceDir, publicUrl, opts.assistantId, "ngrok");
|
|
675
|
+
if (ngrokProcess.pid) {
|
|
676
|
+
stampRootProcess("tunnel", {
|
|
677
|
+
pid: ngrokProcess.pid,
|
|
678
|
+
provider: "ngrok",
|
|
679
|
+
startedAt: new Date().toISOString(),
|
|
680
|
+
assistantId: opts.assistantId,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
513
683
|
if (opts.domain) {
|
|
514
|
-
saveNgrokDomain(
|
|
684
|
+
saveNgrokDomain(opts.domain);
|
|
515
685
|
}
|
|
516
686
|
console.log("Ingress URL saved to config.");
|
|
517
687
|
console.log("");
|
|
518
688
|
console.log("Press Ctrl+C to stop the tunnel and clear the ingress URL.");
|
|
519
689
|
|
|
520
690
|
// Keep running until the ngrok process exits or we receive a signal
|
|
521
|
-
|
|
691
|
+
const waitForExit = new Promise<void>((resolve) => {
|
|
522
692
|
ngrokProcess.on("exit", () => resolve());
|
|
523
693
|
});
|
|
694
|
+
return { publicUrl, waitForExit };
|
|
524
695
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { ProviderMeta, ProviderTunnelResult } from "../tunnel.js";
|
|
2
|
+
|
|
1
3
|
import { spawnSync } from "node:child_process";
|
|
2
4
|
import { win32 } from "node:path";
|
|
3
5
|
|
|
4
|
-
import { GATEWAY_PORT } from "
|
|
6
|
+
import { GATEWAY_PORT } from "../constants.js";
|
|
5
7
|
import {
|
|
6
8
|
clearIngressUrl,
|
|
7
9
|
getDefaultWorkspaceDir,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
saveIngressUrl,
|
|
10
12
|
savePairingTunnel,
|
|
11
13
|
stampLockfileIngressUrl,
|
|
12
|
-
} from "
|
|
14
|
+
} from "../ingress-config.js";
|
|
13
15
|
|
|
14
16
|
// ── Tailscale CLI discovery + invocation ────────────────────────────────────
|
|
15
17
|
|
|
@@ -351,7 +353,7 @@ export function stopTailscaleServe(
|
|
|
351
353
|
*/
|
|
352
354
|
export async function runTailscaleTunnel(
|
|
353
355
|
opts: RunTailscaleTunnelOptions = {},
|
|
354
|
-
): Promise<
|
|
356
|
+
): Promise<ProviderTunnelResult> {
|
|
355
357
|
const deps = realTailscaleDeps();
|
|
356
358
|
// What this run publishes, and therefore what Ctrl+C takes back.
|
|
357
359
|
const publishedLabel = opts.preserveIngressUrl
|
|
@@ -375,7 +377,7 @@ export async function runTailscaleTunnel(
|
|
|
375
377
|
"This URL is reachable only from devices signed in to your tailnet.",
|
|
376
378
|
);
|
|
377
379
|
console.log(
|
|
378
|
-
"For public webhook ingress,
|
|
380
|
+
"For public webhook ingress, run `cadet tunnel --help` to see the public providers.",
|
|
379
381
|
);
|
|
380
382
|
console.log("");
|
|
381
383
|
console.log(
|
|
@@ -429,5 +431,17 @@ export async function runTailscaleTunnel(
|
|
|
429
431
|
// Hold the process open until a signal fires. The serve itself lives in
|
|
430
432
|
// tailscaled, so there is no child process to await — the registered signal
|
|
431
433
|
// listeners keep the event loop alive.
|
|
432
|
-
|
|
434
|
+
const waitForExit = new Promise<void>(() => {});
|
|
435
|
+
return { publicUrl, waitForExit };
|
|
433
436
|
}
|
|
437
|
+
|
|
438
|
+
/** Per-provider copy `cadet tunnel`'s help renders for tailscale. */
|
|
439
|
+
export const TAILSCALE_TUNNEL_META: ProviderMeta = {
|
|
440
|
+
reach: "Tailnet-only tunnel, reachable from your own devices",
|
|
441
|
+
install: "install: https://tailscale.com/download, then run `tailscale up`",
|
|
442
|
+
notes: [
|
|
443
|
+
"When webhook integrations are configured it records its URL for device",
|
|
444
|
+
"pairing only, leaving the saved webhook callback base as it is.",
|
|
445
|
+
],
|
|
446
|
+
supportsReservedDomain: false,
|
|
447
|
+
};
|
|
@@ -24,7 +24,7 @@ import { getStateDir } from "./environments/paths.js";
|
|
|
24
24
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
25
25
|
import { loadGuardianToken } from "./guardian-token.js";
|
|
26
26
|
import { classifyReadyzResponse } from "./http-client.js";
|
|
27
|
-
import { fetchReleases, resolveImageRefs } from "./
|
|
27
|
+
import { fetchReleases, resolveImageRefs } from "./cloud-releases.js";
|
|
28
28
|
import { z } from "zod";
|
|
29
29
|
|
|
30
30
|
const GatewayHealthSchema = z.object({
|
|
@@ -332,7 +332,7 @@ export async function fetchAssistantIngressUrl(
|
|
|
332
332
|
publicBaseUrl?: string;
|
|
333
333
|
managedCallbacks?: boolean;
|
|
334
334
|
};
|
|
335
|
-
// Ignore managed-callback URLs — those belong to the
|
|
335
|
+
// Ignore managed-callback URLs — those belong to the cloud, not the
|
|
336
336
|
// self-hosted assistant's own ingress.
|
|
337
337
|
if (body.managedCallbacks) return undefined;
|
|
338
338
|
return body.publicBaseUrl || undefined;
|
|
@@ -348,7 +348,7 @@ export async function fetchAssistantIngressUrl(
|
|
|
348
348
|
*
|
|
349
349
|
* Checks (in order):
|
|
350
350
|
* 1. `entry.previousVersion` (saved by the upgrade flow from health).
|
|
351
|
-
* 2. The releases list from the
|
|
351
|
+
* 2. The releases list from the cloud API — finds the version
|
|
352
352
|
* immediately before `currentVersion`.
|
|
353
353
|
*
|
|
354
354
|
* Returns `undefined` when neither source yields a result.
|
|
@@ -869,8 +869,6 @@ export async function performDockerRollback(
|
|
|
869
869
|
);
|
|
870
870
|
|
|
871
871
|
// Capture new digests from the rolled-back containers
|
|
872
|
-
const newDigests = await captureImageRefs(res);
|
|
873
|
-
|
|
874
872
|
// Swap current/previous state to enable "rollback the rollback"
|
|
875
873
|
const updatedEntry: AssistantEntry = {
|
|
876
874
|
...entry,
|
|
@@ -878,10 +876,6 @@ export async function performDockerRollback(
|
|
|
878
876
|
assistantImage: targetImageTags.assistant,
|
|
879
877
|
gatewayImage: targetImageTags.gateway,
|
|
880
878
|
cesImage: targetImageTags["credential-executor"],
|
|
881
|
-
assistantDigest: newDigests?.assistant,
|
|
882
|
-
gatewayDigest: newDigests?.gateway,
|
|
883
|
-
cesDigest: newDigests?.["credential-executor"],
|
|
884
|
-
networkName: res.network,
|
|
885
879
|
assistantPort,
|
|
886
880
|
},
|
|
887
881
|
previousContainerInfo: entry.containerInfo,
|
|
@@ -1030,7 +1024,6 @@ export async function performDockerRollback(
|
|
|
1030
1024
|
}
|
|
1031
1025
|
|
|
1032
1026
|
// Restore lockfile state
|
|
1033
|
-
const revertDigests = await captureImageRefs(res);
|
|
1034
1027
|
const revertedEntry: AssistantEntry = {
|
|
1035
1028
|
...entry,
|
|
1036
1029
|
containerInfo: {
|
|
@@ -1042,13 +1035,6 @@ export async function performDockerRollback(
|
|
|
1042
1035
|
cesImage:
|
|
1043
1036
|
entry.containerInfo?.cesImage ??
|
|
1044
1037
|
currentImageRefs["credential-executor"],
|
|
1045
|
-
assistantDigest:
|
|
1046
|
-
revertDigests?.assistant ?? currentImageRefs.assistant,
|
|
1047
|
-
gatewayDigest: revertDigests?.gateway ?? currentImageRefs.gateway,
|
|
1048
|
-
cesDigest:
|
|
1049
|
-
revertDigests?.["credential-executor"] ??
|
|
1050
|
-
currentImageRefs["credential-executor"],
|
|
1051
|
-
networkName: res.network,
|
|
1052
1038
|
assistantPort,
|
|
1053
1039
|
},
|
|
1054
1040
|
previousContainerInfo: undefined,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReleaseListItem } from "./
|
|
1
|
+
import type { ReleaseListItem } from "./cloud-releases.js";
|
|
2
2
|
import { compareVersions, versionsEqual } from "./version-compat.js";
|
|
3
3
|
|
|
4
4
|
export interface UpgradeTargetResolution {
|
|
@@ -88,7 +88,7 @@ export interface UpgradePollState {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Completion predicate for the
|
|
91
|
+
* Completion predicate for the cloud upgrade poll loop. Pure.
|
|
92
92
|
*
|
|
93
93
|
* The primary signal is the DB-backed `current_release_version` (works while
|
|
94
94
|
* the service group restarts or the assistant sleeps); the upgrade-status
|
package/src/lib/workos-pkce.ts
CHANGED
|
@@ -103,9 +103,9 @@ export function selectWorkosClientId(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export async function fetchWorkosClientId(
|
|
106
|
-
|
|
106
|
+
cloudUrl: string,
|
|
107
107
|
): Promise<string> {
|
|
108
|
-
const url = `${new URL(
|
|
108
|
+
const url = `${new URL(cloudUrl).origin}/_allauth/app/v1/config`;
|
|
109
109
|
const response = await loopbackSafeFetch(url);
|
|
110
110
|
if (!response.ok) {
|
|
111
111
|
throw new Error(`Failed to fetch auth config (${response.status})`);
|
|
@@ -157,13 +157,13 @@ export async function exchangeCodeWithWorkos(options: {
|
|
|
157
157
|
return parsed.data.access_token;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/** Exchange the WorkOS access token for a
|
|
160
|
+
/** Exchange the WorkOS access token for a cloud session token. */
|
|
161
161
|
export async function exchangeAccessTokenForSession(
|
|
162
|
-
|
|
162
|
+
cloudUrl: string,
|
|
163
163
|
clientId: string,
|
|
164
164
|
accessToken: string,
|
|
165
165
|
): Promise<string> {
|
|
166
|
-
const url = `${new URL(
|
|
166
|
+
const url = `${new URL(cloudUrl).origin}/_allauth/app/v1/auth/provider/token`;
|
|
167
167
|
const response = await loopbackSafeFetch(url, {
|
|
168
168
|
method: "POST",
|
|
169
169
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `platform_disconnected` SSE event.
|
|
3
|
-
*
|
|
4
|
-
* Emitted by the platform-disconnect route after stored platform
|
|
5
|
-
* credentials are deleted, notifying connected clients that the platform
|
|
6
|
-
* connection is gone.
|
|
7
|
-
*
|
|
8
|
-
* Canonical wire-contract source. Daemon code imports the type
|
|
9
|
-
* directly from this file; external consumers import via
|
|
10
|
-
* `@mycadet/assistant-api`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { z } from "zod";
|
|
14
|
-
|
|
15
|
-
export const PlatformDisconnectedEventSchema = z.object({
|
|
16
|
-
type: z.literal("platform_disconnected"),
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export type PlatformDisconnectedEvent = z.infer<
|
|
20
|
-
typeof PlatformDisconnectedEventSchema
|
|
21
|
-
>;
|