@mycadet/cli 0.12.9 → 0.12.11
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/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-web-config.test.ts +0 -1
- package/src/__tests__/cloud-login-poll.test.ts +69 -0
- package/src/__tests__/devices.test.ts +62 -0
- package/src/__tests__/ensure-binary.test.ts +52 -0
- package/src/__tests__/hatch-next-steps.test.ts +38 -0
- package/src/__tests__/multi-local.test.ts +3 -3
- package/src/__tests__/nginx-ingress.test.ts +58 -26
- package/src/__tests__/pair.test.ts +149 -78
- package/src/__tests__/ps-cloud-status.test.ts +6 -4
- package/src/__tests__/setup.test.ts +14 -4
- package/src/__tests__/teleport.test.ts +0 -15
- package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
- package/src/__tests__/tunnel-stop.test.ts +226 -0
- package/src/__tests__/tunnel.test.ts +265 -70
- package/src/__tests__/uninstall.test.ts +140 -8
- package/src/commands/client.ts +4 -6
- package/src/commands/devices.ts +66 -30
- package/src/commands/hatch.ts +14 -53
- package/src/commands/pair.ts +147 -38
- package/src/commands/ps.ts +10 -20
- package/src/commands/teleport.ts +2 -21
- package/src/commands/tunnel.ts +67 -28
- package/src/commands/uninstall.ts +106 -71
- package/src/commands/upgrade.ts +6 -1
- package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
- package/src/lib/assistant-config.ts +20 -5
- package/src/lib/cloud-client.ts +0 -3
- package/src/lib/cloud-login-poll.ts +75 -0
- package/src/lib/cloud-login.ts +3 -2
- package/src/lib/color.ts +4 -0
- package/src/lib/docker.ts +46 -70
- package/src/lib/ensure-binary.ts +65 -6
- package/src/lib/environments/__tests__/paths.test.ts +27 -1
- package/src/lib/environments/paths.ts +16 -8
- package/src/lib/environments/resolve.ts +2 -4
- package/src/lib/hatch-local.ts +16 -59
- package/src/lib/hatch-next-steps.ts +18 -9
- package/src/lib/live-voice/tunnel.ts +2 -2
- package/src/lib/local.ts +42 -9
- package/src/lib/nginx-ingress.ts +124 -24
- package/src/lib/step-runner.ts +6 -1
- package/src/lib/tunnel.ts +86 -34
- package/src/lib/tunnels/cadet.ts +12 -1
- package/src/lib/tunnels/cloudflare.ts +36 -1
- package/src/lib/tunnels/ngrok.ts +16 -2
- package/src/lib/tunnels/process-lifecycle.ts +26 -0
- package/src/lib/tunnels/tailscale.ts +79 -1
- package/src/lib/xdg-log.ts +11 -0
package/src/commands/pair.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Print the pairing link for another device, and the same link rendered as a
|
|
5
5
|
* QR code. Runs on the machine hosting the assistant: it mints a remote-web
|
|
6
|
-
* pairing challenge
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* pairing challenge through the gateway CLI (`gateway pairing mint <url>` —
|
|
7
|
+
* locally, or docker-exec'd into the gateway container) which approves it
|
|
8
|
+
* immediately, so running this command IS the local-presence proof and one
|
|
9
|
+
* scan (or one paste into `cadet connect import`) completes the pairing.
|
|
10
|
+
* local and docker assistants mint without any HTTP; other topologies keep
|
|
11
|
+
* the loopback HTTP mint.
|
|
9
12
|
*
|
|
10
13
|
* `--web-approve` is the reverse direction: approve a code a device already
|
|
11
14
|
* shows, for pairings that device started on its own.
|
|
@@ -38,8 +41,11 @@ import {
|
|
|
38
41
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
39
42
|
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
40
43
|
import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
44
|
+
import { dockerResourceNames } from "../lib/docker.js";
|
|
45
|
+
import { gatewayCliInvocation } from "../lib/local.js";
|
|
41
46
|
import { formatWebApproveFailure, parseGatewayErrorCode } from "../lib/pair.js";
|
|
42
47
|
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
48
|
+
import { execOutput } from "../lib/step-runner.js";
|
|
43
49
|
|
|
44
50
|
function assistantDisplayName(entry: AssistantEntry): string {
|
|
45
51
|
return entry.name || entry.assistantName || entry.assistantId;
|
|
@@ -89,9 +95,10 @@ function printUsage(): void {
|
|
|
89
95
|
USAGE:
|
|
90
96
|
cadet pair [assistant] [options]
|
|
91
97
|
|
|
92
|
-
Mints a pairing challenge on the assistant's gateway
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
Mints a pairing challenge on the assistant's gateway (via the gateway CLI
|
|
99
|
+
for local and docker assistants, over loopback HTTP otherwise) and approves
|
|
100
|
+
it on the spot (running this command on the host machine is the approval),
|
|
101
|
+
then prints the pairing link and the same link as a QR code.
|
|
95
102
|
Scanning the QR, opening the link, or passing it to 'cadet connect import'
|
|
96
103
|
on the other device completes the pairing in one step.
|
|
97
104
|
|
|
@@ -214,26 +221,122 @@ async function exitOnHttpError(response: Response): Promise<Response> {
|
|
|
214
221
|
return response;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Mint (and approve) a remote-web pairing challenge. `local` and `docker`
|
|
226
|
+
* remotes mint through the gateway CLI — `gateway pairing mint <url>` run on
|
|
227
|
+
* the assistant's host (locally, or docker-exec'd into the gateway container)
|
|
228
|
+
* — replacing the old loopback HTTP mint whose loopback-peer gate
|
|
229
|
+
* containerized gateways could never satisfy (published-port NAT rewrites
|
|
230
|
+
* host-originated peers to the docker bridge address). The mint executes the
|
|
231
|
+
* pairing logic directly against the file-backed challenge store, so the
|
|
232
|
+
* running gateway's verification routes see it with no IPC round-trip.
|
|
233
|
+
* Process execution is the local-presence proof; the mint command approves
|
|
234
|
+
* on the spot, so the returned challenge is already paired-ready. Other
|
|
235
|
+
* topologies keep the loopback HTTP mint they used before.
|
|
236
|
+
*/
|
|
237
|
+
async function mintPairingChallengeOrExit(
|
|
238
|
+
entry: AssistantEntry,
|
|
220
239
|
publicBaseUrl: string,
|
|
221
240
|
): Promise<RemoteWebPairingChallengeResponse> {
|
|
241
|
+
if (entry.cloud !== "local" && entry.cloud !== "docker") {
|
|
242
|
+
return mintPairingChallengeOverLoopbackOrExit(entry, publicBaseUrl);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let stdout: string;
|
|
246
|
+
try {
|
|
247
|
+
if (entry.cloud === "docker") {
|
|
248
|
+
stdout = await execOutput(
|
|
249
|
+
"docker",
|
|
250
|
+
[
|
|
251
|
+
"exec",
|
|
252
|
+
dockerResourceNames(entry.assistantId).gatewayContainer,
|
|
253
|
+
"bun",
|
|
254
|
+
"run",
|
|
255
|
+
"src/cli/index.ts",
|
|
256
|
+
"pairing",
|
|
257
|
+
"mint",
|
|
258
|
+
publicBaseUrl,
|
|
259
|
+
],
|
|
260
|
+
{ timeoutMs: 30_000 },
|
|
261
|
+
);
|
|
262
|
+
} else {
|
|
263
|
+
const invocation = gatewayCliInvocation(entry.resources);
|
|
264
|
+
stdout = await execOutput(
|
|
265
|
+
invocation.command,
|
|
266
|
+
[...invocation.args, "pairing", "mint", publicBaseUrl],
|
|
267
|
+
{
|
|
268
|
+
cwd: invocation.cwd,
|
|
269
|
+
env: invocation.env,
|
|
270
|
+
timeoutMs: 30_000,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
console.error(
|
|
276
|
+
`Error: could not mint a pairing challenge on the gateway ` +
|
|
277
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
278
|
+
);
|
|
279
|
+
console.error("Is the assistant running? Try `cadet wake`.");
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const lastLine = stdout.split("\n").filter(Boolean).pop() ?? "";
|
|
284
|
+
let challenge: RemoteWebPairingChallengeResponse;
|
|
285
|
+
try {
|
|
286
|
+
challenge = JSON.parse(lastLine) as RemoteWebPairingChallengeResponse;
|
|
287
|
+
} catch {
|
|
288
|
+
console.error(
|
|
289
|
+
"Error: could not parse the gateway's pairing mint output.",
|
|
290
|
+
);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
if (!challenge.deviceCode || !challenge.userCode) {
|
|
294
|
+
console.error(
|
|
295
|
+
"Error: the gateway's pairing mint output is missing the challenge codes.",
|
|
296
|
+
);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
return challenge;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Mint a challenge over the gateway's loopback HTTP route — the path every
|
|
304
|
+
* topology took before the gateway-CLI mint existed. Kept for remotes that
|
|
305
|
+
* are neither `local` nor `docker` (apple containers, SSH-managed hosts,
|
|
306
|
+
* cloud-managed), where no gateway-CLI execution surface is defined.
|
|
307
|
+
*/
|
|
308
|
+
async function mintPairingChallengeOverLoopbackOrExit(
|
|
309
|
+
entry: AssistantEntry,
|
|
310
|
+
publicBaseUrl: string,
|
|
311
|
+
): Promise<RemoteWebPairingChallengeResponse> {
|
|
312
|
+
// Mint over loopback (localUrl avoids mDNS for same-machine calls), then
|
|
313
|
+
// approve immediately: running this CLI on the host IS the local-presence
|
|
314
|
+
// proof, so the other device completes pairing in one step from the link.
|
|
315
|
+
const mintUrl = (
|
|
316
|
+
entry.localUrl ||
|
|
317
|
+
entry.runtimeUrl ||
|
|
318
|
+
`http://127.0.0.1:${GATEWAY_PORT}`
|
|
319
|
+
).replace(/\/+$/, "");
|
|
222
320
|
const response = await gatewayPostOrExit(
|
|
223
|
-
|
|
321
|
+
mintUrl,
|
|
224
322
|
"/v1/remote-web/pairing-challenge",
|
|
225
323
|
{ publicBaseUrl } satisfies RemoteWebPairingChallengeRequest,
|
|
226
324
|
);
|
|
227
|
-
|
|
325
|
+
const challenge: RemoteWebPairingChallengeResponse =
|
|
326
|
+
await response.json();
|
|
327
|
+
await gatewayPostOrExit(mintUrl, "/v1/remote-web/pairing-verification", {
|
|
328
|
+
userCode: challenge.userCode,
|
|
329
|
+
} satisfies RemoteWebPairingVerificationRequest);
|
|
330
|
+
return challenge;
|
|
228
331
|
}
|
|
229
332
|
|
|
230
333
|
/**
|
|
231
334
|
* Approve a pending pairing challenge by its user code, the local-presence
|
|
232
335
|
* proof for the device-code flow. Single owner of the pairing-verification
|
|
233
|
-
* route and request body:
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
336
|
+
* route and request body: `--web-approve` calls this directly to inspect
|
|
337
|
+
* rejections and print mismatch diagnostics (see
|
|
338
|
+
* {@link formatWebApproveFailure}); the link flow needs no separate
|
|
339
|
+
* verification call — the gateway-CLI mint approves on the spot.
|
|
237
340
|
*/
|
|
238
341
|
async function postPairingVerification(
|
|
239
342
|
gatewayUrl: string,
|
|
@@ -244,20 +347,6 @@ async function postPairingVerification(
|
|
|
244
347
|
} satisfies RemoteWebPairingVerificationRequest);
|
|
245
348
|
}
|
|
246
349
|
|
|
247
|
-
/**
|
|
248
|
-
* Approve the challenge this command just minted so one scan completes
|
|
249
|
-
* pairing, exiting with a generic message on non-2xx.
|
|
250
|
-
*/
|
|
251
|
-
async function approveRemoteWebPairing(
|
|
252
|
-
gatewayUrl: string,
|
|
253
|
-
userCode: string,
|
|
254
|
-
): Promise<RemoteWebPairingVerificationResponse> {
|
|
255
|
-
const response = await exitOnHttpError(
|
|
256
|
-
await postPairingVerification(gatewayUrl, userCode),
|
|
257
|
-
);
|
|
258
|
-
return (await response.json()) as RemoteWebPairingVerificationResponse;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
350
|
export const pair = Object.assign(
|
|
262
351
|
async function pair(): Promise<void> {
|
|
263
352
|
const rawArgs = process.argv.slice(3);
|
|
@@ -355,6 +444,24 @@ export const pair = Object.assign(
|
|
|
355
444
|
}
|
|
356
445
|
}
|
|
357
446
|
|
|
447
|
+
// Minting requires host presence on this machine. Native and docker
|
|
448
|
+
// topologies qualify (the latter via docker exec); an assistant this
|
|
449
|
+
// machine only knows through `connect import` has no host presence here
|
|
450
|
+
// at all, and `cadet pair` never worked against it (its loopback gate
|
|
451
|
+
// denied remote mints). Refuse clearly instead of a confusing mint
|
|
452
|
+
// failure.
|
|
453
|
+
if (entry.cloud === "paired") {
|
|
454
|
+
console.error(
|
|
455
|
+
`Error: ${formatAssistantReference(entry)} was paired as a remote ` +
|
|
456
|
+
"assistant (cadet connect import), so this machine does not host it.",
|
|
457
|
+
);
|
|
458
|
+
console.error(
|
|
459
|
+
"Run `cadet pair` on the machine hosting the assistant, or approve " +
|
|
460
|
+
"a code it shows with `cadet pair --web-approve <code>`.",
|
|
461
|
+
);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
|
|
358
465
|
// Mint over loopback (localUrl avoids mDNS for same-machine calls), but
|
|
359
466
|
// advertise a REACHABLE url in the link, since the loopback url would point
|
|
360
467
|
// the other device at its own localhost. Prefer an explicit --url, then the
|
|
@@ -437,14 +544,10 @@ export const pair = Object.assign(
|
|
|
437
544
|
}
|
|
438
545
|
const publicBaseUrl = resolved.url;
|
|
439
546
|
|
|
440
|
-
// Mint a challenge
|
|
441
|
-
// IS the local-presence proof, so the other
|
|
442
|
-
// step from the link alone.
|
|
443
|
-
const challenge = await
|
|
444
|
-
mintUrl,
|
|
445
|
-
publicBaseUrl,
|
|
446
|
-
);
|
|
447
|
-
await approveRemoteWebPairing(mintUrl, challenge.userCode);
|
|
547
|
+
// Mint a challenge (approved on the spot by the gateway-CLI invocation):
|
|
548
|
+
// running this CLI on the host IS the local-presence proof, so the other
|
|
549
|
+
// device completes pairing in one step from the link alone.
|
|
550
|
+
const challenge = await mintPairingChallengeOrExit(entry, publicBaseUrl);
|
|
448
551
|
const pairUrl = buildRemoteWebPairingUrl(challenge);
|
|
449
552
|
const displayName = assistantDisplayName(entry);
|
|
450
553
|
const appUrl = appVariant
|
|
@@ -483,6 +586,10 @@ export const pair = Object.assign(
|
|
|
483
586
|
console.log(qr);
|
|
484
587
|
});
|
|
485
588
|
console.log("");
|
|
589
|
+
console.log("Or enter this code on the pairing page:");
|
|
590
|
+
console.log("");
|
|
591
|
+
console.log(` ${challenge.deviceCode}`);
|
|
592
|
+
console.log("");
|
|
486
593
|
if (appUrl) {
|
|
487
594
|
console.log("The QR opens the Cadet app. App link:");
|
|
488
595
|
console.log("");
|
|
@@ -495,7 +602,9 @@ export const pair = Object.assign(
|
|
|
495
602
|
console.log("");
|
|
496
603
|
console.log(` ${pairUrl}`);
|
|
497
604
|
console.log("");
|
|
498
|
-
console.log(
|
|
605
|
+
console.log("Or on another computer, run:");
|
|
606
|
+
console.log("");
|
|
607
|
+
console.log(` cadet connect import "${pairUrl}"`);
|
|
499
608
|
console.log("");
|
|
500
609
|
console.log(`Expires: ${challenge.expiresAt}`);
|
|
501
610
|
},
|
package/src/commands/ps.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getNginxIngressPort } from "../lib/nginx-ingress.js";
|
|
1
2
|
import { isCloudHome } from "../lib/cloud-values.js";
|
|
2
3
|
import { join } from "path";
|
|
3
4
|
|
|
@@ -649,10 +650,10 @@ async function getRootManagedRows(): Promise<RootManagedRow[]> {
|
|
|
649
650
|
rows.push({
|
|
650
651
|
assistantId: app.assistantId,
|
|
651
652
|
row: {
|
|
652
|
-
name: "nginx
|
|
653
|
+
name: "nginx",
|
|
653
654
|
type: APP_TYPE,
|
|
654
655
|
status: withStatusEmoji(alive ? "running" : "not running"),
|
|
655
|
-
info:
|
|
656
|
+
info: `port ${app.port ?? getNginxIngressPort()}, PID ${app.pid}${alive ? "" : " (exited)"}`,
|
|
656
657
|
},
|
|
657
658
|
});
|
|
658
659
|
}
|
|
@@ -660,35 +661,23 @@ async function getRootManagedRows(): Promise<RootManagedRow[]> {
|
|
|
660
661
|
}
|
|
661
662
|
|
|
662
663
|
/**
|
|
663
|
-
* Fold the CLI-managed process rows into ONE table with the assistant rows
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
664
|
+
* Fold the CLI-managed process rows into ONE table with the assistant rows.
|
|
665
|
+
* Tunnel agents and the nginx edge are root resources of the machine, not
|
|
666
|
+
* children of whichever assistant they front — especially as the web bundle
|
|
667
|
+
* splits into /app and /ui — so every CLI-managed row renders machine-level.
|
|
667
668
|
*/
|
|
668
669
|
async function assistantRowsWithRootProcesses(
|
|
669
670
|
assistants: AssistantEntry[],
|
|
670
671
|
healths: AssistantHealth[],
|
|
671
672
|
activeAssistantId: string | null,
|
|
672
673
|
): Promise<TableRow[]> {
|
|
673
|
-
const
|
|
674
|
-
const unattributed: TableRow[] = [];
|
|
675
|
-
const knownIds = new Set(assistants.map((entry) => entry.assistantId));
|
|
676
|
-
for (const managed of await getRootManagedRows()) {
|
|
677
|
-
if (managed.assistantId && knownIds.has(managed.assistantId)) {
|
|
678
|
-
const nested = nestedByAssistant.get(managed.assistantId) ?? [];
|
|
679
|
-
nested.push({ ...managed.row, name: ` └ ${managed.row.name}` });
|
|
680
|
-
nestedByAssistant.set(managed.assistantId, nested);
|
|
681
|
-
} else {
|
|
682
|
-
unattributed.push(managed.row);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
674
|
+
const rootRows = (await getRootManagedRows()).map((managed) => managed.row);
|
|
685
675
|
|
|
686
676
|
const rows: TableRow[] = [];
|
|
687
677
|
assistants.forEach((entry, i) => {
|
|
688
678
|
rows.push(formatAssistantListRow(entry, activeAssistantId, healths[i]!));
|
|
689
|
-
rows.push(...(nestedByAssistant.get(entry.assistantId) ?? []));
|
|
690
679
|
});
|
|
691
|
-
rows.push(...
|
|
680
|
+
rows.push(...rootRows);
|
|
692
681
|
return rows;
|
|
693
682
|
}
|
|
694
683
|
|
|
@@ -825,6 +814,7 @@ export async function listAllAssistants(verbose: boolean): Promise<void> {
|
|
|
825
814
|
),
|
|
826
815
|
{ name: "NAME", type: "TYPE", status: "STATUS", info: "INFO" },
|
|
827
816
|
);
|
|
817
|
+
console.log("");
|
|
828
818
|
}
|
|
829
819
|
|
|
830
820
|
// ── Entry point ─────────────────────────────────────────────────
|
package/src/commands/teleport.ts
CHANGED
|
@@ -185,7 +185,6 @@ function printHelp(): void {
|
|
|
185
185
|
|
|
186
186
|
export function parseArgs(argv: string[]): {
|
|
187
187
|
from: string | undefined;
|
|
188
|
-
to: string | undefined;
|
|
189
188
|
targetEnv: "local" | "docker" | "platform" | undefined;
|
|
190
189
|
targetName: string | undefined;
|
|
191
190
|
keepSource: boolean;
|
|
@@ -193,7 +192,6 @@ export function parseArgs(argv: string[]): {
|
|
|
193
192
|
help: boolean;
|
|
194
193
|
} {
|
|
195
194
|
let from: string | undefined;
|
|
196
|
-
let to: string | undefined;
|
|
197
195
|
let targetEnv: "local" | "docker" | "platform" | undefined;
|
|
198
196
|
let targetName: string | undefined;
|
|
199
197
|
let keepSource = false;
|
|
@@ -209,11 +207,6 @@ export function parseArgs(argv: string[]): {
|
|
|
209
207
|
continue;
|
|
210
208
|
}
|
|
211
209
|
from = argv[++i];
|
|
212
|
-
} else if (arg === "--to" && i + 1 < argv.length) {
|
|
213
|
-
if (argv[i + 1].startsWith("--")) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
to = argv[++i];
|
|
217
210
|
} else if (
|
|
218
211
|
arg === "--local" ||
|
|
219
212
|
arg === "--docker" ||
|
|
@@ -242,7 +235,7 @@ export function parseArgs(argv: string[]): {
|
|
|
242
235
|
process.exit(1);
|
|
243
236
|
}
|
|
244
237
|
|
|
245
|
-
return { from,
|
|
238
|
+
return { from, targetEnv, targetName, keepSource, dryRun, help };
|
|
246
239
|
}
|
|
247
240
|
|
|
248
241
|
// ---------------------------------------------------------------------------
|
|
@@ -1253,7 +1246,6 @@ async function tryInjectCloudCredentials(
|
|
|
1253
1246
|
bearerToken: entry.bearerToken,
|
|
1254
1247
|
assistantApiKey,
|
|
1255
1248
|
cloudAssistantId: registration.assistant.id,
|
|
1256
|
-
cloudBaseUrl: cloudUrl,
|
|
1257
1249
|
organizationId: orgId,
|
|
1258
1250
|
userId: user.id,
|
|
1259
1251
|
webhookSecret: registration.webhook_secret,
|
|
@@ -1277,7 +1269,7 @@ async function tryInjectCloudCredentials(
|
|
|
1277
1269
|
export const teleport = Object.assign(
|
|
1278
1270
|
async function teleport(): Promise<void> {
|
|
1279
1271
|
const args = process.argv.slice(3);
|
|
1280
|
-
const { from,
|
|
1272
|
+
const { from, targetEnv, targetName, keepSource, dryRun, help } =
|
|
1281
1273
|
parseArgs(args);
|
|
1282
1274
|
|
|
1283
1275
|
if (help) {
|
|
@@ -1285,17 +1277,6 @@ export const teleport = Object.assign(
|
|
|
1285
1277
|
process.exit(0);
|
|
1286
1278
|
}
|
|
1287
1279
|
|
|
1288
|
-
// Legacy --to flag deprecation
|
|
1289
|
-
if (to) {
|
|
1290
|
-
console.error("Error: --to is deprecated. Use environment flags instead:");
|
|
1291
|
-
console.error(
|
|
1292
|
-
" cadet teleport --from <source> --local|--docker|--platform [name]",
|
|
1293
|
-
);
|
|
1294
|
-
console.error("");
|
|
1295
|
-
console.error("Run 'cadet teleport --help' for details.");
|
|
1296
|
-
process.exit(1);
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
1280
|
if (!from) {
|
|
1300
1281
|
printHelp();
|
|
1301
1282
|
process.exit(1);
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import {
|
|
11
11
|
formatAssistantReference,
|
|
12
12
|
loadAllAssistants,
|
|
13
|
+
readRootProcesses,
|
|
13
14
|
resolveTargetAssistant,
|
|
14
15
|
type AssistantEntry,
|
|
15
16
|
} from "../lib/assistant-config";
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
hasWebhookIntegrationsConfigured,
|
|
38
39
|
providerSupportsReservedDomain,
|
|
39
40
|
runProviderTunnel,
|
|
41
|
+
stopTunnelProcess,
|
|
42
|
+
summarizeStartupFailure,
|
|
40
43
|
} from "../lib/tunnel.js";
|
|
41
44
|
|
|
42
45
|
// The accepted `--provider` values ARE the shared registry, so validation
|
|
@@ -47,7 +50,7 @@ interface TunnelArgs {
|
|
|
47
50
|
provider: TunnelProviderName;
|
|
48
51
|
domain: string | null;
|
|
49
52
|
clearDomain: boolean;
|
|
50
|
-
|
|
53
|
+
foreground: boolean;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
|
|
@@ -57,13 +60,13 @@ function parseArgs(): TunnelArgs {
|
|
|
57
60
|
let provider: TunnelProviderName | null = null;
|
|
58
61
|
let domain: string | null = null;
|
|
59
62
|
let clearDomain = false;
|
|
60
|
-
let
|
|
63
|
+
let foreground = false;
|
|
61
64
|
|
|
62
65
|
for (let i = 0; i < args.length; i++) {
|
|
63
66
|
const arg = args[i];
|
|
64
67
|
if (arg === "--help" || arg === "-h") {
|
|
65
68
|
console.log(
|
|
66
|
-
"Usage: cadet tunnel [<name>] --provider <provider> [options]",
|
|
69
|
+
"Usage: cadet tunnel [<name>] --provider <provider> [options]\n cadet tunnel stop [<name>]",
|
|
67
70
|
);
|
|
68
71
|
console.log("");
|
|
69
72
|
console.log("Front a locally running assistant with an HTTPS tunnel.");
|
|
@@ -112,19 +115,22 @@ function parseArgs(): TunnelArgs {
|
|
|
112
115
|
" tunnel without one. Cannot be combined with --domain.",
|
|
113
116
|
);
|
|
114
117
|
console.log(
|
|
115
|
-
`
|
|
118
|
+
` --foreground Stream the tunnel's logs to this terminal. By default the tunnel`,
|
|
116
119
|
);
|
|
117
120
|
console.log(
|
|
118
|
-
"
|
|
121
|
+
" runs in the background: the command returns after printing the public",
|
|
119
122
|
);
|
|
120
123
|
console.log(
|
|
121
|
-
|
|
124
|
+
` URL, and logs stream to tunnel.log in the CLI log directory. A background`,
|
|
122
125
|
);
|
|
123
126
|
console.log(
|
|
124
|
-
" adopted an already-running
|
|
127
|
+
" tunnel stops with `cadet tunnel stop`, except when it adopted an already-running",
|
|
125
128
|
);
|
|
126
129
|
console.log(
|
|
127
|
-
" command): killing the supervisor then
|
|
130
|
+
" tunnel (started outside this command): killing the supervisor then",
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
" leaves that tunnel running.",
|
|
128
134
|
);
|
|
129
135
|
console.log("");
|
|
130
136
|
console.log("Providers:");
|
|
@@ -151,9 +157,6 @@ function parseArgs(): TunnelArgs {
|
|
|
151
157
|
}
|
|
152
158
|
console.log(" $ cadet tunnel --provider cloudflare");
|
|
153
159
|
console.log(" $ cadet tunnel my-assistant --provider tailscale");
|
|
154
|
-
console.log(
|
|
155
|
-
` $ cadet tunnel --provider ${DEFAULT_TUNNEL_PROVIDER} --detach`,
|
|
156
|
-
);
|
|
157
160
|
process.exit(0);
|
|
158
161
|
} else if (arg === "--provider") {
|
|
159
162
|
const next = args[i + 1];
|
|
@@ -188,8 +191,8 @@ function parseArgs(): TunnelArgs {
|
|
|
188
191
|
i++;
|
|
189
192
|
} else if (arg === "--clear-domain") {
|
|
190
193
|
clearDomain = true;
|
|
191
|
-
} else if (arg === "
|
|
192
|
-
|
|
194
|
+
} else if (arg === "--foreground") {
|
|
195
|
+
foreground = true;
|
|
193
196
|
} else if (arg.startsWith("-")) {
|
|
194
197
|
console.error(`Error: Unknown option '${arg}'.`);
|
|
195
198
|
process.exit(1);
|
|
@@ -227,7 +230,7 @@ function parseArgs(): TunnelArgs {
|
|
|
227
230
|
process.exit(1);
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
return { assistantName, provider, domain, clearDomain,
|
|
233
|
+
return { assistantName, provider, domain, clearDomain, foreground };
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
/** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
|
|
@@ -345,31 +348,31 @@ const TUNNEL_BACKGROUND_START_TIMEOUT_MS = 35_000;
|
|
|
345
348
|
const TUNNEL_ESTABLISHED_RE = /Tunnel established: (\S+)/;
|
|
346
349
|
|
|
347
350
|
/**
|
|
348
|
-
* Re-invoke `cadet tunnel
|
|
349
|
-
*
|
|
350
|
-
* "Tunnel established" or adopted-tunnel line (readiness) or
|
|
351
|
-
*
|
|
352
|
-
*
|
|
351
|
+
* Re-invoke `cadet tunnel --foreground` as a detached background process via
|
|
352
|
+
* `relaunchDetached`, waiting for the child to either print its
|
|
353
|
+
* "Tunnel established" or adopted-tunnel line (readiness) or exit early
|
|
354
|
+
* (failure), so a misconfigured provider still fails loudly in the terminal
|
|
355
|
+
* that ran `cadet tunnel`.
|
|
353
356
|
*
|
|
354
|
-
* The log
|
|
355
|
-
*
|
|
356
|
-
* truncate and read each other's readiness output.
|
|
357
|
+
* The log file is the fixed `tunnel.log` so there is one stable path to tail;
|
|
358
|
+
* concurrent tunnel runs share (and truncate) it.
|
|
357
359
|
*/
|
|
358
360
|
async function spawnDetachedTunnel(): Promise<void> {
|
|
359
361
|
const childArgs: string[] = ["tunnel"];
|
|
360
362
|
for (const arg of process.argv.slice(3)) {
|
|
361
|
-
if (arg === "
|
|
363
|
+
if (arg === "--foreground") {
|
|
362
364
|
continue;
|
|
363
365
|
}
|
|
364
366
|
childArgs.push(arg);
|
|
365
367
|
}
|
|
368
|
+
childArgs.push("--foreground");
|
|
366
369
|
|
|
367
370
|
let publicUrl: string | undefined;
|
|
368
371
|
let adopted = false;
|
|
369
372
|
|
|
370
373
|
const { child, logPath, ready, exitCode } = await relaunchDetached({
|
|
371
374
|
args: childArgs,
|
|
372
|
-
logFile:
|
|
375
|
+
logFile: "tunnel.log",
|
|
373
376
|
timeoutMs: TUNNEL_BACKGROUND_START_TIMEOUT_MS,
|
|
374
377
|
isReady: (logPath) => {
|
|
375
378
|
let logText = "";
|
|
@@ -394,9 +397,17 @@ async function spawnDetachedTunnel(): Promise<void> {
|
|
|
394
397
|
});
|
|
395
398
|
|
|
396
399
|
if (exitCode !== undefined) {
|
|
400
|
+
let logText = "";
|
|
401
|
+
try {
|
|
402
|
+
logText = readFileSync(logPath, "utf-8");
|
|
403
|
+
} catch {
|
|
404
|
+
// Log file not readable — the summary falls back to none.
|
|
405
|
+
}
|
|
406
|
+
const reason = summarizeStartupFailure(logText);
|
|
397
407
|
console.error(
|
|
398
|
-
`Error: tunnel process exited during startup${
|
|
408
|
+
`Error (${exitCode ?? "unknown"}): tunnel process exited during startup${reason ? `: ${reason}` : "."}`,
|
|
399
409
|
);
|
|
410
|
+
console.error(`Logs: ${logPath}`);
|
|
400
411
|
process.exit(1);
|
|
401
412
|
}
|
|
402
413
|
|
|
@@ -411,16 +422,44 @@ async function spawnDetachedTunnel(): Promise<void> {
|
|
|
411
422
|
if (adopted) {
|
|
412
423
|
console.log(adoptedTunnelStopNotice(child.pid));
|
|
413
424
|
} else {
|
|
414
|
-
console.log(
|
|
425
|
+
console.log("Stop with: cadet tunnel stop");
|
|
415
426
|
}
|
|
416
427
|
}
|
|
417
428
|
|
|
418
429
|
export const tunnel = Object.assign(
|
|
419
430
|
async function tunnel(): Promise<void> {
|
|
420
|
-
|
|
431
|
+
if (process.argv[3] === "stop") {
|
|
432
|
+
const args = process.argv.slice(4);
|
|
433
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
434
|
+
console.log("Usage: cadet tunnel stop [<name>]");
|
|
435
|
+
console.log(
|
|
436
|
+
"Stop the recorded Cadet-managed tunnel, keeping nginx and the saved domain.",
|
|
437
|
+
);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (args.some((arg) => arg.startsWith("-")))
|
|
441
|
+
throw new Error("Usage: cadet tunnel stop [<name>]");
|
|
442
|
+
const { entry, workspaceDir } = resolveLocalTunnelTarget(
|
|
443
|
+
args.join(" ") || null,
|
|
444
|
+
);
|
|
445
|
+
if (!readRootProcesses().tunnel) {
|
|
446
|
+
console.log(
|
|
447
|
+
"No Cadet-managed tunnel is recorded. Externally started tunnels must be stopped directly.",
|
|
448
|
+
);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const stopped = await stopTunnelProcess(entry.assistantId, workspaceDir);
|
|
452
|
+
console.log(
|
|
453
|
+
stopped
|
|
454
|
+
? "Tunnel stopped. nginx and the saved domain are unchanged."
|
|
455
|
+
: "No running tunnel found; cleared stale tunnel state.",
|
|
456
|
+
);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const { assistantName, provider, domain, clearDomain, foreground } =
|
|
421
460
|
parseArgs();
|
|
422
461
|
|
|
423
|
-
if (
|
|
462
|
+
if (!foreground) {
|
|
424
463
|
await spawnDetachedTunnel();
|
|
425
464
|
return;
|
|
426
465
|
}
|