From 946da2dd8af0b1952b1f9f0aaa3243858732e2ad Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Fri, 21 Aug 2026 04:22:07 +0000 Subject: [PATCH 1/5] fix(auth): open a browser on Windows instead of a console window `hivemind login` on Windows opened a new PowerShell window and never reached the sign-in page. Reported from a real Windows box (10.0.26100) while verifying the new PowerShell installer: the install succeeded, then login dead-ended. auth.ts built its own shell string: process.platform === "win32" ? `start "${url}"` : ... execSync(cmd, ...) Under cmd.exe, `start` treats its first quoted argument as the WINDOW TITLE. So `start "https://..."` opens a console titled with the URL and opens no browser. dashboard/open.ts already documents this exact trap and passes the empty title that fixes it - auth.ts had a second, divergent copy that never got the fix. It also could not report the failure honestly: `start ""` succeeds, so execSync did not throw, so openBrowser returned true and the CLI printed "Browser opened. Waiting for sign in..." to someone looking at a shell. Fixed at the source rather than patched in place: auth.ts now calls openInBrowser, which passes `cmd /c start "" `, pre-checks the helper is on PATH, and reports what it actually attempted. The regression test asserts both halves. A unit test of openCommandFor alone could not have caught this, since the defect was a caller keeping its own copy - so it also asserts auth.ts routes through the shared helper. Verified to fail on origin/main: 3 of 3 assertions. Why it matters beyond the error: on Windows the install now succeeds and the funnel then loses the user at login, which reads as a drop-off rather than a bug. Found while closing exactly that measurement gap in activeloopai/deeplake-ui#352. --- src/commands/auth.ts | 27 ++++++++++------ tests/cli/auth-open-browser-windows.test.ts | 35 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 tests/cli/auth-open-browser-windows.test.ts 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..159887593 --- /dev/null +++ b/tests/cli/auth-open-browser-windows.test.ts @@ -0,0 +1,35 @@ +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", () => { + expect(AUTH_SRC).toContain("openInBrowser"); + expect(AUTH_SRC).not.toMatch(/start\s+"\$\{url\}"/); + expect(AUTH_SRC).not.toMatch(/execSync\(\s*cmd/); + }); +}); From b46d6de8d7fbdb17fa08e3217f8ebd0c08b7aac6 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Fri, 21 Aug 2026 06:59:54 +0000 Subject: [PATCH 2/5] test(auth): assert the delegation call, not the bare name CodeRabbit caught that `expect(AUTH_SRC).toContain("openInBrowser")` passed for the wrong reason: the comment in auth.ts explaining this bug mentions the helper, so the assertion held on a file that only talked about it. The test looked stronger than it was. Now matches the import and the call site. Demonstrated on a file that mentions the helper in a comment and is otherwise the original bug: loose toContain("openInBrowser"): PASSES (the hole) FAIL import present FAIL call present --- tests/cli/auth-open-browser-windows.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/cli/auth-open-browser-windows.test.ts b/tests/cli/auth-open-browser-windows.test.ts index 159887593..56b94d890 100644 --- a/tests/cli/auth-open-browser-windows.test.ts +++ b/tests/cli/auth-open-browser-windows.test.ts @@ -28,7 +28,13 @@ describe("device-flow login opens a browser on Windows", () => { // 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", () => { - expect(AUTH_SRC).toContain("openInBrowser"); + // 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/); }); From 19122fdcf1e8108012aac122fa51d3f67807848c Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Fri, 21 Aug 2026 07:17:06 +0000 Subject: [PATCH 3/5] docs(readme): document the curl and PowerShell install routes The README's only documented install was `npm i -g @deeplake/hivemind`, and it did not mention Windows or PowerShell once - so a Windows user reading it had no route at all, and everyone else was pointed past the installer that exists. Adds the two one-liners, keeps npm as the third option for people who already have it. Also documents the shape that is not obvious and fails silently: `iex` evaluates a string and has nowhere to put arguments, so `irm ... | iex -s -- claude install` runs and quietly installs for every assistant instead of the one asked for. The script-block form is the one that works. Ordering: the macOS/Linux line is accurate today. The PowerShell one needs activeloopai/deeplake-ui#352, which adds public/hivemind.ps1, merged and deployed first - until then deeplake.ai/hivemind.ps1 is a 404. --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f090fc5e..608996ac1 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,29 @@ 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 +``` + +`iex` evaluates the downloaded text and has nowhere to put arguments, so +installing for one assistant means building a script block rather than +appending flags: + +```powershell +& ([scriptblock]::Create((irm https://deeplake.ai/hivemind.ps1))) claude install +``` + +**Already have npm** — any platform: ```bash npm i -g @deeplake/hivemind && hivemind install From b1c288f1eb577ea93d0a16d55de9e612243e0441 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Fri, 21 Aug 2026 07:20:06 +0000 Subject: [PATCH 4/5] docs(readme): say when to pick the npm route, not who you are "Already have npm - any platform" describes the reader instead of the choice. The npm route is the right one for CI, Dockerfiles, and shops where policy blocks piping a downloaded script to a shell - and it skips the Node version and prefix-writability checks the installers do, which is worth knowing before you pick it rather than after it fails. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 608996ac1..f97b32a28 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,9 @@ appending flags: & ([scriptblock]::Create((irm https://deeplake.ai/hivemind.ps1))) claude install ``` -**Already have npm** — any platform: +**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 From d4a640c7e4b9ece20faa40fce9e2866d002d1632 Mon Sep 17 00:00:00 2001 From: Emanuele Fenocchi Date: Fri, 21 Aug 2026 07:22:05 +0000 Subject: [PATCH 5/5] docs(readme): drop the PowerShell script-block form from Quick start `& ([scriptblock]::Create((irm ...))) claude install` is the Windows answer to `sh -s -- claude install`, and it is unreadable. It also solves a problem the README answers better twenty lines below: install normally, then run `hivemind claude install`. Two legible commands beat one illegible one. The form still lives where someone actually needs it - the header of hivemind.ps1, readable by anyone who pipes it, and the site's install widget, which generates it correctly when you pick Windows plus a single assistant. --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index f97b32a28..8a6a1b0fd 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,6 @@ curl -fsSL https://deeplake.ai/hivemind.sh | sh irm https://deeplake.ai/hivemind.ps1 | iex ``` -`iex` evaluates the downloaded text and has nowhere to put arguments, so -installing for one assistant means building a script block rather than -appending flags: - -```powershell -& ([scriptblock]::Create((irm https://deeplake.ai/hivemind.ps1))) claude install -``` - **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: