@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.
@@ -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
- });
@@ -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
- `Could not reach the assistant gateway. Is it running? Try 'vellum wake'.${
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
- startRemoteWebIngress,
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 result = await startRemoteWebIngress({
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
- 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
- },
214
+ listenPort,
215
+ remoteWebIngress: { webDistDir },
173
216
  });
217
+ child.unref();
174
218
 
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;
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
- process.exit(1);
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> {
@@ -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 — " +
@@ -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 || next.startsWith("-")) {
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
  }
@@ -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}'.${crossEnvironmentAssistantHint() ?? ""}`;
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>'.`,