@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
|
@@ -3,24 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Import a pairing bundle printed by `vellum pair` on another machine and
|
|
5
5
|
* register it locally so `vellum client`/`message`/`events <name>` work against
|
|
6
|
-
* the remote assistant.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* deviceId })). We store the entry under a UNIQUE LOCAL id (not the bundle's
|
|
10
|
-
* assistantId, which is typically "self" and would collide across hosts). This
|
|
11
|
-
* is safe because the gateway's runtime proxy strips the `/v1/assistants/<id>/`
|
|
12
|
-
* segment before forwarding, so the local id never has to match the remote one
|
|
13
|
-
* — the token (validated by signature/audience) is what authorizes requests.
|
|
6
|
+
* the remote assistant. Decoding, id derivation, and the lockfile/token writes
|
|
7
|
+
* live in `pairAssistant` (`@vellumai/local-mode`); this command owns only the
|
|
8
|
+
* argv parsing and output copy.
|
|
14
9
|
*/
|
|
15
10
|
|
|
16
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
decodePairBundle,
|
|
13
|
+
pairAssistant,
|
|
14
|
+
resolveConfigDir,
|
|
15
|
+
} from "@vellumai/local-mode";
|
|
17
16
|
|
|
18
17
|
import { extractFlag } from "../../lib/arg-utils.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
saveAssistantEntry,
|
|
22
|
-
} from "../../lib/assistant-config.js";
|
|
23
|
-
import { saveGuardianToken } from "../../lib/guardian-token.js";
|
|
18
|
+
import { getLockfilePaths } from "../../lib/environments/paths.js";
|
|
19
|
+
import { getCurrentEnvironment } from "../../lib/environments/resolve.js";
|
|
24
20
|
|
|
25
21
|
function printUsage(): void {
|
|
26
22
|
console.log(`vellum connect import - Register an assistant paired from another machine
|
|
@@ -41,85 +37,6 @@ EXAMPLES:
|
|
|
41
37
|
`);
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
interface PairBundle {
|
|
45
|
-
gatewayUrl: string;
|
|
46
|
-
token: string;
|
|
47
|
-
assistantId?: string;
|
|
48
|
-
deviceId?: string;
|
|
49
|
-
// Optional refresh credential. Present when the host's gateway issued a
|
|
50
|
-
// device-bound token pair; absent for older access-only bundles (which remain
|
|
51
|
-
// importable, just without auto-renewal). `refreshTokenExpiresAt` mirrors
|
|
52
|
-
// GuardianTokenData (ISO string OR epoch-ms number) so a numeric expiry isn't
|
|
53
|
-
// silently dropped on import.
|
|
54
|
-
refreshToken?: string;
|
|
55
|
-
refreshTokenExpiresAt?: string | number;
|
|
56
|
-
refreshAfter?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Decode the base64 bundle, returning null if malformed or missing fields. */
|
|
60
|
-
function decodeBundle(blob: string): PairBundle | null {
|
|
61
|
-
let json: unknown;
|
|
62
|
-
try {
|
|
63
|
-
json = JSON.parse(Buffer.from(blob, "base64").toString("utf8"));
|
|
64
|
-
} catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
if (typeof json !== "object" || json === null) return null;
|
|
68
|
-
const b = json as Record<string, unknown>;
|
|
69
|
-
if (typeof b.gatewayUrl !== "string" || typeof b.token !== "string") {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
// The gatewayUrl is persisted as runtimeUrl and used to build fetch URLs, so
|
|
73
|
-
// require an absolute http(s) URL here rather than letting an invalid string
|
|
74
|
-
// through (which would crash `new URL(...)` or break later client calls).
|
|
75
|
-
try {
|
|
76
|
-
const parsed = new URL(b.gatewayUrl);
|
|
77
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
gatewayUrl: b.gatewayUrl,
|
|
85
|
-
token: b.token,
|
|
86
|
-
assistantId: typeof b.assistantId === "string" ? b.assistantId : undefined,
|
|
87
|
-
deviceId: typeof b.deviceId === "string" ? b.deviceId : undefined,
|
|
88
|
-
refreshToken:
|
|
89
|
-
typeof b.refreshToken === "string" ? b.refreshToken : undefined,
|
|
90
|
-
refreshTokenExpiresAt:
|
|
91
|
-
typeof b.refreshTokenExpiresAt === "string" ||
|
|
92
|
-
typeof b.refreshTokenExpiresAt === "number"
|
|
93
|
-
? b.refreshTokenExpiresAt
|
|
94
|
-
: undefined,
|
|
95
|
-
refreshAfter:
|
|
96
|
-
typeof b.refreshAfter === "string" ? b.refreshAfter : undefined,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Lowercase, collapse non-alphanumerics to single dashes, trim dashes. */
|
|
101
|
-
function slugify(name: string): string {
|
|
102
|
-
return name
|
|
103
|
-
.toLowerCase()
|
|
104
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
105
|
-
.replace(/^-+|-+$/g, "");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Best-effort JWT `exp` (epoch seconds) → epoch ms; null if undecodable. */
|
|
109
|
-
function jwtExpiryMs(token: string): number | null {
|
|
110
|
-
const parts = token.split(".");
|
|
111
|
-
if (parts.length !== 3) return null;
|
|
112
|
-
try {
|
|
113
|
-
const payload = JSON.parse(
|
|
114
|
-
Buffer.from(parts[1], "base64").toString("utf8"),
|
|
115
|
-
);
|
|
116
|
-
if (typeof payload.exp === "number") return payload.exp * 1000;
|
|
117
|
-
} catch {
|
|
118
|
-
/* fall through */
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
40
|
export async function connectImport(): Promise<void> {
|
|
124
41
|
const rawArgs = process.argv.slice(4);
|
|
125
42
|
|
|
@@ -136,82 +53,44 @@ export async function connectImport(): Promise<void> {
|
|
|
136
53
|
process.exit(1);
|
|
137
54
|
}
|
|
138
55
|
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
56
|
+
const decoded = decodePairBundle(blob);
|
|
57
|
+
if (!decoded.ok) {
|
|
141
58
|
console.error(
|
|
142
59
|
"Error: invalid pairing bundle. Paste the full base64 string printed by `vellum pair`.",
|
|
143
60
|
);
|
|
144
61
|
process.exit(1);
|
|
145
62
|
}
|
|
146
63
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// resources/runtime metadata. Reject and let the user pick a fresh --name.
|
|
166
|
-
const existing = findAssistantByName(localId);
|
|
167
|
-
if (existing && existing.paired !== true) {
|
|
168
|
-
console.error(
|
|
169
|
-
`Error: an assistant named '${localId}' already exists locally. ` +
|
|
170
|
-
"Choose a different --name to avoid overwriting it.",
|
|
171
|
-
);
|
|
64
|
+
const result = pairAssistant(
|
|
65
|
+
getLockfilePaths(getCurrentEnvironment()),
|
|
66
|
+
resolveConfigDir(process.env),
|
|
67
|
+
{ bundle: decoded.bundle, name: nameFlag },
|
|
68
|
+
);
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
if (result.status === 400) {
|
|
71
|
+
console.error(
|
|
72
|
+
"Error: --name must contain at least one alphanumeric character.",
|
|
73
|
+
);
|
|
74
|
+
} else if (result.status === 409) {
|
|
75
|
+
console.error(
|
|
76
|
+
`Error: an assistant named '${result.assistantId}' already exists locally. ` +
|
|
77
|
+
"Choose a different --name to avoid overwriting it.",
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
console.error(`Error: ${result.error}`);
|
|
81
|
+
}
|
|
172
82
|
process.exit(1);
|
|
173
83
|
}
|
|
174
|
-
const existed = existing !== null;
|
|
175
|
-
|
|
176
|
-
saveAssistantEntry({
|
|
177
|
-
assistantId: localId,
|
|
178
|
-
name: nameFlag ?? `paired (${new URL(bundle.gatewayUrl).host})`,
|
|
179
|
-
runtimeUrl: bundle.gatewayUrl,
|
|
180
|
-
// Paired entries are reached by bearer token at the remote runtimeUrl
|
|
181
|
-
// (a non-"vellum" cloud selects the bearer-token auth path in client.ts).
|
|
182
|
-
// The "paired" topology lets lifecycle/status commands (ps/wake/sleep)
|
|
183
|
-
// recognize this as a remote pairing rather than an on-machine process.
|
|
184
|
-
cloud: "paired",
|
|
185
|
-
// Marks this entry as a connect-import so re-imports update in place while
|
|
186
|
-
// imports never silently overwrite a non-paired assistant (see guard above).
|
|
187
|
-
paired: true,
|
|
188
|
-
species: "vellum",
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
const now = Date.now();
|
|
192
|
-
const hasRefresh = Boolean(bundle.refreshToken);
|
|
193
|
-
saveGuardianToken(localId, {
|
|
194
|
-
guardianPrincipalId: "imported",
|
|
195
|
-
accessToken: bundle.token,
|
|
196
|
-
accessTokenExpiresAt:
|
|
197
|
-
jwtExpiryMs(bundle.token) ?? now + 24 * 60 * 60 * 1000,
|
|
198
|
-
refreshToken: bundle.refreshToken ?? "",
|
|
199
|
-
refreshTokenExpiresAt: bundle.refreshTokenExpiresAt ?? 0,
|
|
200
|
-
refreshAfter: bundle.refreshAfter ?? "",
|
|
201
|
-
isNew: false,
|
|
202
|
-
deviceId: bundle.deviceId ?? "",
|
|
203
|
-
leasedAt: new Date(now).toISOString(),
|
|
204
|
-
});
|
|
205
84
|
|
|
206
85
|
console.log(
|
|
207
|
-
`${
|
|
86
|
+
`${result.updated ? "Updated" : "Imported"} paired assistant '${result.assistantId}'.`,
|
|
208
87
|
);
|
|
209
88
|
console.log("");
|
|
210
|
-
console.log(` Connect with: vellum client ${
|
|
89
|
+
console.log(` Connect with: vellum client ${result.assistantId}`);
|
|
211
90
|
console.log("");
|
|
212
91
|
console.log(
|
|
213
|
-
|
|
214
|
-
? "Note:
|
|
215
|
-
: "Note:
|
|
92
|
+
result.accessOnly
|
|
93
|
+
? "Note: the token is access-only and will expire. Re-run `vellum pair` and import again when it does."
|
|
94
|
+
: "Note: this connection includes a refresh credential, so it can renew itself. Re-pair only if it's revoked or the refresh credential expires.",
|
|
216
95
|
);
|
|
217
96
|
}
|
|
@@ -9,36 +9,39 @@ import {
|
|
|
9
9
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
10
10
|
import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
|
|
11
11
|
import { GATEWAY_PORT } from "../lib/constants.js";
|
|
12
|
-
import {
|
|
13
|
-
formatFeatureFlagGateMessage,
|
|
14
|
-
isAssistantFeatureFlagEnabled,
|
|
15
|
-
WEB_REMOTE_INGRESS_FLAG,
|
|
16
|
-
} from "../lib/feature-flags.js";
|
|
12
|
+
import { WEB_REMOTE_INGRESS_FLAG } from "../lib/feature-flags.js";
|
|
17
13
|
import {
|
|
18
14
|
DEFAULT_NGINX_INGRESS_PORT,
|
|
15
|
+
ensureTunnelEdge,
|
|
16
|
+
formatEdgeMode,
|
|
19
17
|
getIngressPaths,
|
|
20
18
|
getIngressPid,
|
|
19
|
+
getNginxIngressPort,
|
|
21
20
|
isIngressRunning,
|
|
22
|
-
|
|
23
|
-
startRemoteWebIngress,
|
|
21
|
+
readIngressState,
|
|
24
22
|
stopIngressNginx,
|
|
23
|
+
type TunnelEdge,
|
|
25
24
|
} from "../lib/nginx-ingress.js";
|
|
26
25
|
|
|
27
26
|
function printHelp(): void {
|
|
28
27
|
console.log("Usage: vellum nginx-ingress <subcommand> [<name>] [options]");
|
|
29
28
|
console.log("");
|
|
29
|
+
console.log("Manage the nginx edge that fronts the gateway for tunnel");
|
|
30
|
+
console.log("traffic: browser/webhooks → tunnel (TLS) → nginx@127.0.0.1.");
|
|
30
31
|
console.log(
|
|
31
|
-
"
|
|
32
|
+
"`vellum tunnel` starts or reuses this edge automatically; use this",
|
|
32
33
|
);
|
|
33
34
|
console.log(
|
|
34
|
-
"
|
|
35
|
+
"command as plumbing: check status, stop the edge, or start it for a",
|
|
35
36
|
);
|
|
36
|
-
console.log("
|
|
37
|
+
console.log("bring-your-own HTTPS front (e.g. `tailscale serve`).");
|
|
37
38
|
console.log("");
|
|
38
39
|
console.log("Subcommands:");
|
|
39
|
-
console.log(" up Generate the nginx config and start the
|
|
40
|
-
console.log(" down Stop the
|
|
41
|
-
console.log(
|
|
40
|
+
console.log(" up Generate the nginx config and start the edge");
|
|
41
|
+
console.log(" down Stop the edge");
|
|
42
|
+
console.log(
|
|
43
|
+
" status Show whether the edge is running, its mode, and where",
|
|
44
|
+
);
|
|
42
45
|
console.log("");
|
|
43
46
|
console.log("Arguments:");
|
|
44
47
|
console.log(
|
|
@@ -50,19 +53,23 @@ function printHelp(): void {
|
|
|
50
53
|
console.log("");
|
|
51
54
|
console.log("Environment:");
|
|
52
55
|
console.log(
|
|
53
|
-
` VELLUM_NGINX_INGRESS_PORT nginx
|
|
56
|
+
` VELLUM_NGINX_INGRESS_PORT nginx edge loopback listen port (default ${DEFAULT_NGINX_INGRESS_PORT})`,
|
|
54
57
|
);
|
|
55
58
|
console.log(" NGINX_BIN Path to the nginx binary");
|
|
56
59
|
console.log("");
|
|
57
60
|
console.log("Examples:");
|
|
58
|
-
console.log(" $ vellum nginx-ingress up");
|
|
59
61
|
console.log(" $ vellum nginx-ingress status");
|
|
62
|
+
console.log(" $ vellum nginx-ingress up");
|
|
60
63
|
console.log(" $ vellum nginx-ingress down my-assistant");
|
|
61
64
|
console.log("");
|
|
62
65
|
console.log("Feature flags:");
|
|
63
66
|
console.log(
|
|
64
|
-
` ${WEB_REMOTE_INGRESS_FLAG}
|
|
67
|
+
` ${WEB_REMOTE_INGRESS_FLAG} selects the edge mode: enabled serves the`,
|
|
65
68
|
);
|
|
69
|
+
console.log(
|
|
70
|
+
" remote web app alongside webhooks; disabled starts a webhooks-only",
|
|
71
|
+
);
|
|
72
|
+
console.log(" proxy.");
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
interface NginxIngressTarget {
|
|
@@ -83,14 +90,23 @@ function parsePortFromUrl(url: unknown): number | undefined {
|
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Derive the gateway port from an entry's recorded URLs, preferring the
|
|
95
|
+
* loopback `localUrl` over `runtimeUrl`. Undefined when neither carries an
|
|
96
|
+
* explicit port (e.g. a platform-hosted https runtime URL).
|
|
97
|
+
*/
|
|
98
|
+
export function parseGatewayPortFromEntryUrls(
|
|
99
|
+
entry: AssistantEntry | undefined,
|
|
100
|
+
): number | undefined {
|
|
87
101
|
return (
|
|
88
|
-
parsePortFromUrl(entry?.localUrl) ??
|
|
89
|
-
parsePortFromUrl(entry?.runtimeUrl) ??
|
|
90
|
-
GATEWAY_PORT
|
|
102
|
+
parsePortFromUrl(entry?.localUrl) ?? parsePortFromUrl(entry?.runtimeUrl)
|
|
91
103
|
);
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
function resolveEntryGatewayPort(entry: AssistantEntry | undefined): number {
|
|
107
|
+
return parseGatewayPortFromEntryUrls(entry) ?? GATEWAY_PORT;
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
/**
|
|
95
111
|
* Resolve which assistant nginx ingress fronts. Multi-instance hatches allocate
|
|
96
112
|
* per-assistant gateway ports and workspaces, so both must come from the
|
|
@@ -129,100 +145,52 @@ export function resolveNginxIngressTarget(
|
|
|
129
145
|
};
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
async function
|
|
133
|
-
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
if (!target.assistantId) {
|
|
136
|
-
throw new Error(formatFeatureFlagGateMessage(WEB_REMOTE_INGRESS_FLAG));
|
|
137
|
-
}
|
|
148
|
+
export async function up(target: NginxIngressTarget): Promise<void> {
|
|
149
|
+
const { workspaceDir, gatewayPort } = target;
|
|
138
150
|
|
|
139
|
-
let
|
|
151
|
+
let edge: TunnelEdge;
|
|
140
152
|
try {
|
|
141
|
-
|
|
142
|
-
target.assistantId,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
edge = await ensureTunnelEdge({
|
|
154
|
+
assistantId: target.assistantId,
|
|
155
|
+
workspaceDir,
|
|
156
|
+
gatewayPort,
|
|
157
|
+
onStarting: ({ version, webDistDir, listenPort }) => {
|
|
158
|
+
console.log(`Using ${version}`);
|
|
159
|
+
const webSegment = webDistDir ? `web ${webDistDir} + ` : "";
|
|
160
|
+
console.log(
|
|
161
|
+
`Starting nginx ingress on 127.0.0.1:${listenPort} → ${webSegment}gateway 127.0.0.1:${gatewayPort}...`,
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
146
165
|
} catch (err) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
err instanceof Error ? err.message : String(err)
|
|
150
|
-
}`,
|
|
151
|
-
);
|
|
166
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
167
|
+
process.exit(1);
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
|
|
155
|
-
throw new Error(formatFeatureFlagGateMessage(WEB_REMOTE_INGRESS_FLAG));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function up(target: NginxIngressTarget): Promise<void> {
|
|
160
|
-
const { workspaceDir, gatewayPort } = target;
|
|
170
|
+
const mode = formatEdgeMode(edge.includesWebApp);
|
|
161
171
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
gatewayPort,
|
|
167
|
-
onStarting: ({ version, webDistDir, listenPort }) => {
|
|
168
|
-
console.log(`Using ${version}`);
|
|
169
|
-
console.log(
|
|
170
|
-
`Starting nginx ingress on 127.0.0.1:${listenPort} → web ${webDistDir} + gateway 127.0.0.1:${gatewayPort}...`,
|
|
171
|
-
);
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
switch (result.status) {
|
|
176
|
-
case "already-running":
|
|
177
|
-
console.log("nginx ingress is already running.");
|
|
178
|
-
await status(target);
|
|
179
|
-
return;
|
|
180
|
-
case "started":
|
|
181
|
-
console.log("");
|
|
182
|
-
console.log(
|
|
183
|
-
`nginx ingress running: http://127.0.0.1:${result.listenPort}`,
|
|
184
|
-
);
|
|
185
|
-
console.log("");
|
|
186
|
-
console.log("Next steps:");
|
|
187
|
-
console.log(
|
|
188
|
-
" vellum tunnel --provider ngrok # tunnel now targets nginx ingress",
|
|
189
|
-
);
|
|
190
|
-
console.log(" vellum nginx-ingress down # stop the proxy");
|
|
191
|
-
return;
|
|
192
|
-
case "nginx-missing":
|
|
193
|
-
console.error("Error: nginx is not installed.");
|
|
194
|
-
console.error("");
|
|
195
|
-
console.error("Install nginx:");
|
|
196
|
-
console.error(" macOS: brew install nginx");
|
|
197
|
-
console.error(" Linux: sudo apt install nginx");
|
|
198
|
-
console.error("");
|
|
199
|
-
console.error(
|
|
200
|
-
"Or point NGINX_BIN at an existing binary: NGINX_BIN=/path/to/nginx",
|
|
201
|
-
);
|
|
202
|
-
break;
|
|
203
|
-
case "web-dist-missing":
|
|
204
|
-
console.error(
|
|
205
|
-
"Error: unable to locate built web assets for remote web ingress.",
|
|
206
|
-
);
|
|
207
|
-
console.error("");
|
|
208
|
-
console.error("Build the SPA first:");
|
|
209
|
-
console.error(
|
|
210
|
-
" cd clients/web && VITE_PLATFORM_MODE=false bun run build",
|
|
211
|
-
);
|
|
212
|
-
console.error("");
|
|
213
|
-
console.error(
|
|
214
|
-
"Or install @vellumai/web so its packaged dist directory is available.",
|
|
215
|
-
);
|
|
216
|
-
break;
|
|
217
|
-
case "unreachable":
|
|
218
|
-
console.error(
|
|
219
|
-
`Error: nginx ingress did not become reachable on 127.0.0.1:${result.listenPort}.`,
|
|
220
|
-
);
|
|
221
|
-
console.error(`Check the nginx log: ${result.logPath}`);
|
|
222
|
-
break;
|
|
172
|
+
if (!edge.started) {
|
|
173
|
+
console.log(`nginx ingress is already running (${mode}).`);
|
|
174
|
+
await status(target);
|
|
175
|
+
return;
|
|
223
176
|
}
|
|
224
177
|
|
|
225
|
-
|
|
178
|
+
console.log("");
|
|
179
|
+
console.log(`nginx ingress running: http://127.0.0.1:${edge.port} (${mode})`);
|
|
180
|
+
if (!edge.includesWebApp) {
|
|
181
|
+
console.log(
|
|
182
|
+
`Enable the ${WEB_REMOTE_INGRESS_FLAG} feature flag to also serve the web app remotely.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log("Next steps:");
|
|
187
|
+
console.log(
|
|
188
|
+
" vellum tunnel --provider ngrok # put an HTTPS front on this edge",
|
|
189
|
+
);
|
|
190
|
+
console.log(
|
|
191
|
+
" # (cloudflare and tailscale work too)",
|
|
192
|
+
);
|
|
193
|
+
console.log(" vellum nginx-ingress down # stop the edge");
|
|
226
194
|
}
|
|
227
195
|
|
|
228
196
|
async function down(target: NginxIngressTarget): Promise<void> {
|
|
@@ -236,7 +204,7 @@ async function down(target: NginxIngressTarget): Promise<void> {
|
|
|
236
204
|
);
|
|
237
205
|
}
|
|
238
206
|
|
|
239
|
-
async function status(target: NginxIngressTarget): Promise<void> {
|
|
207
|
+
export async function status(target: NginxIngressTarget): Promise<void> {
|
|
240
208
|
const { workspaceDir, gatewayPort } = target;
|
|
241
209
|
const { confPath, logPath } = getIngressPaths(workspaceDir);
|
|
242
210
|
const pid = getIngressPid(workspaceDir);
|
|
@@ -244,11 +212,20 @@ async function status(target: NginxIngressTarget): Promise<void> {
|
|
|
244
212
|
console.log("nginx ingress: not running");
|
|
245
213
|
return;
|
|
246
214
|
}
|
|
247
|
-
const
|
|
215
|
+
const state = readIngressState(workspaceDir);
|
|
216
|
+
const listenPort = state?.listenPort ?? getNginxIngressPort();
|
|
217
|
+
const mode = formatEdgeMode(state?.includeWebApp ?? true);
|
|
218
|
+
// The recorded gateway port is what the running edge proxies; a state
|
|
219
|
+
// record without gatewayPort is unverified.
|
|
220
|
+
const gatewayLine =
|
|
221
|
+
state?.gatewayPort !== undefined
|
|
222
|
+
? `http://127.0.0.1:${state.gatewayPort}`
|
|
223
|
+
: `http://127.0.0.1:${gatewayPort} (unverified)`;
|
|
248
224
|
console.log("nginx ingress: running");
|
|
249
225
|
console.log(` PID: ${pid}`);
|
|
250
|
-
console.log(`
|
|
251
|
-
console.log(`
|
|
226
|
+
console.log(` Mode: ${mode}`);
|
|
227
|
+
console.log(` Listen: http://127.0.0.1:${listenPort}`);
|
|
228
|
+
console.log(` Gateway: ${gatewayLine}`);
|
|
252
229
|
console.log(` Config: ${confPath}`);
|
|
253
230
|
console.log(` Log: ${logPath}`);
|
|
254
231
|
}
|