src / container / runtime.ts

/**
 * @file container/runtime.ts
 * Auto-detects Docker or Podman on the host system.
 *
 * Priority: Docker first (most common), then Podman fallback.
 * Caches the result after first successful detection.
 */

import { execFile } from "child_process";
import { promisify } from "util";
import type { RuntimeInfo, RuntimeKind } from "../types";

const execAsync = promisify(execFile);

/** Cached runtime info after first detection. */
let cachedRuntime: RuntimeInfo | null = null;

/**
 * Attempt to detect a specific runtime by running `<cmd> --version`.
 */
async function probe(
  cmd: string,
  kind: RuntimeKind,
): Promise<RuntimeInfo | null> {
  try {
    const { stdout } = await execAsync(cmd, ["--version"], { timeout: 5_000 });
    const version = stdout.trim().split("\n")[0] ?? "unknown";
    return { kind, path: cmd, version };
  } catch {
    return null;
  }
}

/**
 * Returns runtime candidates ordered by priority.
 * On Windows, also probes known Docker Desktop install paths since
 * LM Studio may launch with a restricted PATH that omits Program Files.
 */
function getRuntimeCandidates(): Array<{ cmd: string; kind: RuntimeKind }> {
  const candidates: Array<{ cmd: string; kind: RuntimeKind }> = [
    { cmd: "docker", kind: "docker" },
    { cmd: "podman", kind: "podman" },
  ];
  if (process.platform === "win32") {
    candidates.push(
      {
        cmd: "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe",
        kind: "docker",
      },
      {
        cmd: "C:\\Program Files\\Docker\\Docker\\resources\\docker.exe",
        kind: "docker",
      },
    );
  }
  return candidates;
}

/**
 * Detect the available container runtime.
 * Tries Docker first, then Podman. Caches the result.
 *
 * @throws Error if neither Docker nor Podman is found.
 */
export async function detectRuntime(): Promise<RuntimeInfo> {
  if (cachedRuntime) return cachedRuntime;

  for (const { cmd, kind } of getRuntimeCandidates()) {
    const result = await probe(cmd, kind);
    if (result) {
      cachedRuntime = result;
      return result;
    }
  }

  const isWin = process.platform === "win32";
  throw new Error(
    "No container runtime found. Please install Docker Desktop" +
      (isWin
        ? " from https://docs.docker.com/desktop/setup/install/windows-install/"
        : " (https://docs.docker.com/get-docker/)") +
      " or Podman (https://podman.io/getting-started/installation) to use this plugin.",
  );
}

/**
 * Check if a container runtime is available without throwing.
 */
export async function isRuntimeAvailable(): Promise<boolean> {
  try {
    await detectRuntime();
    return true;
  } catch {
    return false;
  }
}

/**
 * Get the cached runtime, or null if not yet detected.
 */
export function getCachedRuntime(): RuntimeInfo | null {
  return cachedRuntime;
}

/**
 * Clear the cached runtime (useful for testing or re-detection).
 */
export function clearRuntimeCache(): void {
  cachedRuntime = null;
}