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