@vellumai/cli 0.11.2 → 0.11.3-staging.2
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/knip.json +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/ipc-framing.ts +295 -0
- package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +444 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +236 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/index.ts +17 -6
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
- package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/LICENSE +20 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/README.md +38 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/bin/nanoid.js +55 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.browser.js +29 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.d.ts +106 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.js +47 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/nanoid.js +1 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.d.ts +48 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.js +21 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/package.json +46 -0
- package/node_modules/@vellumai/local-mode/node_modules/nanoid/url-alphabet/index.js +2 -0
- package/node_modules/@vellumai/local-mode/package.json +1 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +63 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +503 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token.test.ts +203 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +46 -1
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +6 -4
- package/node_modules/@vellumai/local-mode/src/__tests__/pair.test.ts +621 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +5 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +232 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +126 -20
- package/node_modules/@vellumai/local-mode/src/environment.ts +36 -15
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +313 -25
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +222 -13
- package/node_modules/@vellumai/local-mode/src/index.ts +35 -3
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +18 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +72 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +130 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +134 -45
- package/node_modules/@vellumai/local-mode/src/pair.ts +354 -0
- package/node_modules/@vellumai/local-mode/src/paths.ts +49 -0
- package/node_modules/@vellumai/local-mode/src/status.ts +10 -8
- package/node_modules/@vellumai/local-mode/src/unpair.ts +101 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +34 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/package.json +3 -1
- package/src/__tests__/client-token.test.ts +26 -1
- package/src/__tests__/guardian-token.test.ts +4 -0
- package/src/__tests__/nginx-ingress-command.test.ts +176 -16
- package/src/__tests__/nginx-ingress.test.ts +1821 -101
- package/src/__tests__/pair.test.ts +104 -2
- package/src/__tests__/sleep.test.ts +7 -3
- package/src/__tests__/tailscale-tunnel.test.ts +0 -1
- package/src/__tests__/tunnel.test.ts +1099 -55
- package/src/__tests__/wake.test.ts +166 -99
- package/src/commands/client.ts +182 -27
- package/src/commands/connect/import.ts +35 -156
- package/src/commands/nginx-ingress.ts +87 -110
- package/src/commands/pair.ts +84 -21
- package/src/commands/tunnel.ts +196 -85
- package/src/commands/upgrade.ts +3 -2
- package/src/commands/wake.ts +5 -163
- package/src/index.ts +1 -1
- package/src/lib/__tests__/local-ces.test.ts +10 -1
- package/src/lib/cloudflare-tunnel.ts +3 -16
- package/src/lib/environments/__tests__/paths.test.ts +20 -1
- package/src/lib/environments/paths.ts +29 -41
- package/src/lib/environments/resolve.ts +7 -5
- package/src/lib/guardian-token.ts +22 -62
- package/src/lib/ingress-config.ts +25 -0
- package/src/lib/local.ts +77 -20
- package/src/lib/nginx-ingress.ts +523 -76
- package/src/lib/ngrok.ts +154 -61
- package/src/lib/orphan-detection.test.ts +167 -0
- package/src/lib/orphan-detection.ts +40 -0
- package/src/lib/pair.test.ts +87 -0
- package/src/lib/pair.ts +55 -0
- package/src/lib/platform-client.ts +8 -5
- package/src/lib/tailscale-tunnel.ts +7 -18
- package/src/lib/tunnel-edge.ts +114 -0
- package/src/lib/xdg-log.ts +2 -2
package/src/commands/pair.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { extractFlag } from "../lib/arg-utils.js";
|
|
|
31
31
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
32
32
|
import {
|
|
33
33
|
formatAssistantLookupError,
|
|
34
|
+
formatAssistantReference,
|
|
34
35
|
lookupAssistantByIdentifier,
|
|
35
36
|
resolveAssistant,
|
|
36
37
|
type AssistantEntry,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
getClientRegistrationHeaders,
|
|
41
42
|
} from "../lib/client-identity.js";
|
|
42
43
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
44
|
+
import { getCurrentEnvironment } from "../lib/environments/resolve.js";
|
|
43
45
|
import {
|
|
44
46
|
formatFeatureFlagGateMessage,
|
|
45
47
|
isAssistantFeatureFlagEnabled,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
} from "../lib/feature-flags.js";
|
|
48
50
|
import { getLocalLanIPv4 } from "../lib/local.js";
|
|
49
51
|
import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
52
|
+
import { formatWebApproveFailure, parseGatewayErrorCode } from "../lib/pair.js";
|
|
50
53
|
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
51
54
|
|
|
52
55
|
function assistantDisplayName(entry: AssistantEntry): string {
|
|
@@ -89,7 +92,8 @@ ARGUMENTS:
|
|
|
89
92
|
OPTIONS:
|
|
90
93
|
--url <url> Reachable gateway URL to advertise in the bundle
|
|
91
94
|
(default: the assistant's runtime URL, not loopback)
|
|
92
|
-
--label <name> Human label for this pairing (echoed in the output
|
|
95
|
+
--label <name> Human label for this pairing (echoed in the output; with
|
|
96
|
+
--qr --app it also names the assistant in the connect link)
|
|
93
97
|
--web Create a browser pairing URL for remote web access
|
|
94
98
|
--web-approve <code>
|
|
95
99
|
Approve a browser pairing code shown by /assistant/pair
|
|
@@ -100,8 +104,10 @@ OPTIONS:
|
|
|
100
104
|
loopback or non-https URLs.
|
|
101
105
|
--app With --qr: encode the QR as a vellum-assistant://connect
|
|
102
106
|
link that opens the Vellum iOS app directly (the plain
|
|
103
|
-
https pairing URL is printed as a fallback).
|
|
104
|
-
|
|
107
|
+
https pairing URL is printed as a fallback). The link
|
|
108
|
+
carries the assistant's name (--label overrides it) so the
|
|
109
|
+
app can label the pairing. Requires an app build with the
|
|
110
|
+
connect handler installed on the phone.
|
|
105
111
|
--app-scheme <scheme>
|
|
106
112
|
URL scheme for --app links (default: vellum-assistant;
|
|
107
113
|
dev/staging app builds use vellum-assistant-dev /
|
|
@@ -138,33 +144,40 @@ const DEFAULT_APP_CONNECT_SCHEME = "vellum-assistant";
|
|
|
138
144
|
|
|
139
145
|
/**
|
|
140
146
|
* Compose the custom-scheme link the iOS app's connect handler accepts:
|
|
141
|
-
* `<scheme>://connect?url=<base>&code=<device code
|
|
142
|
-
* base as its self-hosted server
|
|
147
|
+
* `<scheme>://connect?url=<base>&code=<device code>[&name=<label>]`. The app
|
|
148
|
+
* persists the base (and the label, when present) as its self-hosted server
|
|
149
|
+
* and opens the pair page with the code.
|
|
143
150
|
*/
|
|
144
151
|
export function buildAppConnectUrl(
|
|
145
152
|
scheme: string,
|
|
146
153
|
baseUrl: string,
|
|
147
154
|
deviceCode: string,
|
|
155
|
+
name?: string,
|
|
148
156
|
): string {
|
|
149
157
|
const params = new URLSearchParams({ url: baseUrl, code: deviceCode });
|
|
150
|
-
|
|
158
|
+
if (name) {
|
|
159
|
+
params.set("name", name);
|
|
160
|
+
}
|
|
161
|
+
// Percent-encode spaces: URLSearchParams form-encodes them as `+`, which
|
|
162
|
+
// the app's Foundation URLComponents parser keeps as a literal plus.
|
|
163
|
+
const query = params.toString().replace(/\+/g, "%20");
|
|
164
|
+
return `${scheme}://connect?${query}`;
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
168
|
* POST a JSON body to a loopback gateway route, exiting with a clear message
|
|
155
|
-
* when the gateway is unreachable
|
|
156
|
-
*
|
|
157
|
-
*
|
|
169
|
+
* when the gateway is unreachable. Non-2xx responses are returned to the
|
|
170
|
+
* caller; use {@link gatewayPostOrExit} unless the call site prints its own
|
|
171
|
+
* HTTP-error diagnostics.
|
|
158
172
|
*/
|
|
159
|
-
async function
|
|
173
|
+
async function gatewayPost(
|
|
160
174
|
gatewayUrl: string,
|
|
161
175
|
path: string,
|
|
162
176
|
body: unknown,
|
|
163
177
|
headers?: Record<string, string>,
|
|
164
178
|
): Promise<Response> {
|
|
165
|
-
let response: Response;
|
|
166
179
|
try {
|
|
167
|
-
|
|
180
|
+
return await loopbackSafeFetch(`${gatewayUrl}${path}`, {
|
|
168
181
|
method: "POST",
|
|
169
182
|
headers: { "Content-Type": "application/json", ...headers },
|
|
170
183
|
body: JSON.stringify(body),
|
|
@@ -177,7 +190,24 @@ async function gatewayPostOrExit(
|
|
|
177
190
|
console.error("Is the assistant running? Try `vellum wake`.");
|
|
178
191
|
process.exit(1);
|
|
179
192
|
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* {@link gatewayPost}, but also exiting with a generic message on non-2xx.
|
|
197
|
+
* Every pairing subcommand talks to the gateway this way, so the reachability
|
|
198
|
+
* + HTTP-error handling has a single home.
|
|
199
|
+
*/
|
|
200
|
+
async function gatewayPostOrExit(
|
|
201
|
+
gatewayUrl: string,
|
|
202
|
+
path: string,
|
|
203
|
+
body: unknown,
|
|
204
|
+
headers?: Record<string, string>,
|
|
205
|
+
): Promise<Response> {
|
|
206
|
+
return exitOnHttpError(await gatewayPost(gatewayUrl, path, body, headers));
|
|
207
|
+
}
|
|
180
208
|
|
|
209
|
+
/** Exit with a generic HTTP-error message on non-2xx; pass 2xx through. */
|
|
210
|
+
async function exitOnHttpError(response: Response): Promise<Response> {
|
|
181
211
|
if (!response.ok) {
|
|
182
212
|
const errorBody = await response.text().catch(() => "");
|
|
183
213
|
console.error(
|
|
@@ -185,7 +215,6 @@ async function gatewayPostOrExit(
|
|
|
185
215
|
);
|
|
186
216
|
process.exit(1);
|
|
187
217
|
}
|
|
188
|
-
|
|
189
218
|
return response;
|
|
190
219
|
}
|
|
191
220
|
|
|
@@ -206,18 +235,32 @@ async function createRemoteWebPairingChallenge(
|
|
|
206
235
|
}
|
|
207
236
|
|
|
208
237
|
/**
|
|
209
|
-
* Approve a pending pairing challenge by its user code
|
|
210
|
-
* proof for the device-code flow.
|
|
211
|
-
*
|
|
238
|
+
* Approve a pending pairing challenge by its user code, the local-presence
|
|
239
|
+
* proof for the device-code flow. Single owner of the pairing-verification
|
|
240
|
+
* route and request body: `--qr` approves via {@link approveRemoteWebPairing}
|
|
241
|
+
* (generic exit on non-2xx), while `--web-approve` calls this directly to
|
|
242
|
+
* inspect rejections and print mismatch diagnostics (see
|
|
243
|
+
* {@link formatWebApproveFailure}).
|
|
244
|
+
*/
|
|
245
|
+
async function postPairingVerification(
|
|
246
|
+
gatewayUrl: string,
|
|
247
|
+
userCode: string,
|
|
248
|
+
): Promise<Response> {
|
|
249
|
+
return gatewayPost(gatewayUrl, "/v1/remote-web/pairing-verification", {
|
|
250
|
+
userCode,
|
|
251
|
+
} satisfies RemoteWebPairingVerificationRequest);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Approve the challenge `--qr` just minted so one scan completes pairing,
|
|
256
|
+
* exiting with a generic message on non-2xx.
|
|
212
257
|
*/
|
|
213
258
|
async function approveRemoteWebPairing(
|
|
214
259
|
gatewayUrl: string,
|
|
215
260
|
userCode: string,
|
|
216
261
|
): Promise<RemoteWebPairingVerificationResponse> {
|
|
217
|
-
const response = await
|
|
218
|
-
gatewayUrl,
|
|
219
|
-
"/v1/remote-web/pairing-verification",
|
|
220
|
-
{ userCode } satisfies RemoteWebPairingVerificationRequest,
|
|
262
|
+
const response = await exitOnHttpError(
|
|
263
|
+
await postPairingVerification(gatewayUrl, userCode),
|
|
221
264
|
);
|
|
222
265
|
return (await response.json()) as RemoteWebPairingVerificationResponse;
|
|
223
266
|
}
|
|
@@ -399,7 +442,26 @@ export async function pair(): Promise<void> {
|
|
|
399
442
|
if (webApproveCode) {
|
|
400
443
|
await assertWebRemoteIngressEnabled(entry.assistantId, mintUrl);
|
|
401
444
|
|
|
402
|
-
|
|
445
|
+
// Rejections are diagnosed here rather than by exitOnHttpError: a
|
|
446
|
+
// rejected code must name the gateway that was asked, or an
|
|
447
|
+
// assistant/environment mismatch is indistinguishable from a typo.
|
|
448
|
+
const response = await postPairingVerification(mintUrl, webApproveCode);
|
|
449
|
+
if (!response.ok) {
|
|
450
|
+
const errorBody = await response.text().catch(() => "");
|
|
451
|
+
const diagnostic = formatWebApproveFailure(
|
|
452
|
+
mintUrl,
|
|
453
|
+
formatAssistantReference(entry),
|
|
454
|
+
getCurrentEnvironment().name,
|
|
455
|
+
parseGatewayErrorCode(errorBody),
|
|
456
|
+
);
|
|
457
|
+
console.error(
|
|
458
|
+
diagnostic ??
|
|
459
|
+
`Error: HTTP ${response.status}: ${errorBody || response.statusText}`,
|
|
460
|
+
);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
const result =
|
|
464
|
+
(await response.json()) as RemoteWebPairingVerificationResponse;
|
|
403
465
|
if (jsonOutput) {
|
|
404
466
|
console.log(JSON.stringify(result, null, 2));
|
|
405
467
|
return;
|
|
@@ -502,6 +564,7 @@ export async function pair(): Promise<void> {
|
|
|
502
564
|
appSchemeOverride ?? DEFAULT_APP_CONNECT_SCHEME,
|
|
503
565
|
qrBaseUrl,
|
|
504
566
|
challenge.deviceCode,
|
|
567
|
+
label || assistantDisplayName(entry),
|
|
505
568
|
)
|
|
506
569
|
: null;
|
|
507
570
|
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatAssistantReference,
|
|
5
|
+
loadAllAssistants,
|
|
6
|
+
resolveTargetAssistant,
|
|
7
|
+
type AssistantEntry,
|
|
8
|
+
} from "../lib/assistant-config";
|
|
9
|
+
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
4
10
|
import { runCloudflareTunnel } from "../lib/cloudflare-tunnel.js";
|
|
5
|
-
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
6
11
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../lib/
|
|
12
|
+
getDefaultWorkspaceDir,
|
|
13
|
+
saveNgrokDomain,
|
|
14
|
+
} from "../lib/ingress-config.js";
|
|
15
|
+
import {
|
|
16
|
+
ensureTunnelEdge,
|
|
17
|
+
formatEdgeMode,
|
|
18
|
+
type TunnelEdge,
|
|
19
|
+
} from "../lib/nginx-ingress.js";
|
|
10
20
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
11
21
|
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
12
22
|
import { runTailscaleTunnel } from "../lib/tailscale-tunnel.js";
|
|
23
|
+
import { parseGatewayPortFromEntryUrls } from "./nginx-ingress.js";
|
|
13
24
|
|
|
14
25
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
15
26
|
type TunnelProvider = (typeof VALID_PROVIDERS)[number];
|
|
@@ -19,12 +30,17 @@ const DEFAULT_PROVIDER: TunnelProvider = "vellum";
|
|
|
19
30
|
interface TunnelArgs {
|
|
20
31
|
assistantName: string | null;
|
|
21
32
|
provider: TunnelProvider;
|
|
33
|
+
domain: string | null;
|
|
34
|
+
clearDomain: boolean;
|
|
22
35
|
}
|
|
23
36
|
|
|
37
|
+
const FLAGS_WITH_VALUES = ["--provider", "--domain"] as const;
|
|
38
|
+
|
|
24
39
|
function parseArgs(): TunnelArgs {
|
|
25
40
|
const args = process.argv.slice(3);
|
|
26
|
-
let assistantName: string | null = null;
|
|
27
41
|
let provider: TunnelProvider = DEFAULT_PROVIDER;
|
|
42
|
+
let domain: string | null = null;
|
|
43
|
+
let clearDomain = false;
|
|
28
44
|
|
|
29
45
|
for (let i = 0; i < args.length; i++) {
|
|
30
46
|
const arg = args[i];
|
|
@@ -41,6 +57,13 @@ function parseArgs(): TunnelArgs {
|
|
|
41
57
|
"enabling webhook integrations (Telegram, Twilio, etc.) to reach the assistant.",
|
|
42
58
|
);
|
|
43
59
|
console.log("");
|
|
60
|
+
console.log(
|
|
61
|
+
"The tunnel always fronts the local nginx edge, which is started automatically.",
|
|
62
|
+
);
|
|
63
|
+
console.log(
|
|
64
|
+
"nginx must be installed (macOS: brew install nginx, Linux: sudo apt install nginx).",
|
|
65
|
+
);
|
|
66
|
+
console.log("");
|
|
44
67
|
console.log("Arguments:");
|
|
45
68
|
console.log(
|
|
46
69
|
" <name> Name of the assistant (defaults to active or only local)",
|
|
@@ -50,6 +73,18 @@ function parseArgs(): TunnelArgs {
|
|
|
50
73
|
console.log(
|
|
51
74
|
` --provider <provider> Tunnel provider: ${VALID_PROVIDERS.join(", ")} (default: ${DEFAULT_PROVIDER})`,
|
|
52
75
|
);
|
|
76
|
+
console.log(
|
|
77
|
+
" --domain <domain> Reserved ngrok domain to bind (ngrok provider only).",
|
|
78
|
+
);
|
|
79
|
+
console.log(
|
|
80
|
+
" Saved to the workspace config so `vellum wake` restores reuse it.",
|
|
81
|
+
);
|
|
82
|
+
console.log(
|
|
83
|
+
" --clear-domain Clear the saved ngrok domain (ngrok provider only) and tunnel",
|
|
84
|
+
);
|
|
85
|
+
console.log(
|
|
86
|
+
" without one. Cannot be combined with --domain.",
|
|
87
|
+
);
|
|
53
88
|
console.log("");
|
|
54
89
|
console.log("Providers:");
|
|
55
90
|
console.log(
|
|
@@ -72,8 +107,11 @@ function parseArgs(): TunnelArgs {
|
|
|
72
107
|
);
|
|
73
108
|
console.log("");
|
|
74
109
|
console.log("Examples:");
|
|
75
|
-
console.log(" $ vellum tunnel");
|
|
76
110
|
console.log(" $ vellum tunnel --provider ngrok");
|
|
111
|
+
console.log(
|
|
112
|
+
" $ vellum tunnel --provider ngrok --domain my-assistant.ngrok.app",
|
|
113
|
+
);
|
|
114
|
+
console.log(" $ vellum tunnel --provider ngrok --clear-domain");
|
|
77
115
|
console.log(" $ vellum tunnel --provider cloudflare");
|
|
78
116
|
console.log(" $ vellum tunnel my-assistant --provider tailscale");
|
|
79
117
|
process.exit(0);
|
|
@@ -96,121 +134,194 @@ function parseArgs(): TunnelArgs {
|
|
|
96
134
|
}
|
|
97
135
|
provider = next as TunnelProvider;
|
|
98
136
|
i++;
|
|
137
|
+
} else if (arg === "--domain") {
|
|
138
|
+
const next = args[i + 1];
|
|
139
|
+
if (!next || next.startsWith("-")) {
|
|
140
|
+
console.error(
|
|
141
|
+
"Error: --domain requires a value, e.g. --domain my-assistant.ngrok.app",
|
|
142
|
+
);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
domain = next;
|
|
146
|
+
i++;
|
|
147
|
+
} else if (arg === "--clear-domain") {
|
|
148
|
+
clearDomain = true;
|
|
99
149
|
} else if (arg.startsWith("-")) {
|
|
100
150
|
console.error(`Error: Unknown option '${arg}'.`);
|
|
101
151
|
process.exit(1);
|
|
102
|
-
} else if (!assistantName) {
|
|
103
|
-
assistantName = arg;
|
|
104
|
-
} else {
|
|
105
|
-
console.error(`Error: Unexpected argument '${arg}'.`);
|
|
106
|
-
process.exit(1);
|
|
107
152
|
}
|
|
108
153
|
}
|
|
109
154
|
|
|
110
|
-
|
|
155
|
+
// Joins all positionals so unquoted multi-word display names resolve as one
|
|
156
|
+
// identifier (cli/AGENTS.md "Assistant targeting convention").
|
|
157
|
+
const assistantName =
|
|
158
|
+
parseAssistantTargetArg(args, FLAGS_WITH_VALUES) ?? null;
|
|
159
|
+
|
|
160
|
+
if (domain && provider !== "ngrok") {
|
|
161
|
+
console.error(
|
|
162
|
+
`Error: --domain is only supported with --provider ngrok (got '${provider}').`,
|
|
163
|
+
);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (clearDomain && provider !== "ngrok") {
|
|
168
|
+
console.error(
|
|
169
|
+
`Error: --clear-domain is only supported with --provider ngrok (got '${provider}').`,
|
|
170
|
+
);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (clearDomain && domain) {
|
|
175
|
+
console.error(
|
|
176
|
+
"Error: --clear-domain cannot be combined with --domain. Pass --domain alone to replace the saved domain.",
|
|
177
|
+
);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { assistantName, provider, domain, clearDomain };
|
|
111
182
|
}
|
|
112
183
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
/** A tunnelable assistant plus the gateway port and workspace the edge fronts. */
|
|
185
|
+
interface LocalTunnelTarget {
|
|
186
|
+
entry: AssistantEntry;
|
|
187
|
+
gatewayPort: number;
|
|
188
|
+
workspaceDir: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Container topologies whose gateway runs on this machine without host `resources`. */
|
|
192
|
+
function isLocalContainerEntry(entry: AssistantEntry): boolean {
|
|
193
|
+
return entry.cloud === "docker" || entry.cloud === "apple-container";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Map an entry to its local tunnel target, or null when it has no locally
|
|
198
|
+
* reachable gateway (e.g. platform-hosted). Entries with `resources` carry
|
|
199
|
+
* their own gateway port and instance workspace. Local container entries
|
|
200
|
+
* (docker, apple-container) run locally without host resources: their gateway
|
|
201
|
+
* port comes from localUrl/runtimeUrl and their ingress state lives in the
|
|
202
|
+
* default workspace, matching the `vellum nginx-ingress` resolution for the
|
|
203
|
+
* same topology.
|
|
204
|
+
*/
|
|
205
|
+
function toLocalTunnelTarget(entry: AssistantEntry): LocalTunnelTarget | null {
|
|
206
|
+
if (entry.resources) {
|
|
207
|
+
return {
|
|
208
|
+
entry,
|
|
209
|
+
gatewayPort: entry.resources.gatewayPort,
|
|
210
|
+
workspaceDir: join(entry.resources.instanceDir, ".vellum", "workspace"),
|
|
211
|
+
};
|
|
122
212
|
}
|
|
213
|
+
if (isLocalContainerEntry(entry)) {
|
|
214
|
+
const gatewayPort = parseGatewayPortFromEntryUrls(entry);
|
|
215
|
+
if (gatewayPort !== undefined) {
|
|
216
|
+
return { entry, gatewayPort, workspaceDir: getDefaultWorkspaceDir() };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
123
220
|
}
|
|
124
221
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
GATEWAY_PORT
|
|
131
|
-
);
|
|
222
|
+
function describeUntunnelableEntry(entry: AssistantEntry): string {
|
|
223
|
+
const reference = formatAssistantReference(entry);
|
|
224
|
+
return entry.cloud === "vellum"
|
|
225
|
+
? `Assistant '${reference}' runs on Vellum Cloud and needs no tunnel.`
|
|
226
|
+
: `Assistant '${reference}' has no locally managed runtime to tunnel.`;
|
|
132
227
|
}
|
|
133
228
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Resolve the assistant whose gateway and workspace the tunnel edge fronts.
|
|
231
|
+
* Tunnels only make sense for assistants with a local gateway; when the
|
|
232
|
+
* resolved entry has none (e.g. the active assistant is platform-hosted) and
|
|
233
|
+
* no name was given, fall back to the sole local target, otherwise exit with
|
|
234
|
+
* an error naming the local assistants to pass explicitly.
|
|
235
|
+
*/
|
|
236
|
+
function resolveLocalTunnelTarget(
|
|
237
|
+
assistantName: string | null,
|
|
238
|
+
): LocalTunnelTarget {
|
|
239
|
+
const entry = resolveTargetAssistant(assistantName ?? undefined);
|
|
240
|
+
|
|
241
|
+
const target = toLocalTunnelTarget(entry);
|
|
242
|
+
if (target) {
|
|
243
|
+
return target;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const localTargets = loadAllAssistants()
|
|
247
|
+
.map(toLocalTunnelTarget)
|
|
248
|
+
.filter((local): local is LocalTunnelTarget => local !== null);
|
|
249
|
+
|
|
250
|
+
if (!assistantName && localTargets.length === 1) {
|
|
251
|
+
console.log(
|
|
252
|
+
`${describeUntunnelableEntry(entry)} Tunneling the local assistant '${formatAssistantReference(localTargets[0].entry)}' instead.`,
|
|
143
253
|
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
254
|
+
return localTargets[0];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.error(describeUntunnelableEntry(entry));
|
|
258
|
+
if (localTargets.length === 0) {
|
|
259
|
+
console.error(
|
|
260
|
+
"No local assistant found to tunnel. Run `vellum hatch` first.",
|
|
261
|
+
);
|
|
262
|
+
} else {
|
|
263
|
+
console.error(
|
|
264
|
+
`Pass a local assistant as the name argument: ${localTargets
|
|
265
|
+
.map((local) => formatAssistantReference(local.entry))
|
|
266
|
+
.join(", ")}.`,
|
|
149
267
|
);
|
|
150
268
|
}
|
|
269
|
+
process.exit(1);
|
|
151
270
|
}
|
|
152
271
|
|
|
153
272
|
export async function tunnel(): Promise<void> {
|
|
154
|
-
const { assistantName, provider } = parseArgs();
|
|
273
|
+
const { assistantName, provider, domain, clearDomain } = parseArgs();
|
|
155
274
|
|
|
156
|
-
|
|
275
|
+
if (provider === "vellum") {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Tunnel provider '${provider}' is not yet implemented. ` +
|
|
278
|
+
`If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
157
281
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
282
|
+
const { entry, gatewayPort, workspaceDir } =
|
|
283
|
+
resolveLocalTunnelTarget(assistantName);
|
|
284
|
+
|
|
285
|
+
if (clearDomain) {
|
|
286
|
+
saveNgrokDomain(workspaceDir, null);
|
|
287
|
+
console.log("Cleared the saved ngrok domain from the workspace config.");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let edge: TunnelEdge;
|
|
291
|
+
try {
|
|
292
|
+
edge = await ensureTunnelEdge({
|
|
293
|
+
assistantId: entry.assistantId,
|
|
294
|
+
workspaceDir,
|
|
295
|
+
gatewayPort,
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
299
|
process.exit(1);
|
|
167
300
|
}
|
|
168
301
|
|
|
169
|
-
|
|
170
|
-
|
|
302
|
+
console.log(
|
|
303
|
+
`${edge.started ? "Started" : "Reusing"} the nginx edge on 127.0.0.1:${edge.port} ` +
|
|
304
|
+
`(serves ${formatEdgeMode(edge.includesWebApp)}).`,
|
|
305
|
+
);
|
|
306
|
+
|
|
171
307
|
const baseTunnelOpts = {
|
|
172
|
-
port:
|
|
308
|
+
port: edge.port,
|
|
173
309
|
assistantId: entry.assistantId,
|
|
174
|
-
|
|
175
|
-
? { workspaceDir: join(resources.instanceDir, ".vellum", "workspace") }
|
|
176
|
-
: {}),
|
|
310
|
+
workspaceDir,
|
|
177
311
|
};
|
|
178
312
|
|
|
179
313
|
if (provider === "ngrok") {
|
|
180
314
|
await runNgrokTunnel({
|
|
181
315
|
...baseTunnelOpts,
|
|
182
|
-
|
|
183
|
-
entry.assistantId,
|
|
184
|
-
gatewayPort,
|
|
185
|
-
),
|
|
316
|
+
...(domain ? { domain } : {}),
|
|
186
317
|
});
|
|
187
318
|
return;
|
|
188
319
|
}
|
|
189
320
|
|
|
190
321
|
if (provider === "cloudflare") {
|
|
191
|
-
await runCloudflareTunnel(
|
|
192
|
-
...baseTunnelOpts,
|
|
193
|
-
preferNginxIngress: await shouldPreferNginxIngress(
|
|
194
|
-
entry.assistantId,
|
|
195
|
-
gatewayPort,
|
|
196
|
-
),
|
|
197
|
-
});
|
|
322
|
+
await runCloudflareTunnel(baseTunnelOpts);
|
|
198
323
|
return;
|
|
199
324
|
}
|
|
200
325
|
|
|
201
|
-
|
|
202
|
-
await runTailscaleTunnel({
|
|
203
|
-
...baseTunnelOpts,
|
|
204
|
-
preferNginxIngress: await shouldPreferNginxIngress(
|
|
205
|
-
entry.assistantId,
|
|
206
|
-
gatewayPort,
|
|
207
|
-
),
|
|
208
|
-
});
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Tunnel provider '${provider}' is not yet implemented. ` +
|
|
214
|
-
`If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
|
|
215
|
-
);
|
|
326
|
+
await runTailscaleTunnel(baseTunnelOpts);
|
|
216
327
|
}
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
startLocalDaemon,
|
|
73
73
|
stopLocalProcesses,
|
|
74
74
|
} from "../lib/local.js";
|
|
75
|
-
import {
|
|
75
|
+
import { restoreTunnelEdgeAndAutoTunnel } from "../lib/tunnel-edge.js";
|
|
76
76
|
import {
|
|
77
77
|
leaseGuardianToken,
|
|
78
78
|
resetGuardianBootstrap,
|
|
@@ -1044,7 +1044,8 @@ async function upgradeLocal(
|
|
|
1044
1044
|
".vellum",
|
|
1045
1045
|
"workspace",
|
|
1046
1046
|
);
|
|
1047
|
-
const ngrokChild = await
|
|
1047
|
+
const ngrokChild = await restoreTunnelEdgeAndAutoTunnel(
|
|
1048
|
+
entry.assistantId,
|
|
1048
1049
|
entry.resources.gatewayPort,
|
|
1049
1050
|
workspaceDir,
|
|
1050
1051
|
);
|