cligentic
← back to catalog
cligentic block

open-url

Open a URL in the user's default browser across macOS, Linux, Windows, WSL, SSH, and headless CI. Respects BROWSER env var. Fallback chain with manual print. Never throws.

Install
open-url demo
NPM dependencies

zero deps — pure TS

Block dependencies
Size

127 LOC

The problem

Every CLI that does OAuth, opens docs, or launches a local server eventually needs to open a URL in the user's browser. Every CLI writes this badly.

const cmd =
  process.platform === "darwin"
    ? "open"
    : process.platform === "win32"
      ? "start"
      : "xdg-open";
spawn(cmd, [url]);

This fails in at least five ways:

  • WSL - xdg-open doesn't exist; you need wslview or cmd.exe
  • SSH sessions - no DISPLAY, xdg-open hangs or errors silently
  • Headless CI - no browser at all, but your command still "succeeds"
  • BROWSER env var - the Unix convention to override the default is ignored
  • Spaces or & in URLs - unquoted on Windows, your shell eats them

The open-url block handles all five. It returns a typed verdict instead of throwing, so you decide what to do when opening fails.

Quickstart

import { openUrl } from "@/cli/platform/open-url";
 
const result = await openUrl("https://cligentic.railly.dev");
 
if (!result.opened) {
  console.log("Open this URL manually:", result.url);
}

The fallback chain

  1. BROWSER env var - if set and not "none", spawn whatever the user picked
  2. Platform-specific opener - open on macOS, powershell Start-Process on Windows
  3. WSL detection - reads /proc/version for "microsoft"; routes through wslview or cmd.exe /c start
  4. Linux chain - xdg-open, gio open, sensible-browser, firefox, google-chrome, chromium
  5. Headless detection - no DISPLAY, no WAYLAND_DISPLAY, or CI=1 - returns manual verdict

Signature

export type OpenUrlResult = {
  url: string;
  opened: boolean;
  via: "browser-env" | "darwin" | "wsl" | "linux" | "windows" | "manual";
  reason?: string;
};
 
export function openUrl(
  url: string,
  options?: { dryRun?: boolean; manualOnly?: boolean },
): Promise<OpenUrlResult>;

Never throws, never exits

openUrl returns a verdict, it never crashes your CLI. Invalid URLs, missing binaries, headless env, CI - all return { opened: false, via: "manual" } with a human-readable reason.

Source

The full file, 127 lines.

This is the exact TypeScript that lands in your project when you run the install command. Read it, copy it, edit it, own it. cligentic never touches it again.

src/cli/platform/open-url.ts
// cligentic block: open-url
//
// Opens a URL in the user's default browser, across macOS, Linux, Windows,
// WSL, SSH sessions, and headless CI environments.
//
// Design rules:
//   1. Respect BROWSER env var first (Unix convention).
//   2. Detect WSL and route through wslview / cmd.exe.
//   3. Detect headless (no DISPLAY on Linux, SSH without X forwarding).
//   4. Fall back to printing the URL and letting the user click it.
//   5. Never throw. Returns a verdict so callers can decide what to do.
//   6. Never block. Spawn detached so long-lived CLIs don't hang.
//
// Usage:
//   import { openUrl } from "./platform/open-url";
//
//   const result = await openUrl("https://cligentic.railly.dev");
//   if (!result.opened) {
//     console.log("Please open this URL manually:", result.url);
//   }

import { spawn } from "node:child_process";
import { platform } from "node:os";
import { hasCommand, isCi, isHeadlessLinux, isWsl } from "./detect";

export type OpenUrlResult = {
  url: string;
  opened: boolean;
  via: "browser-env" | "darwin" | "wsl" | "linux" | "windows" | "manual";
  reason?: string;
};

export type OpenUrlOptions = {
  dryRun?: boolean;
  manualOnly?: boolean;
};

function spawnDetached(cmd: string, args: string[]): void {
  const child = spawn(cmd, args, {
    detached: true,
    stdio: "ignore",
    shell: false,
  });
  child.unref();
}

export async function openUrl(
  url: string,
  options: OpenUrlOptions = {},
): Promise<OpenUrlResult> {
  const { dryRun = false, manualOnly = false } = options;

  if (manualOnly || isCi() || isHeadlessLinux()) {
    return {
      url,
      opened: false,
      via: "manual",
      reason: manualOnly
        ? "manualOnly flag set"
        : isCi()
          ? "CI environment detected"
          : "headless Linux (no DISPLAY / WAYLAND_DISPLAY)",
    };
  }

  const browserEnv = process.env.BROWSER;
  if (browserEnv && browserEnv !== "none") {
    if (dryRun) return { url, opened: true, via: "browser-env", reason: `would run: ${browserEnv} ${url}` };
    try {
      spawnDetached(browserEnv, [url]);
      return { url, opened: true, via: "browser-env" };
    } catch {
      // fall through
    }
  }

  const os = platform();

  if (os === "darwin") {
    if (dryRun) return { url, opened: true, via: "darwin", reason: `would run: open ${url}` };
    try {
      spawnDetached("open", [url]);
      return { url, opened: true, via: "darwin" };
    } catch (err) {
      return { url, opened: false, via: "manual", reason: `open failed: ${(err as Error).message}` };
    }
  }

  if (os === "win32") {
    if (dryRun) return { url, opened: true, via: "windows", reason: "would run: powershell Start-Process" };
    try {
      spawnDetached("powershell.exe", ["-NoProfile", "-Command", `Start-Process "${url}"`]);
      return { url, opened: true, via: "windows" };
    } catch (err) {
      return { url, opened: false, via: "manual", reason: `powershell failed: ${(err as Error).message}` };
    }
  }

  if (os === "linux" && isWsl()) {
    if (hasCommand("wslview")) {
      if (dryRun) return { url, opened: true, via: "wsl", reason: "would run: wslview" };
      try { spawnDetached("wslview", [url]); return { url, opened: true, via: "wsl" }; } catch { /* fall through */ }
    }
    if (hasCommand("cmd.exe")) {
      if (dryRun) return { url, opened: true, via: "wsl", reason: "would run: cmd.exe /c start" };
      try { spawnDetached("cmd.exe", ["/c", "start", "", url]); return { url, opened: true, via: "wsl" }; } catch (err) {
        return { url, opened: false, via: "manual", reason: `wsl cmd.exe failed: ${(err as Error).message}` };
      }
    }
    return { url, opened: false, via: "manual", reason: "WSL without wslview or cmd.exe" };
  }

  const candidates = ["xdg-open", "gio", "sensible-browser", "firefox", "google-chrome", "chromium"];
  for (const cmd of candidates) {
    if (hasCommand(cmd)) {
      if (dryRun) return { url, opened: true, via: "linux", reason: `would run: ${cmd} ${url}` };
      try {
        const args = cmd === "gio" ? ["open", url] : [url];
        spawnDetached(cmd, args);
        return { url, opened: true, via: "linux" };
      } catch { /* try next */ }
    }
  }

  return { url, opened: false, via: "manual", reason: "no known browser opener found on PATH" };
}