@vellumai/cli 0.10.2-dev.202606241743.799fce7 → 0.10.2-dev.202606241938.e233a07
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 +1 -1
- package/src/__tests__/client-token.test.ts +27 -0
- package/src/commands/client.ts +48 -3
- package/src/commands/login.ts +1 -19
- package/src/lib/open-browser.ts +20 -0
package/package.json
CHANGED
|
@@ -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
|
});
|
package/src/commands/client.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
1149
|
+
await runWebInterface(
|
|
1150
|
+
flagEnvVars,
|
|
1151
|
+
parsedFlagOverrides,
|
|
1152
|
+
disablePlatform,
|
|
1153
|
+
openInBrowser,
|
|
1154
|
+
);
|
|
1110
1155
|
return;
|
|
1111
1156
|
}
|
|
1112
1157
|
|
package/src/commands/login.ts
CHANGED
|
@@ -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
|
+
}
|