@vellumai/cli 0.10.2-dev.202606241843.6f7158f → 0.10.2-dev.202606242023.5e459ba

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.2-dev.202606241843.6f7158f",
3
+ "version": "0.10.2-dev.202606242023.5e459ba",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -84,4 +84,31 @@ describe("client --token (ephemeral)", () => {
84
84
  expect(parsed.assistantId).toBe("remote-xyz");
85
85
  expect(parsed.bearerToken).toBe("tok");
86
86
  });
87
+
88
+ test("auto-opens the browser by default", () => {
89
+ process.argv = [
90
+ "bun",
91
+ "vellum",
92
+ "client",
93
+ "--url",
94
+ REMOTE_URL,
95
+ "--token",
96
+ "tok",
97
+ ];
98
+ expect(parseArgs().openBrowser).toBe(true);
99
+ });
100
+
101
+ test("--no-open opts out of auto-opening the browser", () => {
102
+ process.argv = [
103
+ "bun",
104
+ "vellum",
105
+ "client",
106
+ "--url",
107
+ REMOTE_URL,
108
+ "--token",
109
+ "tok",
110
+ "--no-open",
111
+ ];
112
+ expect(parseArgs().openBrowser).toBe(false);
113
+ });
87
114
  });
@@ -60,6 +60,7 @@ import {
60
60
  import { tuiLog } from "../lib/tui-log";
61
61
  import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
62
62
  import { probePort } from "../lib/port-probe.js";
63
+ import { openBrowser } from "../lib/open-browser";
63
64
 
64
65
  const SUPPORTED_INTERFACES = ["cli", "web"] as const;
65
66
  type SupportedInterface = (typeof SUPPORTED_INTERFACES)[number];
@@ -90,6 +91,8 @@ interface ParsedArgs {
90
91
  /** Parsed --flag overrides: kebab-case key -> typed value (for web injection). */
91
92
  parsedFlagOverrides: Record<string, boolean | string>;
92
93
  disablePlatform: boolean;
94
+ /** Auto-open the web interface in the default browser (--interface web only). */
95
+ openBrowser: boolean;
93
96
  }
94
97
 
95
98
  function readAssistantName(entry: AssistantEntry | null): string | undefined {
@@ -136,6 +139,8 @@ export function parseArgs(): ParsedArgs {
136
139
  "--token",
137
140
  "-t",
138
141
  ]);
142
+ // Auto-open the web interface in the browser by default; --no-open opts out.
143
+ let openBrowserPref = true;
139
144
  const flagArgs: string[] = [];
140
145
  for (let i = 0; i < args.length; i++) {
141
146
  const arg = args[i];
@@ -144,6 +149,8 @@ export function parseArgs(): ParsedArgs {
144
149
  process.exit(0);
145
150
  } else if (arg === "--disable-platform") {
146
151
  disablePlatform = true;
152
+ } else if (arg === "--no-open") {
153
+ openBrowserPref = false;
147
154
  } else if (
148
155
  (arg === "--url" ||
149
156
  arg === "-u" ||
@@ -264,6 +271,7 @@ export function parseArgs(): ParsedArgs {
264
271
  flagEnvVars,
265
272
  parsedFlagOverrides,
266
273
  disablePlatform,
274
+ openBrowser: openBrowserPref,
267
275
  };
268
276
  }
269
277
 
@@ -283,6 +291,7 @@ ${ANSI.bold}OPTIONS:${ANSI.reset}
283
291
  not persisted.
284
292
  -a, --assistant-id <id> Assistant ID
285
293
  -i, --interface <id> Interface identifier: cli (default) or web
294
+ --no-open Don't auto-open the browser (--interface web)
286
295
  --flag <key=value> Feature flag override (repeatable, kebab-case key)
287
296
  --disable-platform Suppress all outbound platform API calls
288
297
  -h, --help Show this help message
@@ -816,10 +825,26 @@ async function findFreeDualLoopbackPort(preferred: number): Promise<number> {
816
825
  return preferred;
817
826
  }
818
827
 
828
+ /**
829
+ * Open `url` in the browser once `port` is accepting connections, polling for
830
+ * up to ~10s. Used for the Vite dev server, which binds the port asynchronously
831
+ * after spawn — opening immediately would load the tab before Vite is ready.
832
+ */
833
+ async function openBrowserWhenReady(url: string, port: number): Promise<void> {
834
+ for (let attempt = 0; attempt < 50; attempt++) {
835
+ if (await probePort(port, "127.0.0.1")) {
836
+ openBrowser(url);
837
+ return;
838
+ }
839
+ await new Promise((resolve) => setTimeout(resolve, 200));
840
+ }
841
+ }
842
+
819
843
  async function runWebInterface(
820
844
  flagEnvVars: Record<string, string>,
821
845
  parsedFlagOverrides: Record<string, boolean | string>,
822
846
  disablePlatform: boolean,
847
+ openInBrowser: boolean,
823
848
  ): Promise<void> {
824
849
  // Propagate flag env vars so child processes (e.g. hatch from the web UI) inherit them.
825
850
  Object.assign(process.env, flagEnvVars);
@@ -828,7 +853,12 @@ async function runWebInterface(
828
853
  // (HMR, __local endpoints, gateway proxy).
829
854
  const webSourceDir = findWebSourceDir();
830
855
  if (webSourceDir) {
831
- return runViteDevServer(webSourceDir, flagEnvVars, disablePlatform);
856
+ return runViteDevServer(
857
+ webSourceDir,
858
+ flagEnvVars,
859
+ disablePlatform,
860
+ openInBrowser,
861
+ );
832
862
  }
833
863
 
834
864
  const distDir = findWebDistDir();
@@ -978,7 +1008,9 @@ async function runWebInterface(
978
1008
  // Advertise `localhost` (not `127.0.0.1`) so the app origin matches the host
979
1009
  // the platform hardcodes in its loopback callback. We bind both loopback
980
1010
  // families above so `localhost` reaches us whichever one it resolves to.
981
- console.log(`Vellum web interface: http://localhost:${port}${SPA_BASE}`);
1011
+ const webInterfaceUrl = `http://localhost:${port}${SPA_BASE}`;
1012
+ console.log(`Vellum web interface: ${webInterfaceUrl}`);
1013
+ if (openInBrowser) openBrowser(webInterfaceUrl);
982
1014
 
983
1015
  const shutdown = (): void => {
984
1016
  for (const server of servers) server.stop();
@@ -994,6 +1026,7 @@ async function runViteDevServer(
994
1026
  webSourceDir: string,
995
1027
  flagEnvVars: Record<string, string>,
996
1028
  disablePlatform: boolean,
1029
+ openInBrowser: boolean,
997
1030
  ): Promise<void> {
998
1031
  const platformUrl = getPlatformUrl();
999
1032
 
@@ -1027,6 +1060,12 @@ async function runViteDevServer(
1027
1060
  },
1028
1061
  });
1029
1062
 
1063
+ // Vite binds the port itself, so wait until it's listening before opening the
1064
+ // browser — otherwise the tab loads before the dev server is ready.
1065
+ if (openInBrowser) {
1066
+ void openBrowserWhenReady(`http://localhost:${port}${SPA_BASE}`, port);
1067
+ }
1068
+
1030
1069
  const shutdown = (): void => {
1031
1070
  child.kill();
1032
1071
  process.exit(0);
@@ -1099,6 +1138,7 @@ export async function client(): Promise<void> {
1099
1138
  flagEnvVars,
1100
1139
  parsedFlagOverrides,
1101
1140
  disablePlatform,
1141
+ openBrowser: openInBrowser,
1102
1142
  } = parseArgs();
1103
1143
 
1104
1144
  if (disablePlatform) {
@@ -1106,7 +1146,12 @@ export async function client(): Promise<void> {
1106
1146
  }
1107
1147
 
1108
1148
  if (interfaceId === WEB_INTERFACE_ID) {
1109
- await runWebInterface(flagEnvVars, parsedFlagOverrides, disablePlatform);
1149
+ await runWebInterface(
1150
+ flagEnvVars,
1151
+ parsedFlagOverrides,
1152
+ disablePlatform,
1153
+ openInBrowser,
1154
+ );
1110
1155
  return;
1111
1156
  }
1112
1157
 
@@ -1,4 +1,3 @@
1
- import { spawn } from "child_process";
2
1
  import { randomBytes } from "crypto";
3
2
  import { createServer } from "http";
4
3
  import type { AddressInfo } from "net";
@@ -11,6 +10,7 @@ import {
11
10
  setActiveAssistant,
12
11
  } from "../lib/assistant-config";
13
12
  import { computeDeviceId } from "../lib/guardian-token";
13
+ import { openBrowser } from "../lib/open-browser";
14
14
  import {
15
15
  clearPlatformToken,
16
16
  ensureSelfHostedLocalRegistration,
@@ -169,24 +169,6 @@ function renderLoginPage(
169
169
  </html>`;
170
170
  }
171
171
 
172
- /**
173
- * Open a URL in the user's default browser.
174
- */
175
- function openBrowser(url: string): void {
176
- const platform = process.platform;
177
- const cmd =
178
- platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
179
- const args =
180
- platform === "win32"
181
- ? ["/c", "start", '""', url.replace(/&/g, "^&")]
182
- : [url];
183
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
184
- child.on("error", () => {
185
- // Silently ignore — the user can still copy the URL from the console
186
- });
187
- child.unref();
188
- }
189
-
190
172
  export interface LoopbackListener {
191
173
  /** The full `http://127.0.0.1:<port>/auth/callback` redirect URI. */
192
174
  redirectUri: string;
@@ -0,0 +1,20 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ /**
4
+ * Open a URL in the user's default browser. Best-effort: a failure to launch is
5
+ * swallowed so the caller can still surface the URL for the user to copy.
6
+ */
7
+ export function openBrowser(url: string): void {
8
+ const platform = process.platform;
9
+ const cmd =
10
+ platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
11
+ const args =
12
+ platform === "win32"
13
+ ? ["/c", "start", '""', url.replace(/&/g, "^&")]
14
+ : [url];
15
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
16
+ child.on("error", () => {
17
+ // Silently ignore — the user can still copy the URL from the console.
18
+ });
19
+ child.unref();
20
+ }