cligentic
← back to catalog
cligentic block

detect

Environment detection helpers: isWsl, isCi, isHeadlessLinux, hasCommand, detectMode, shouldColor. Shared across platform and agent blocks. Auto-installed as dependency.

Install
myapp doctor
NPM dependencies

zero deps — pure TS

Block dependencies

standalone

Size

100 LOC

What it does

detect is the shared detection layer that all platform and agent blocks import from. Instead of each block duplicating environment checks, they all import from this single file. One fix, all blocks benefit.

This block is auto-installed when you add any platform block (open-url, copy-clipboard, notify-os) or agent block (json-mode, next-steps).

API

import {
  currentPlatform,   // "darwin" | "win32" | "wsl" | "linux" | "unknown"
  isWindows,         // process.platform === "win32"
  isCi,              // CI, GITHUB_ACTIONS, GITLAB_CI, etc.
  isHeadlessLinux,   // no DISPLAY, no WAYLAND_DISPLAY, not WSL
  hasCommand,        // checks if a binary exists in PATH (cross-OS)
  detectMode,        // "human" | "json" based on --json flag / TTY / pipe
  shouldColor,       // respects NO_COLOR and FORCE_COLOR
} from "@/cli/platform/detect";

Why a separate block

  • DRY: WSL detection, CI detection, and mode detection were duplicated across blocks. One bug fix had to land in multiple places.
  • Cache: WSL detection reads /proc/version. The result is cached per-process. Without sharing, each block reads the file independently.
  • Composable: your own commands can import detect too. Need isCi() in your deploy guard? Import it.

currentPlatform() vs process.platform

process.platform returns "linux" for both native Linux and WSL. currentPlatform() distinguishes WSL as its own value because the behavior difference (routing through Windows host binaries) is significant enough to warrant a distinct branch in CLI code.

const os = currentPlatform();
// "darwin" | "win32" | "wsl" | "linux" | "unknown"
 
if (os === "wsl") {
  // route through cmd.exe or wslview
}

hasCommand(cmd)

Walks PATH, checks existsSync for each candidate. On Windows, automatically appends .exe, .cmd, .bat extensions.

if (hasCommand("docker")) {
  // docker is available
}

detectMode(opts)

Returns "human" or "json". Precedence: explicit --json flag > NO_JSON env > TTY detection > default human.

Used by json-mode and next-steps blocks for consistent mode resolution.

Source

The full file, 100 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/detect.ts
// cligentic block: detect
//
// Environment detection helpers shared across platform blocks.
// Detects WSL, CI, headless environments, and binary availability.
//
// Usage:
//   import { isWsl, isCi, hasCommand } from "./platform/detect";

import { existsSync, readFileSync } from "node:fs";
import { platform } from "node:os";

/**
 * Detects WSL (Windows Subsystem for Linux). Reads /proc/version once
 * and caches the result for the lifetime of the process.
 */
let wslCache: boolean | null = null;
export function isWsl(): boolean {
  if (wslCache !== null) return wslCache;
  if (platform() !== "linux") {
    wslCache = false;
    return false;
  }
  try {
    const version = readFileSync("/proc/version", "utf8").toLowerCase();
    wslCache = version.includes("microsoft") || version.includes("wsl");
  } catch {
    wslCache = false;
  }
  return wslCache;
}

/**
 * Detects CI environments. Checks standard CI env vars.
 */
export function isCi(): boolean {
  return Boolean(
    process.env.CI ||
      process.env.CONTINUOUS_INTEGRATION ||
      process.env.GITHUB_ACTIONS ||
      process.env.GITLAB_CI ||
      process.env.CIRCLECI ||
      process.env.BUILDKITE,
  );
}

/**
 * Detects headless Linux (no graphical display).
 * WSL is NOT headless because it can open browsers on the Windows host.
 */
export function isHeadlessLinux(): boolean {
  if (platform() !== "linux") return false;
  if (isWsl()) return false;
  return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
}

/**
 * Checks if a command exists in PATH. Handles Windows .exe/.cmd/.bat
 * extensions automatically.
 */
export function hasCommand(cmd: string): boolean {
  const separator = platform() === "win32" ? ";" : ":";
  const paths = (process.env.PATH || "").split(separator);
  const exts = platform() === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
  for (const p of paths) {
    for (const ext of exts) {
      if (existsSync(`${p}/${cmd}${ext}`)) return true;
    }
  }
  return false;
}

// --- Output mode detection (shared by json-mode + next-steps) ---

export type OutputMode = "human" | "json";

export type EmitOptions = {
  json?: boolean;
  quiet?: boolean;
};

/**
 * Detects whether the CLI should emit structured JSON or human output.
 * Precedence: explicit --json flag > NO_JSON env > TTY detection > default human.
 */
export function detectMode(opts: EmitOptions = {}): OutputMode {
  if (opts.json === true) return "json";
  if (process.env.NO_JSON === "1") return "human";
  if (!process.stdout.isTTY) return "json";
  return "human";
}

/**
 * Detects whether colors should be used. Respects NO_COLOR and FORCE_COLOR.
 */
export function shouldColor(): boolean {
  if (process.env.NO_COLOR) return false;
  if (process.env.FORCE_COLOR) return true;
  return Boolean(process.stdout.isTTY);
}