@vellumai/cli 0.10.12-dev.202607250047.75b9ec6 → 0.10.12-staging.1
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/@vellumai/local-mode/src/lockfile-contract.test.ts +0 -25
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +0 -7
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +0 -24
- package/node_modules/@vellumai/service-contracts/package.json +0 -1
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +1 -69
- package/package.json +1 -1
- package/src/__tests__/assistant-config.test.ts +2 -118
- package/src/__tests__/flags.test.ts +0 -77
- package/src/__tests__/pair.test.ts +0 -146
- package/src/__tests__/tunnel.test.ts +0 -53
- package/src/__tests__/wake.test.ts +0 -178
- package/src/commands/flags.ts +1 -4
- package/src/commands/nginx-ingress.ts +71 -57
- package/src/commands/pair.ts +0 -25
- package/src/commands/tunnel.ts +2 -15
- package/src/commands/wake.ts +0 -118
- package/src/lib/assistant-config.ts +2 -5
- package/src/lib/hatch-local.ts +86 -98
- package/src/lib/local.ts +2 -20
- package/src/lib/nginx-ingress.ts +0 -90
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +0 -449
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +0 -468
- package/src/__tests__/local-hatch-lock.test.ts +0 -163
- package/src/lib/environments/__tests__/detect.test.ts +0 -101
- package/src/lib/environments/detect.ts +0 -79
- package/src/lib/local-hatch-lock.ts +0 -322
- package/src/lib/stale-cli-hint.ts +0 -11
|
@@ -14,11 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
|
|
15
15
|
import * as assistantConfig from "../lib/assistant-config.js";
|
|
16
16
|
import * as docker from "../lib/docker.js";
|
|
17
|
-
import * as featureFlags from "../lib/feature-flags.js";
|
|
18
17
|
import * as guardianToken from "../lib/guardian-token.js";
|
|
19
|
-
import * as ingressConfig from "../lib/ingress-config.js";
|
|
20
18
|
import * as local from "../lib/local.js";
|
|
21
|
-
import * as nginxIngress from "../lib/nginx-ingress.js";
|
|
22
19
|
import * as ngrok from "../lib/ngrok.js";
|
|
23
20
|
import * as processLib from "../lib/process.js";
|
|
24
21
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
@@ -141,50 +138,11 @@ mock.module("../lib/ngrok", () => ({
|
|
|
141
138
|
maybeStartNgrokTunnel: maybeStartNgrokTunnelMock,
|
|
142
139
|
}));
|
|
143
140
|
|
|
144
|
-
const realFeatureFlags = { ...featureFlags };
|
|
145
|
-
const realIngressConfig = { ...ingressConfig };
|
|
146
|
-
const realNginxIngress = { ...nginxIngress };
|
|
147
|
-
|
|
148
|
-
const isAssistantFeatureFlagEnabledMock = mock<
|
|
149
|
-
typeof featureFlags.isAssistantFeatureFlagEnabled
|
|
150
|
-
>(async () => true);
|
|
151
|
-
|
|
152
|
-
mock.module("../lib/feature-flags.js", () => ({
|
|
153
|
-
...realFeatureFlags,
|
|
154
|
-
isAssistantFeatureFlagEnabled: isAssistantFeatureFlagEnabledMock,
|
|
155
|
-
}));
|
|
156
|
-
|
|
157
|
-
const loadRawConfigMock = mock<typeof ingressConfig.loadRawConfig>(() => ({}));
|
|
158
|
-
|
|
159
|
-
mock.module("../lib/ingress-config.js", () => ({
|
|
160
|
-
...realIngressConfig,
|
|
161
|
-
loadRawConfig: loadRawConfigMock,
|
|
162
|
-
}));
|
|
163
|
-
|
|
164
|
-
const isIngressRunningMock = mock<typeof nginxIngress.isIngressRunning>(
|
|
165
|
-
() => false,
|
|
166
|
-
);
|
|
167
|
-
const startRemoteWebIngressMock = mock<
|
|
168
|
-
typeof nginxIngress.startRemoteWebIngress
|
|
169
|
-
>(async () => ({
|
|
170
|
-
status: "started",
|
|
171
|
-
listenPort: 7840,
|
|
172
|
-
webDistDir: "/tmp/web/dist",
|
|
173
|
-
version: "nginx/1.25.3",
|
|
174
|
-
}));
|
|
175
|
-
|
|
176
|
-
mock.module("../lib/nginx-ingress.js", () => ({
|
|
177
|
-
...realNginxIngress,
|
|
178
|
-
isIngressRunning: isIngressRunningMock,
|
|
179
|
-
startRemoteWebIngress: startRemoteWebIngressMock,
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
141
|
const { wake } = await import("../commands/wake.js");
|
|
183
142
|
|
|
184
143
|
let tempDir: string;
|
|
185
144
|
let originalArgv: string[];
|
|
186
145
|
let logSpy: ReturnType<typeof spyOn>;
|
|
187
|
-
let warnSpy: ReturnType<typeof spyOn>;
|
|
188
146
|
|
|
189
147
|
function makeLocalEntry(): AssistantEntry {
|
|
190
148
|
tempDir = mkdtempSync(join(tmpdir(), "vellum-wake-test-"));
|
|
@@ -209,7 +167,6 @@ beforeEach(() => {
|
|
|
209
167
|
tempDir = "";
|
|
210
168
|
process.argv = ["bun", "vellum", "wake", "--watch", "local-assistant"];
|
|
211
169
|
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
212
|
-
warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
213
170
|
|
|
214
171
|
const entry = makeLocalEntry();
|
|
215
172
|
resolveTargetAssistantMock.mockReset();
|
|
@@ -256,25 +213,11 @@ beforeEach(() => {
|
|
|
256
213
|
);
|
|
257
214
|
maybeStartNgrokTunnelMock.mockReset();
|
|
258
215
|
maybeStartNgrokTunnelMock.mockResolvedValue(null);
|
|
259
|
-
isAssistantFeatureFlagEnabledMock.mockReset();
|
|
260
|
-
isAssistantFeatureFlagEnabledMock.mockResolvedValue(true);
|
|
261
|
-
loadRawConfigMock.mockReset();
|
|
262
|
-
loadRawConfigMock.mockReturnValue({});
|
|
263
|
-
isIngressRunningMock.mockReset();
|
|
264
|
-
isIngressRunningMock.mockReturnValue(false);
|
|
265
|
-
startRemoteWebIngressMock.mockReset();
|
|
266
|
-
startRemoteWebIngressMock.mockResolvedValue({
|
|
267
|
-
status: "started",
|
|
268
|
-
listenPort: 7840,
|
|
269
|
-
webDistDir: "/tmp/web/dist",
|
|
270
|
-
version: "nginx/1.25.3",
|
|
271
|
-
});
|
|
272
216
|
});
|
|
273
217
|
|
|
274
218
|
afterEach(() => {
|
|
275
219
|
process.argv = originalArgv;
|
|
276
220
|
logSpy.mockRestore();
|
|
277
|
-
warnSpy.mockRestore();
|
|
278
221
|
if (tempDir) {
|
|
279
222
|
rmSync(tempDir, { recursive: true, force: true });
|
|
280
223
|
}
|
|
@@ -287,9 +230,6 @@ afterAll(() => {
|
|
|
287
230
|
mock.module("../lib/process", () => realProcessLib);
|
|
288
231
|
mock.module("../lib/local", () => realLocal);
|
|
289
232
|
mock.module("../lib/ngrok", () => realNgrok);
|
|
290
|
-
mock.module("../lib/feature-flags.js", () => realFeatureFlags);
|
|
291
|
-
mock.module("../lib/ingress-config.js", () => realIngressConfig);
|
|
292
|
-
mock.module("../lib/nginx-ingress.js", () => realNginxIngress);
|
|
293
233
|
});
|
|
294
234
|
|
|
295
235
|
describe("vellum wake", () => {
|
|
@@ -402,121 +342,3 @@ describe("vellum wake", () => {
|
|
|
402
342
|
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
403
343
|
});
|
|
404
344
|
});
|
|
405
|
-
|
|
406
|
-
describe("vellum wake — web ingress restore", () => {
|
|
407
|
-
const enabledConfig = {
|
|
408
|
-
ingress: { enabled: true, publicBaseUrl: "https://assistant.example.com" },
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
beforeEach(() => {
|
|
412
|
-
process.argv = ["bun", "vellum", "wake", "local-assistant"];
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test("restores the edge when ingress is enabled, the flag is on, and it is not already running", async () => {
|
|
416
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
417
|
-
|
|
418
|
-
await wake();
|
|
419
|
-
|
|
420
|
-
expect(isAssistantFeatureFlagEnabledMock).toHaveBeenCalledWith(
|
|
421
|
-
"local-assistant",
|
|
422
|
-
"web-remote-ingress",
|
|
423
|
-
{ runtimeUrl: "http://127.0.0.1:7830" },
|
|
424
|
-
);
|
|
425
|
-
expect(startRemoteWebIngressMock).toHaveBeenCalledWith(
|
|
426
|
-
expect.objectContaining({
|
|
427
|
-
workspaceDir: join(tempDir, ".vellum", "workspace"),
|
|
428
|
-
gatewayPort: 7830,
|
|
429
|
-
}),
|
|
430
|
-
);
|
|
431
|
-
expect(logSpy).toHaveBeenCalledWith("Wake complete.");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test("does not attempt a restore when ingress is disabled", async () => {
|
|
435
|
-
loadRawConfigMock.mockReturnValue({ ingress: { enabled: false } });
|
|
436
|
-
|
|
437
|
-
await wake();
|
|
438
|
-
|
|
439
|
-
expect(isAssistantFeatureFlagEnabledMock).not.toHaveBeenCalled();
|
|
440
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
test("does not attempt a restore when ingress config is absent", async () => {
|
|
444
|
-
loadRawConfigMock.mockReturnValue({});
|
|
445
|
-
|
|
446
|
-
await wake();
|
|
447
|
-
|
|
448
|
-
expect(isAssistantFeatureFlagEnabledMock).not.toHaveBeenCalled();
|
|
449
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
test("does not attempt a restore when enabled but the public URL is missing", async () => {
|
|
453
|
-
loadRawConfigMock.mockReturnValue({ ingress: { enabled: true } });
|
|
454
|
-
|
|
455
|
-
await wake();
|
|
456
|
-
|
|
457
|
-
expect(isAssistantFeatureFlagEnabledMock).not.toHaveBeenCalled();
|
|
458
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
test("does not start a second edge when one is already running", async () => {
|
|
462
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
463
|
-
isIngressRunningMock.mockReturnValue(true);
|
|
464
|
-
|
|
465
|
-
await wake();
|
|
466
|
-
|
|
467
|
-
// Already up — skip the flag probe and the start entirely.
|
|
468
|
-
expect(isAssistantFeatureFlagEnabledMock).not.toHaveBeenCalled();
|
|
469
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
test("skips the restore quietly with a hint when the flag is off", async () => {
|
|
473
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
474
|
-
isAssistantFeatureFlagEnabledMock.mockResolvedValue(false);
|
|
475
|
-
|
|
476
|
-
await wake();
|
|
477
|
-
|
|
478
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
479
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
480
|
-
expect.stringContaining("web-remote-ingress"),
|
|
481
|
-
);
|
|
482
|
-
expect(logSpy).toHaveBeenCalledWith("Wake complete.");
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
test("a thrown restore failure warns but does not fail the wake", async () => {
|
|
486
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
487
|
-
startRemoteWebIngressMock.mockRejectedValue(new Error("nginx boom"));
|
|
488
|
-
|
|
489
|
-
await wake();
|
|
490
|
-
|
|
491
|
-
expect(startRemoteWebIngressMock).toHaveBeenCalled();
|
|
492
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
493
|
-
expect(logSpy).toHaveBeenCalledWith("Wake complete.");
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test("an unreachable edge warns but does not fail the wake", async () => {
|
|
497
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
498
|
-
startRemoteWebIngressMock.mockResolvedValue({
|
|
499
|
-
status: "unreachable",
|
|
500
|
-
listenPort: 7840,
|
|
501
|
-
logPath: "/tmp/nginx-ingress.log",
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
await wake();
|
|
505
|
-
|
|
506
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
507
|
-
expect(logSpy).toHaveBeenCalledWith("Wake complete.");
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test("a failed flag probe warns and leaves the edge down without failing the wake", async () => {
|
|
511
|
-
loadRawConfigMock.mockReturnValue(enabledConfig);
|
|
512
|
-
isAssistantFeatureFlagEnabledMock.mockRejectedValue(
|
|
513
|
-
new Error("gateway unreachable"),
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
await wake();
|
|
517
|
-
|
|
518
|
-
expect(startRemoteWebIngressMock).not.toHaveBeenCalled();
|
|
519
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
520
|
-
expect(logSpy).toHaveBeenCalledWith("Wake complete.");
|
|
521
|
-
});
|
|
522
|
-
});
|
package/src/commands/flags.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
formatAssistantLookupError,
|
|
5
5
|
lookupAssistantByIdentifier,
|
|
6
6
|
} from "../lib/assistant-config.js";
|
|
7
|
-
import { crossEnvironmentAssistantHint } from "../lib/environments/detect.js";
|
|
8
7
|
|
|
9
8
|
type FeatureFlagEntry = {
|
|
10
9
|
key: string;
|
|
@@ -133,9 +132,7 @@ function rethrowFetchError(err: unknown): never {
|
|
|
133
132
|
(err.message.includes("fetch") || err.message.includes("connect"))
|
|
134
133
|
) {
|
|
135
134
|
throw new Error(
|
|
136
|
-
|
|
137
|
-
crossEnvironmentAssistantHint() ?? ""
|
|
138
|
-
}`,
|
|
135
|
+
"Could not reach the assistant gateway. Is it running? Try 'vellum wake'.",
|
|
139
136
|
);
|
|
140
137
|
}
|
|
141
138
|
throw err;
|
|
@@ -14,16 +14,22 @@ import {
|
|
|
14
14
|
isAssistantFeatureFlagEnabled,
|
|
15
15
|
WEB_REMOTE_INGRESS_FLAG,
|
|
16
16
|
} from "../lib/feature-flags.js";
|
|
17
|
+
import { waitForDaemonReady } from "../lib/http-client.js";
|
|
17
18
|
import {
|
|
18
19
|
DEFAULT_NGINX_INGRESS_PORT,
|
|
20
|
+
findWebDistDir,
|
|
19
21
|
getIngressPaths,
|
|
20
22
|
getIngressPid,
|
|
23
|
+
getNginxIngressPort,
|
|
24
|
+
getNginxVersion,
|
|
21
25
|
isIngressRunning,
|
|
22
26
|
resolveTunnelTargetPort,
|
|
23
|
-
|
|
27
|
+
startIngressNginx,
|
|
24
28
|
stopIngressNginx,
|
|
25
29
|
} from "../lib/nginx-ingress.js";
|
|
26
30
|
|
|
31
|
+
const READY_TIMEOUT_MS = 5_000;
|
|
32
|
+
|
|
27
33
|
function printHelp(): void {
|
|
28
34
|
console.log("Usage: vellum nginx-ingress <subcommand> [<name>] [options]");
|
|
29
35
|
console.log("");
|
|
@@ -158,71 +164,79 @@ async function assertWebRemoteIngressEnabled(
|
|
|
158
164
|
|
|
159
165
|
async function up(target: NginxIngressTarget): Promise<void> {
|
|
160
166
|
const { workspaceDir, gatewayPort } = target;
|
|
167
|
+
const listenPort = getNginxIngressPort();
|
|
161
168
|
|
|
162
169
|
await assertWebRemoteIngressEnabled(target);
|
|
163
170
|
|
|
164
|
-
const
|
|
171
|
+
const version = getNginxVersion();
|
|
172
|
+
if (!version) {
|
|
173
|
+
console.error("Error: nginx is not installed.");
|
|
174
|
+
console.error("");
|
|
175
|
+
console.error("Install nginx:");
|
|
176
|
+
console.error(" macOS: brew install nginx");
|
|
177
|
+
console.error(" Linux: sudo apt install nginx");
|
|
178
|
+
console.error("");
|
|
179
|
+
console.error(
|
|
180
|
+
"Or point NGINX_BIN at an existing binary: NGINX_BIN=/path/to/nginx",
|
|
181
|
+
);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isIngressRunning(workspaceDir)) {
|
|
186
|
+
console.log("nginx ingress is already running.");
|
|
187
|
+
await status(target);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const webDistDir = findWebDistDir();
|
|
192
|
+
if (!webDistDir) {
|
|
193
|
+
console.error(
|
|
194
|
+
"Error: unable to locate built web assets for remote web ingress.",
|
|
195
|
+
);
|
|
196
|
+
console.error("");
|
|
197
|
+
console.error("Build the SPA first:");
|
|
198
|
+
console.error(" cd clients/web && VITE_PLATFORM_MODE=false bun run build");
|
|
199
|
+
console.error("");
|
|
200
|
+
console.error(
|
|
201
|
+
"Or install @vellumai/web so its packaged dist directory is available.",
|
|
202
|
+
);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(`Using ${version}`);
|
|
207
|
+
console.log(
|
|
208
|
+
`Starting nginx ingress on 127.0.0.1:${listenPort} → web ${webDistDir} + gateway 127.0.0.1:${gatewayPort}...`,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const child = startIngressNginx({
|
|
165
212
|
workspaceDir,
|
|
166
213
|
gatewayPort,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.log(
|
|
170
|
-
`Starting nginx ingress on 127.0.0.1:${listenPort} → web ${webDistDir} + gateway 127.0.0.1:${gatewayPort}...`,
|
|
171
|
-
);
|
|
172
|
-
},
|
|
214
|
+
listenPort,
|
|
215
|
+
remoteWebIngress: { webDistDir },
|
|
173
216
|
});
|
|
217
|
+
child.unref();
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
219
|
+
// /healthz proxies through nginx to the gateway, so a 200 proves the whole
|
|
220
|
+
// ingress → gateway path works.
|
|
221
|
+
const ready = await waitForDaemonReady(listenPort, READY_TIMEOUT_MS);
|
|
222
|
+
if (!ready) {
|
|
223
|
+
const { logPath } = getIngressPaths(workspaceDir);
|
|
224
|
+
await stopIngressNginx(workspaceDir);
|
|
225
|
+
console.error(
|
|
226
|
+
`Error: nginx ingress did not become reachable on 127.0.0.1:${listenPort}.`,
|
|
227
|
+
);
|
|
228
|
+
console.error(`Check the nginx log: ${logPath}`);
|
|
229
|
+
process.exit(1);
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(`nginx ingress running: http://127.0.0.1:${listenPort}`);
|
|
234
|
+
console.log("");
|
|
235
|
+
console.log("Next steps:");
|
|
236
|
+
console.log(
|
|
237
|
+
" vellum tunnel --provider ngrok # tunnel now targets nginx ingress",
|
|
238
|
+
);
|
|
239
|
+
console.log(" vellum nginx-ingress down # stop the proxy");
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
async function down(target: NginxIngressTarget): Promise<void> {
|
package/src/commands/pair.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
buildRemoteWebPairingUrl,
|
|
20
20
|
normalizePublicBaseUrl,
|
|
21
21
|
resolvePublicBaseUrl,
|
|
22
|
-
tunnelProviderWebsiteName,
|
|
23
22
|
type PublicBaseUrlRejection,
|
|
24
23
|
type RemoteWebPairingChallengeRequest,
|
|
25
24
|
type RemoteWebPairingChallengeResponse,
|
|
@@ -47,7 +46,6 @@ import {
|
|
|
47
46
|
} from "../lib/feature-flags.js";
|
|
48
47
|
import { getLocalLanIPv4 } from "../lib/local.js";
|
|
49
48
|
import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
50
|
-
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
51
49
|
|
|
52
50
|
function assistantDisplayName(entry: AssistantEntry): string {
|
|
53
51
|
return entry.name || entry.assistantName || entry.assistantId;
|
|
@@ -279,22 +277,6 @@ export async function pair(): Promise<void> {
|
|
|
279
277
|
);
|
|
280
278
|
args = afterAppScheme;
|
|
281
279
|
|
|
282
|
-
// Any `--`-prefixed token left after known-flag extraction is an option this
|
|
283
|
-
// CLI version doesn't support. Fail loud before any network call rather than
|
|
284
|
-
// letting it fall through: an unknown flag would otherwise be silently
|
|
285
|
-
// dropped and the command would run the wrong flow (e.g. `--qr` on an older
|
|
286
|
-
// CLI minting a copy-paste bundle instead of a QR). Positional names never
|
|
287
|
-
// start with `--`, so multi-word assistant targets are unaffected.
|
|
288
|
-
const unknownFlag = args.find((a) => a.startsWith("--"));
|
|
289
|
-
if (unknownFlag) {
|
|
290
|
-
console.error(`Error: unknown option '${unknownFlag}'.`);
|
|
291
|
-
console.error("Run `vellum pair --help` to see available options.");
|
|
292
|
-
console.error(
|
|
293
|
-
`If this option is from newer docs, ${STALE_CLI_UPDATE_HINT}`,
|
|
294
|
-
);
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
280
|
if (webPairing && webApproveCode) {
|
|
299
281
|
console.error("Error: use either --web or --web-approve, not both.");
|
|
300
282
|
process.exit(1);
|
|
@@ -389,10 +371,6 @@ export async function pair(): Promise<void> {
|
|
|
389
371
|
console.error(
|
|
390
372
|
`Re-run with a reachable URL, e.g.:\n vellum pair --url ${suggestion}`,
|
|
391
373
|
);
|
|
392
|
-
console.error(
|
|
393
|
-
"Pairing a phone by QR instead? Use: vellum pair --qr " +
|
|
394
|
-
"(needs an https URL — run `vellum tunnel --provider tailscale` first).",
|
|
395
|
-
);
|
|
396
374
|
process.exit(1);
|
|
397
375
|
}
|
|
398
376
|
|
|
@@ -473,9 +451,6 @@ export async function pair(): Promise<void> {
|
|
|
473
451
|
unparseable: `${advertisedUrl} isn't a valid URL`,
|
|
474
452
|
loopback: `${advertisedUrl} is a loopback address`,
|
|
475
453
|
"non-https": `${advertisedUrl} is not https`,
|
|
476
|
-
"service-website": `${advertisedUrl} is ${
|
|
477
|
-
tunnelProviderWebsiteName(advertisedUrl) ?? "a tunnel provider"
|
|
478
|
-
}'s website, not your assistant's address`,
|
|
479
454
|
};
|
|
480
455
|
console.error(
|
|
481
456
|
"Error: --qr needs a public https URL the phone can open — " +
|
package/src/commands/tunnel.ts
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
WEB_REMOTE_INGRESS_FLAG,
|
|
9
9
|
} from "../lib/feature-flags.js";
|
|
10
10
|
import { runNgrokTunnel } from "../lib/ngrok";
|
|
11
|
-
import { STALE_CLI_UPDATE_HINT } from "../lib/stale-cli-hint.js";
|
|
12
11
|
import { runTailscaleTunnel } from "../lib/tailscale-tunnel.js";
|
|
13
12
|
|
|
14
13
|
const VALID_PROVIDERS = ["vellum", "ngrok", "cloudflare", "tailscale"] as const;
|
|
@@ -79,21 +78,12 @@ function parseArgs(): TunnelArgs {
|
|
|
79
78
|
process.exit(0);
|
|
80
79
|
} else if (arg === "--provider") {
|
|
81
80
|
const next = args[i + 1];
|
|
82
|
-
if (!next ||
|
|
81
|
+
if (!next || !VALID_PROVIDERS.includes(next as TunnelProvider)) {
|
|
83
82
|
console.error(
|
|
84
83
|
`Error: --provider requires one of: ${VALID_PROVIDERS.join(", ")}`,
|
|
85
84
|
);
|
|
86
85
|
process.exit(1);
|
|
87
86
|
}
|
|
88
|
-
if (!VALID_PROVIDERS.includes(next as TunnelProvider)) {
|
|
89
|
-
console.error(
|
|
90
|
-
`Error: unknown tunnel provider '${next}'. Valid providers: ${VALID_PROVIDERS.join(", ")}.`,
|
|
91
|
-
);
|
|
92
|
-
console.error(
|
|
93
|
-
`If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
|
|
94
|
-
);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
87
|
provider = next as TunnelProvider;
|
|
98
88
|
i++;
|
|
99
89
|
} else if (arg.startsWith("-")) {
|
|
@@ -209,8 +199,5 @@ export async function tunnel(): Promise<void> {
|
|
|
209
199
|
return;
|
|
210
200
|
}
|
|
211
201
|
|
|
212
|
-
throw new Error(
|
|
213
|
-
`Tunnel provider '${provider}' is not yet implemented. ` +
|
|
214
|
-
`If this provider is documented, ${STALE_CLI_UPDATE_HINT}`,
|
|
215
|
-
);
|
|
202
|
+
throw new Error(`Tunnel provider '${provider}' is not yet implemented.`);
|
|
216
203
|
}
|
package/src/commands/wake.ts
CHANGED
|
@@ -30,15 +30,6 @@ import {
|
|
|
30
30
|
startGateway,
|
|
31
31
|
} from "../lib/local";
|
|
32
32
|
import { maybeStartNgrokTunnel } from "../lib/ngrok";
|
|
33
|
-
import {
|
|
34
|
-
isAssistantFeatureFlagEnabled,
|
|
35
|
-
WEB_REMOTE_INGRESS_FLAG,
|
|
36
|
-
} from "../lib/feature-flags.js";
|
|
37
|
-
import { loadRawConfig } from "../lib/ingress-config.js";
|
|
38
|
-
import {
|
|
39
|
-
isIngressRunning,
|
|
40
|
-
startRemoteWebIngress,
|
|
41
|
-
} from "../lib/nginx-ingress.js";
|
|
42
33
|
|
|
43
34
|
export async function wake(): Promise<void> {
|
|
44
35
|
const args = process.argv.slice(3);
|
|
@@ -388,17 +379,6 @@ export async function wake(): Promise<void> {
|
|
|
388
379
|
writeFileSync(ngrokPidFile, String(ngrokChild.pid));
|
|
389
380
|
}
|
|
390
381
|
|
|
391
|
-
// Restore the nginx web ingress edge when the workspace config still wants
|
|
392
|
-
// it. A TLS-terminating front (tailscale serve / tunnel) persists across
|
|
393
|
-
// restarts and keeps proxying to the edge's loopback port, but the edge has
|
|
394
|
-
// a manual lifecycle — so a routine restart otherwise leaves the self-hosted
|
|
395
|
-
// remote-web path dead (502 / blank page) until someone runs it back up.
|
|
396
|
-
await restoreWebIngressIfEnabled(
|
|
397
|
-
entry.assistantId,
|
|
398
|
-
resources.gatewayPort,
|
|
399
|
-
workspaceDir,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
382
|
if (daemonMigrationsFailed) {
|
|
403
383
|
console.log(
|
|
404
384
|
"Assistant database migrations FAILED — DB-backed routes will return 503 until the assistant is restarted. Check the daemon logs.",
|
|
@@ -424,101 +404,3 @@ export async function wake(): Promise<void> {
|
|
|
424
404
|
});
|
|
425
405
|
}
|
|
426
406
|
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Bring the nginx web ingress edge back up after a wake when the workspace
|
|
430
|
-
* config still wants it. Only restores when ingress is explicitly enabled with
|
|
431
|
-
* a saved public URL and the `web-remote-ingress` flag is on — the edge is
|
|
432
|
-
* pointless without the flag, so a disabled flag skips quietly with a hint.
|
|
433
|
-
*
|
|
434
|
-
* Reads the same workspace config the edge serves. Any failure to restore
|
|
435
|
-
* warns with the manual `vellum nginx-ingress up` command and never fails the
|
|
436
|
-
* wake — a down edge is a degraded remote-web path, not a broken assistant.
|
|
437
|
-
*/
|
|
438
|
-
async function restoreWebIngressIfEnabled(
|
|
439
|
-
assistantId: string,
|
|
440
|
-
gatewayPort: number,
|
|
441
|
-
workspaceDir: string,
|
|
442
|
-
): Promise<void> {
|
|
443
|
-
const config = loadRawConfig(workspaceDir);
|
|
444
|
-
const ingress = config.ingress as
|
|
445
|
-
| { enabled?: unknown; publicBaseUrl?: unknown }
|
|
446
|
-
| undefined;
|
|
447
|
-
const enabled = ingress?.enabled === true;
|
|
448
|
-
const publicBaseUrl =
|
|
449
|
-
typeof ingress?.publicBaseUrl === "string"
|
|
450
|
-
? ingress.publicBaseUrl.trim()
|
|
451
|
-
: "";
|
|
452
|
-
if (!enabled || !publicBaseUrl) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// The edge already survived (or was manually brought back) — nothing to do.
|
|
457
|
-
if (isIngressRunning(workspaceDir)) {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
let flagEnabled: boolean;
|
|
462
|
-
try {
|
|
463
|
-
flagEnabled = await isAssistantFeatureFlagEnabled(
|
|
464
|
-
assistantId,
|
|
465
|
-
WEB_REMOTE_INGRESS_FLAG,
|
|
466
|
-
{ runtimeUrl: `http://127.0.0.1:${gatewayPort}` },
|
|
467
|
-
);
|
|
468
|
-
} catch (err) {
|
|
469
|
-
console.warn(
|
|
470
|
-
` Could not verify the \`${WEB_REMOTE_INGRESS_FLAG}\` flag to restore the web ingress edge; leaving it down. Bring it up manually with \`vellum nginx-ingress up\`. ${
|
|
471
|
-
err instanceof Error ? err.message : String(err)
|
|
472
|
-
}`,
|
|
473
|
-
);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (!flagEnabled) {
|
|
477
|
-
console.log(
|
|
478
|
-
` Web ingress edge not restored: the \`${WEB_REMOTE_INGRESS_FLAG}\` flag is off. Enable it and run \`vellum nginx-ingress up\` to serve remote web access.`,
|
|
479
|
-
);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
try {
|
|
484
|
-
const result = await startRemoteWebIngress({
|
|
485
|
-
workspaceDir,
|
|
486
|
-
gatewayPort,
|
|
487
|
-
onStarting: ({ listenPort }) => {
|
|
488
|
-
console.log(
|
|
489
|
-
`Restoring web ingress edge on 127.0.0.1:${listenPort} (ingress.enabled)...`,
|
|
490
|
-
);
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
switch (result.status) {
|
|
494
|
-
case "started":
|
|
495
|
-
console.log(
|
|
496
|
-
` Web ingress edge running: http://127.0.0.1:${result.listenPort}`,
|
|
497
|
-
);
|
|
498
|
-
break;
|
|
499
|
-
case "already-running":
|
|
500
|
-
break;
|
|
501
|
-
case "nginx-missing":
|
|
502
|
-
console.warn(
|
|
503
|
-
" Could not restore the web ingress edge: nginx is not installed. Bring it up manually with `vellum nginx-ingress up`.",
|
|
504
|
-
);
|
|
505
|
-
break;
|
|
506
|
-
case "web-dist-missing":
|
|
507
|
-
console.warn(
|
|
508
|
-
" Could not restore the web ingress edge: built web assets were not found. Bring it up manually with `vellum nginx-ingress up`.",
|
|
509
|
-
);
|
|
510
|
-
break;
|
|
511
|
-
case "unreachable":
|
|
512
|
-
console.warn(
|
|
513
|
-
` Web ingress edge did not become reachable on 127.0.0.1:${result.listenPort}; check ${result.logPath}. Bring it up manually with \`vellum nginx-ingress up\`.`,
|
|
514
|
-
);
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
} catch (err) {
|
|
518
|
-
console.warn(
|
|
519
|
-
` Failed to restore the web ingress edge: ${
|
|
520
|
-
err instanceof Error ? err.message : String(err)
|
|
521
|
-
}. Bring it up manually with \`vellum nginx-ingress up\`.`,
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from "@vellumai/local-mode/contract";
|
|
19
19
|
|
|
20
20
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./constants.js";
|
|
21
|
-
import { crossEnvironmentAssistantHint } from "./environments/detect.js";
|
|
22
21
|
import {
|
|
23
22
|
getDefaultPorts,
|
|
24
23
|
getLockfilePath,
|
|
@@ -424,7 +423,7 @@ export function formatAssistantLookupError(
|
|
|
424
423
|
return `Multiple assistants match '${identifier}': ${matches}. Use the assistant ID to disambiguate.`;
|
|
425
424
|
}
|
|
426
425
|
|
|
427
|
-
return `No assistant found with name or ID '${identifier}'
|
|
426
|
+
return `No assistant found with name or ID '${identifier}'.`;
|
|
428
427
|
}
|
|
429
428
|
|
|
430
429
|
export function removeAssistantEntry(assistantId: string): void {
|
|
@@ -581,9 +580,7 @@ export function resolveTargetAssistant(nameArg?: string): AssistantEntry {
|
|
|
581
580
|
if (all.length === 1) return all[0];
|
|
582
581
|
|
|
583
582
|
if (all.length === 0) {
|
|
584
|
-
console.error(
|
|
585
|
-
`No assistant found. Run 'vellum hatch' first.${crossEnvironmentAssistantHint() ?? ""}`,
|
|
586
|
-
);
|
|
583
|
+
console.error("No assistant found. Run 'vellum hatch' first.");
|
|
587
584
|
} else {
|
|
588
585
|
console.error(
|
|
589
586
|
`Multiple assistants found. Set an active assistant with 'vellum use <name>'.`,
|