diff --git a/README.md b/README.md index 5f090fc5e..8a6a1b0fd 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,23 @@ The agent reaches the answer in fewer turns with less context, because the prior ## Quick start -One command, all your agents: +One command, all your agents. + +**macOS / Linux** + +```bash +curl -fsSL https://deeplake.ai/hivemind.sh | sh +``` + +**Windows** — in PowerShell: + +```powershell +irm https://deeplake.ai/hivemind.ps1 | iex +``` + +**Any platform, via npm** — for CI and Dockerfiles, or where policy blocks +piping a downloaded script to a shell. Skips the checks the installers do, so +Node 22+ and a writable npm prefix are on you: ```bash npm i -g @deeplake/hivemind && hivemind install diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 0b9d09d52..5990c6524 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -3,9 +3,9 @@ * and org/workspace management. */ -import { execSync } from "node:child_process"; import { deeplakeClientHeader } from "../utils/client-header.js"; import { hivemindInstallIDHeader } from "./install-id.js"; +import { openInBrowser } from "../dashboard/open.js"; import { type Credentials, loadCredentials, @@ -148,16 +148,23 @@ export async function pollForToken(deviceCode: string, apiUrl = DEFAULT_API_URL) throw new Error(`Token polling failed: HTTP ${resp.status}`); } +/** + * Opens the device-flow URL, via the shared helper in dashboard/open.ts. + * + * This used to build its own shell string, and got Windows wrong in the one + * way open.ts already documents: `start ""` under cmd.exe treats its + * first quoted argument as the WINDOW TITLE, so it opened a new console + * titled with the URL and never opened a browser. openCommandFor passes the + * empty title (`cmd /c start "" `) that makes the URL an argument. + * + * The old version also could not report that honestly: `start ""` + * *succeeds*, so execSync did not throw, so it returned true and the CLI + * printed "Browser opened. Waiting for sign in..." to someone staring at a + * shell. openInBrowser pre-checks the helper is on PATH and reports what it + * actually attempted. + */ function openBrowser(url: string): boolean { - try { - const cmd = process.platform === "darwin" ? `open "${url}"` - : process.platform === "win32" ? `start "${url}"` - : `xdg-open "${url}" 2>/dev/null`; - execSync(cmd, { stdio: "ignore", timeout: 5000 }); - return true; - } catch { - return false; - } + return openInBrowser(url).attempted; } export async function deviceFlowLogin(apiUrl = DEFAULT_API_URL, ref?: string): Promise<{ token: string; expiresIn: number }> { diff --git a/tests/cli/auth-open-browser-windows.test.ts b/tests/cli/auth-open-browser-windows.test.ts new file mode 100644 index 000000000..56b94d890 --- /dev/null +++ b/tests/cli/auth-open-browser-windows.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { openCommandFor } from "../../src/dashboard/open.js"; + +const AUTH_SRC = readFileSync( + join(import.meta.dirname, "../../src/commands/auth.ts"), + "utf-8", +); + +// The device-flow login opened a new console window instead of a browser on +// Windows, for a reason this repo had already found and written down in +// dashboard/open.ts: under cmd.exe, `start ""` treats its first quoted +// argument as the WINDOW TITLE. auth.ts had its own copy of the open logic and +// never got the fix, so Windows users installed fine and then could not log in +// — a drop-off indistinguishable from someone walking away. +describe("device-flow login opens a browser on Windows", () => { + it("passes the empty title argument that makes the URL an argument", () => { + const { command, args } = openCommandFor("win32", "https://example.com/device?code=ABCD"); + + expect(command).toBe("cmd"); + // The "" is the whole bug. Without it the URL becomes the window title. + expect(args).toEqual(["/c", "start", "", "https://example.com/device?code=ABCD"]); + expect(args[2]).toBe(""); + }); + + // A unit test of openCommandFor cannot catch auth.ts keeping a second, + // divergent implementation — which is exactly how this shipped. Assert the + // caller actually routes through the shared helper. + it("auth.ts delegates to the shared helper instead of building its own command", () => { + // Match the import and the call, not the bare name: the explanation of this + // bug lives in a comment in auth.ts, so a substring check for + // "openInBrowser" would pass on a file that only talks about it. + expect(AUTH_SRC).toMatch( + /import\s*\{\s*openInBrowser\s*\}\s*from\s*["']\.\.\/dashboard\/open\.js["']/, + ); + expect(AUTH_SRC).toMatch(/return\s+openInBrowser\(\s*url\s*\)\s*\.attempted/); + expect(AUTH_SRC).not.toMatch(/start\s+"\$\{url\}"/); + expect(AUTH_SRC).not.toMatch(/execSync\(\s*cmd/); + }); +});