/**
 * SF Doctor — Environment Health Checks (#1221)
 *
 * Deterministic checks for environment readiness that prevent the model
 * from spinning its wheels on missing tools, port conflicts, stale
 * dependencies, and other infrastructure issues.
 *
 * These checks complement the existing git/runtime health checks and
 * integrate into the doctor pipeline via checkEnvironmentHealth().
 */
import { execFileSync, execSync } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";

// ── Constants ──────────────────────────────────────────────────────────────
/** Default dev server ports to scan for conflicts. */
const DEFAULT_DEV_PORTS = [3000, 3001, 4000, 5000, 5173, 8000, 8080, 8888];
/** Minimum free disk space in bytes (500MB). */
const MIN_DISK_BYTES = 500 * 1024 * 1024;
/** Timeout for external commands (ms). */
const CMD_TIMEOUT = 5_000;
const INSTALL_TIMEOUT = 10 * 60_000;
// ── Helpers ────────────────────────────────────────────────────────────────
/** Worktree sentinel — path segment that marks an auto-worktree directory. */
const WORKTREE_PATH_SEGMENT = `${join(".sf", "worktrees")}/`;
/**
 * Resolve the project root when running inside a `.sf/worktrees/<name>/`
 * auto-worktree. Returns `null` if not in a worktree.
 *
 * Detection order:
 *   1. `SF_WORKTREE` env var (set by the worktree launcher)
 *   2. `.sf/worktrees/` segment in basePath
 */
function resolveWorktreeProjectRoot(basePath) {
	const envRoot = process.env.SF_WORKTREE;
	if (envRoot) return envRoot;
	const normalised = basePath.replace(/\\/g, "/");
	const idx = normalised.indexOf(WORKTREE_PATH_SEGMENT.replace(/\\/g, "/"));
	if (idx === -1) return null;
	// Everything before `.sf/worktrees/` is the project root
	return basePath.slice(0, idx);
}
function tryExec(cmd, cwd) {
	try {
		return execSync(cmd, {
			cwd,
			timeout: CMD_TIMEOUT,
			stdio: ["ignore", "pipe", "pipe"],
			encoding: "utf-8",
		}).trim();
	} catch {
		return null;
	}
}
function commandExists(name, cwd) {
	const whichCmd =
		process.platform === "win32" ? `where ${name}` : `command -v ${name}`;
	return tryExec(whichCmd, cwd) !== null;
}
function detectNodePackageManager(basePath) {
	if (existsSync(join(basePath, "package-lock.json")))
		return {
			name: "npm",
			args: ["install", "--package-lock=false"],
			command: "npm install --package-lock=false",
		};
	if (existsSync(join(basePath, "pnpm-lock.yaml")))
		return { name: "pnpm", args: ["install"], command: "pnpm install" };
	if (existsSync(join(basePath, "yarn.lock")))
		return { name: "yarn", args: ["install"], command: "yarn install" };
	if (
		existsSync(join(basePath, "bun.lock")) ||
		existsSync(join(basePath, "bun.lockb"))
	)
		return { name: "bun", args: ["install"], command: "bun install" };
	return null;
}
function runNodeInstall(basePath, manager) {
	execFileSync(manager.name, manager.args, {
		cwd: basePath,
		timeout: INSTALL_TIMEOUT,
		stdio: ["ignore", "pipe", "pipe"],
		encoding: "utf-8",
	});
}
// ── Individual Checks ──────────────────────────────────────────────────────
/**
 * Check that the Python package manager declared by lockfile is installed.
 *
 * Detects uv / poetry / pdm by lockfile presence and verifies the binary is
 * on PATH. Surfaces missing-tool early so SF doesn't hand a Python milestone
 * to an agent that will hit "uv: command not found" mid-task.
 *
 * Returns null when the project has no Python signals (not a Python repo).
 */
function checkPythonEnvironment(basePath) {
	const hasPyproject = existsSync(join(basePath, "pyproject.toml"));
	const hasRequirements = existsSync(join(basePath, "requirements.txt"));
	if (!hasPyproject && !hasRequirements) return null;
	const hasUvLock = existsSync(join(basePath, "uv.lock"));
	const hasPoetryLock = existsSync(join(basePath, "poetry.lock"));
	const hasPdmLock = existsSync(join(basePath, "pdm.lock"));
	let manager = null;
	let installHint = "";
	if (hasUvLock) {
		manager = "uv";
		installHint = "Install: curl -LsSf https://astral.sh/uv/install.sh | sh";
	} else if (hasPoetryLock) {
		manager = "poetry";
		installHint =
			"Install: curl -sSL https://install.python-poetry.org | python3 -";
	} else if (hasPdmLock) {
		manager = "pdm";
		installHint =
			"Install: curl -sSL https://pdm-project.org/install-pdm.py | python3 -";
	}
	if (!manager) {
		return {
			name: "python_env",
			status: "ok",
			message: "Python project (no lockfile detected)",
		};
	}
	const version = tryExec(`${manager} --version`, basePath);
	if (!version) {
		return {
			name: "python_env",
			status: "warning",
			message: `${manager} not found in PATH (project uses ${manager}.lock)`,
			detail: installHint,
		};
	}
	return {
		name: "python_env",
		status: "ok",
		message: `Python project (${manager}: ${version})`,
	};
}
/**
 * Recommend installing sift on large repos where code intelligence quality
 * matters most. Non-fatal — sift is optional but significantly improves
 * codebase_search and the code-intelligence context block.
 *
 * Returns null when the repo is small (< 5000 source files) or sift is
 * already on PATH.
 */
function checkSiftAvailable(basePath) {
	let fileCount = 0;
	try {
		// Lazy import — scanProjectFiles walks the filesystem, only do this
		// when called by the doctor pipeline.
		// eslint-disable-next-line @typescript-eslint/no-require-imports
		const { scanProjectFiles } = require("./detection.js");
		fileCount = scanProjectFiles(basePath).length;
	} catch {
		return null;
	}
	const SIFT_RECOMMENDED_THRESHOLD = 5000;
	if (fileCount < SIFT_RECOMMENDED_THRESHOLD) return null;
	if (commandExists("sift", basePath)) {
		return {
			name: "sift_available",
			status: "ok",
			message: `sift on PATH (recommended for ${fileCount}-file repo)`,
		};
	}
	return {
		name: "sift_available",
		status: "warning",
		message: `sift not installed (recommended for repos > ${SIFT_RECOMMENDED_THRESHOLD} files; this repo has ${fileCount})`,
		detail: "Install: cargo install --git https://github.com/rupurt/sift",
	};
}
/**
 * Check that Node.js version meets the project's engines requirement.
 */
function checkNodeVersion(basePath) {
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) return null;
	try {
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		const required = pkg.engines?.node;
		if (!required) return null;
		const currentVersion = tryExec("node --version", basePath);
		if (!currentVersion) {
			return {
				name: "node_version",
				status: "error",
				message: "Node.js not found in PATH",
			};
		}
		// Parse semver requirement (handles >=X.Y.Z format)
		const reqMatch = required.match(/>=?\s*(\d+)(?:\.(\d+))?/);
		if (!reqMatch) return null;
		const reqMajor = parseInt(reqMatch[1], 10);
		const reqMinor = parseInt(reqMatch[2] ?? "0", 10);
		const curMatch = currentVersion.match(/v?(\d+)\.(\d+)/);
		if (!curMatch) return null;
		const curMajor = parseInt(curMatch[1], 10);
		const curMinor = parseInt(curMatch[2], 10);
		if (curMajor < reqMajor || (curMajor === reqMajor && curMinor < reqMinor)) {
			return {
				name: "node_version",
				status: "warning",
				message: `Node.js ${currentVersion} does not meet requirement "${required}"`,
				detail: `Current: ${currentVersion}, Required: ${required}`,
			};
		}
		return {
			name: "node_version",
			status: "ok",
			message: `Node.js ${currentVersion}`,
		};
	} catch {
		return null;
	}
}
/**
 * Check if node_modules exists and is not stale vs the lockfile.
 */
function checkDependenciesInstalled(basePath) {
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) return null;
	const nodeModules = join(basePath, "node_modules");
	if (!existsSync(nodeModules)) {
		// In auto-worktrees node_modules is absent by design — the worktree
		// symlinks to (or expects) the project root's copy.  Fall back to
		// checking the project root before reporting an error (#2303).
		const projectRoot = resolveWorktreeProjectRoot(basePath);
		if (projectRoot && existsSync(join(projectRoot, "node_modules"))) {
			return {
				name: "dependencies",
				status: "ok",
				message: "Dependencies installed (project root)",
			};
		}
		return {
			name: "dependencies",
			status: "error",
			message: "node_modules missing — run npm install",
			fixCode: "env_dependencies",
			fixCommand: detectNodePackageManager(basePath)?.command ?? "npm install",
		};
	}
	// Check if lockfile is newer than the last install.
	//
	// Each package manager writes a metadata marker inside node_modules on
	// every install. Comparing the lockfile mtime against the marker is
	// reliable; comparing against the node_modules *directory* mtime is not,
	// because directory mtime only changes when entries are added or removed
	// — not when files inside it are updated. (#1974)
	const lockfiles = [
		{ lock: "package-lock.json", markers: ["node_modules/.package-lock.json"] },
		{ lock: "yarn.lock", markers: ["node_modules/.yarn-integrity"] },
		{ lock: "pnpm-lock.yaml", markers: ["node_modules/.modules.yaml"] },
	];
	for (const { lock, markers } of lockfiles) {
		const lockPath = join(basePath, lock);
		if (!existsSync(lockPath)) continue;
		try {
			const lockMtime = statSync(lockPath).mtimeMs;
			// Prefer the package manager's marker file; fall back to directory mtime
			// only when no marker exists (e.g., manually created node_modules).
			let installMtime = 0;
			for (const marker of markers) {
				const markerPath = join(basePath, marker);
				if (existsSync(markerPath)) {
					installMtime = Math.max(installMtime, statSync(markerPath).mtimeMs);
				}
			}
			if (installMtime === 0) {
				installMtime = statSync(nodeModules).mtimeMs;
			}
			if (lockMtime > installMtime) {
				return {
					name: "dependencies",
					status: "warning",
					message: `${lock} is newer than node_modules — dependencies may be stale`,
					detail: `Run npm install / yarn / pnpm install to update`,
					fixCode: "env_dependencies",
					fixCommand:
						detectNodePackageManager(basePath)?.command ?? "npm install",
				};
			}
		} catch {
			// stat failed — skip
		}
	}
	return {
		name: "dependencies",
		status: "ok",
		message: "Dependencies installed",
	};
}
/**
 * Check for .env.example files without corresponding .env files.
 */
function checkEnvFiles(basePath) {
	const examplePath = join(basePath, ".env.example");
	if (!existsSync(examplePath)) return null;
	const envPath = join(basePath, ".env");
	const envLocalPath = join(basePath, ".env.local");
	if (!existsSync(envPath) && !existsSync(envLocalPath)) {
		return {
			name: "env_file",
			status: "warning",
			message: ".env.example exists but no .env or .env.local found",
			detail: "Copy .env.example to .env and fill in values",
		};
	}
	return {
		name: "env_file",
		status: "ok",
		message: "Environment file present",
	};
}
/**
 * Check for port conflicts on common dev server ports.
 * Only checks ports that appear in package.json scripts.
 */
function checkPortConflicts(basePath) {
	// Only run on macOS/Linux — lsof is not available on Windows
	if (process.platform === "win32") return [];
	const results = [];
	// Try to detect ports from package.json scripts
	const portsToCheck = new Set();
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) {
		// No package.json — this isn't a Node.js project. Skip port checks
		// entirely to avoid false positives from system services (e.g., macOS
		// AirPlay Receiver on port 5000). (#1381)
		return [];
	}
	try {
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		const scripts = pkg.scripts ?? {};
		const scriptText = Object.values(scripts).join(" ");
		// Look for --port NNNN, -p NNNN, PORT=NNNN patterns
		// Anchor more tightly: require whitespace or start-of-string for --port/-p,
		// and require whitespace or = for PORT=, avoid IPv6 colons.
		const portMatches = scriptText.matchAll(
			/(?:^|\s)(?:--port\s+|-p\s+)(\d{4,5})\b|(?:^|[\s=])PORT=(\d{4,5})(?:\s|$)/gm,
		);
		for (const m of portMatches) {
			const port = parseInt(m[1] || m[2], 10);
			if (port >= 1024 && port <= 65535) portsToCheck.add(port);
		}
	} catch {
		// parse failed — skip port checks rather than using defaults
		return [];
	}
	// If no ports found in scripts, check common defaults.
	// Filter out port 5000 on macOS — AirPlay Receiver uses it by default (#1381).
	if (portsToCheck.size === 0) {
		for (const p of DEFAULT_DEV_PORTS) {
			if (p === 5000 && process.platform === "darwin") continue;
			portsToCheck.add(p);
		}
	}
	for (const port of portsToCheck) {
		const result = tryExec(`lsof -i :${port} -sTCP:LISTEN -t`, basePath);
		if (result && result.length > 0) {
			// Get process name
			const nameResult = tryExec(
				`lsof -i :${port} -sTCP:LISTEN -F cn | head -2`,
				basePath,
			);
			// Parse lsof -F cn output: lines like "c<cmdname>" and "n<name>"
			// Use field mode to reliably extract process name from COMMAND field.
			// Defensive: if the first 'c' line is missing, scan all lines.
			let processName = "unknown";
			if (nameResult) {
				const cLine = nameResult
					.split("\n")
					.find((line) => line.startsWith("c"));
				if (cLine !== undefined) {
					processName = cLine.substring(1);
				}
			}
			results.push({
				name: "port_conflict",
				status: "warning",
				message: `Port ${port} is already in use by ${processName} (PID ${result.split("\n")[0]})`,
				detail: `Kill the process or use a different port`,
			});
		}
	}
	return results;
}
/**
 * Check available disk space on the working directory partition.
 */
function checkDiskSpace(basePath) {
	// Only run on macOS/Linux
	if (process.platform === "win32") return null;
	const dfOutput = tryExec(`df -k "${basePath}" | tail -1`, basePath);
	if (!dfOutput) return null;
	try {
		// df output: filesystem blocks used avail capacity mount
		const parts = dfOutput.split(/\s+/);
		const availKB = parseInt(parts[3], 10);
		if (Number.isNaN(availKB)) return null;
		const availBytes = availKB * 1024;
		const availMB = Math.round(availBytes / (1024 * 1024));
		const availGB = (availBytes / (1024 * 1024 * 1024)).toFixed(1);
		if (availBytes < MIN_DISK_BYTES) {
			return {
				name: "disk_space",
				status: "error",
				message: `Low disk space: ${availMB}MB free`,
				detail: `Free up space — builds and git operations may fail`,
			};
		}
		if (availBytes < MIN_DISK_BYTES * 4) {
			return {
				name: "disk_space",
				status: "warning",
				message: `Disk space getting low: ${availGB}GB free`,
			};
		}
		return { name: "disk_space", status: "ok", message: `${availGB}GB free` };
	} catch {
		return null;
	}
}
/**
 * Check if Docker is available when project has a Dockerfile.
 */
function checkDocker(basePath) {
	const hasDockerfile =
		existsSync(join(basePath, "Dockerfile")) ||
		existsSync(join(basePath, "docker-compose.yml")) ||
		existsSync(join(basePath, "docker-compose.yaml")) ||
		existsSync(join(basePath, "compose.yml")) ||
		existsSync(join(basePath, "compose.yaml"));
	if (!hasDockerfile) return null;
	if (!commandExists("docker", basePath)) {
		return {
			name: "docker",
			status: "warning",
			message: "Project has Docker files but docker is not installed",
		};
	}
	const info = tryExec("docker info --format '{{.ServerVersion}}'", basePath);
	if (!info) {
		return {
			name: "docker",
			status: "warning",
			message: "Docker is installed but daemon is not running",
			detail: "Start Docker Desktop or the docker daemon",
		};
	}
	return { name: "docker", status: "ok", message: `Docker ${info}` };
}
/**
 * Check for common project tools that should be available.
 */
function checkProjectTools(basePath) {
	const results = [];
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) return results;
	try {
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		const allDeps = {
			...(pkg.dependencies ?? {}),
			...(pkg.devDependencies ?? {}),
		};
		// Check for package manager
		const packageManager = pkg.packageManager;
		if (packageManager) {
			const managerName = packageManager.split("@")[0];
			if (
				managerName &&
				managerName !== "npm" &&
				!commandExists(managerName, basePath)
			) {
				results.push({
					name: "package_manager",
					status: "warning",
					message: `Project requires ${managerName} but it's not installed`,
					detail: `Install with: npm install -g ${managerName}`,
				});
			}
		}
		// Check for TypeScript if it's a dependency
		if (
			allDeps["typescript"] &&
			!existsSync(join(basePath, "node_modules", ".bin", "tsc"))
		) {
			results.push({
				name: "typescript",
				status: "warning",
				message:
					"TypeScript is a dependency but tsc is not available (run npm install)",
			});
		}
		// Check for Python if pyproject.toml or requirements.txt exists
		if (
			existsSync(join(basePath, "pyproject.toml")) ||
			existsSync(join(basePath, "requirements.txt"))
		) {
			if (
				!commandExists("python3", basePath) &&
				!commandExists("python", basePath)
			) {
				results.push({
					name: "python",
					status: "warning",
					message: "Project has Python config but python is not installed",
				});
			}
		}
		// Check for Rust if Cargo.toml exists
		if (existsSync(join(basePath, "Cargo.toml"))) {
			if (!commandExists("cargo", basePath)) {
				results.push({
					name: "cargo",
					status: "warning",
					message: "Project has Cargo.toml but cargo is not installed",
				});
			}
		}
		// Check for Go if go.mod exists
		if (existsSync(join(basePath, "go.mod"))) {
			if (!commandExists("go", basePath)) {
				results.push({
					name: "go",
					status: "warning",
					message: "Project has go.mod but go is not installed",
				});
			}
		}
	} catch {
		// parse failed — skip
	}
	return results;
}
/**
 * Check git remote reachability.
 */
function checkGitRemote(basePath) {
	// Only check if it's a git repo with a remote
	const remote = tryExec("git remote get-url origin", basePath);
	if (!remote) return null;
	// Quick connectivity check with short timeout
	const result = tryExec("git ls-remote --exit-code origin HEAD", basePath);
	if (result === null) {
		return {
			name: "git_remote",
			status: "warning",
			message: "Git remote 'origin' is unreachable",
			detail: `Remote: ${remote}`,
		};
	}
	return { name: "git_remote", status: "ok", message: "Git remote reachable" };
}
/**
 * Check if the project build passes (opt-in slow check, use --build flag).
 * Runs npm run build and reports failure as env_build.
 */
function checkBuildHealth(basePath) {
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) return null;
	try {
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		const buildScript = pkg.scripts?.build;
		if (!buildScript) return null;
		const result = tryExec("npm run build 2>&1", basePath);
		if (result === null) {
			return {
				name: "build",
				status: "error",
				message: "Build failed — npm run build exited non-zero",
				detail: "Fix build errors before dispatching work",
			};
		}
		return { name: "build", status: "ok", message: "Build passes" };
	} catch {
		return null;
	}
}
/**
 * Check if tests pass (opt-in slow check, use --test flag).
 * Runs npm test and reports failures as env_test.
 */
function checkTestHealth(basePath) {
	const pkgPath = join(basePath, "package.json");
	if (!existsSync(pkgPath)) return null;
	try {
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		const testScript = pkg.scripts?.test;
		// Skip if no test script or the default placeholder
		if (!testScript || testScript.includes("no test specified")) return null;
		const result = tryExec("npm test 2>&1", basePath);
		if (result === null) {
			return {
				name: "test",
				status: "warning",
				message: "Tests failing — npm test exited non-zero",
				detail: "Fix failing tests before shipping",
			};
		}
		return { name: "test", status: "ok", message: "Tests pass" };
	} catch {
		return null;
	}
}
// ── Public API ─────────────────────────────────────────────────────────────
/**
 * Run all environment health checks. Returns structured results for
 * integration with the doctor pipeline.
 */
export function runEnvironmentChecks(basePath) {
	const results = [];
	const nodeCheck = checkNodeVersion(basePath);
	if (nodeCheck) results.push(nodeCheck);
	const pythonCheck = checkPythonEnvironment(basePath);
	if (pythonCheck) results.push(pythonCheck);
	const siftCheck = checkSiftAvailable(basePath);
	if (siftCheck) results.push(siftCheck);
	const depsCheck = checkDependenciesInstalled(basePath);
	if (depsCheck) results.push(depsCheck);
	const envCheck = checkEnvFiles(basePath);
	if (envCheck) results.push(envCheck);
	results.push(...checkPortConflicts(basePath));
	const diskCheck = checkDiskSpace(basePath);
	if (diskCheck) results.push(diskCheck);
	const dockerCheck = checkDocker(basePath);
	if (dockerCheck) results.push(dockerCheck);
	results.push(...checkProjectTools(basePath));
	// Git remote check can be slow — only run on explicit doctor invocation
	// (not on pre-dispatch gate)
	return results;
}
/**
 * Run environment checks with git remote check included.
 * Use this for explicit /doctor invocations, not pre-dispatch gates.
 */
export function runFullEnvironmentChecks(basePath) {
	const results = runEnvironmentChecks(basePath);
	const remoteCheck = checkGitRemote(basePath);
	if (remoteCheck) results.push(remoteCheck);
	return results;
}
/**
 * Run slow opt-in checks (build and/or test).
 * These are never run on the pre-dispatch gate — only on explicit /doctor --build/--test.
 */
export function runSlowEnvironmentChecks(basePath, options) {
	const results = [];
	if (options?.includeBuild) {
		const buildCheck = checkBuildHealth(basePath);
		if (buildCheck) results.push(buildCheck);
	}
	if (options?.includeTests) {
		const testCheck = checkTestHealth(basePath);
		if (testCheck) results.push(testCheck);
	}
	return results;
}
/**
 * Convert environment check results to DoctorIssue format for the doctor pipeline.
 */
export function environmentResultsToDoctorIssues(results) {
	return results
		.filter((r) => r.status !== "ok")
		.map((r) => ({
			severity: r.status === "error" ? "error" : "warning",
			code: `env_${r.name}`,
			scope: "project",
			unitId: "environment",
			message: r.detail ? `${r.message} — ${r.detail}` : r.message,
			fixable: r.fixCode === `env_${r.name}`,
		}));
}
export function applyEnvironmentFixes(basePath, results, options) {
	const shouldFix = options?.shouldFix;
	const fixesApplied = options?.fixesApplied;
	if (!shouldFix || !fixesApplied) return false;
	const dependencyIssue = results.find(
		(r) =>
			r.name === "dependencies" &&
			r.status !== "ok" &&
			r.fixCode === "env_dependencies",
	);
	if (!dependencyIssue || !shouldFix("env_dependencies")) return false;
	const manager = detectNodePackageManager(basePath);
	if (!manager) {
		fixesApplied.push("dependencies: skipped install (no supported lockfile)");
		return false;
	}
	if (!commandExists(manager.name, basePath)) {
		fixesApplied.push(
			`dependencies: skipped ${manager.command} (${manager.name} not found)`,
		);
		return false;
	}
	try {
		runNodeInstall(basePath, manager);
		fixesApplied.push(`dependencies: ran ${manager.command}`);
		return true;
	} catch {
		fixesApplied.push(`dependencies: ${manager.command} failed`);
		return false;
	}
}
/**
 * Integration point for the doctor pipeline. Runs environment checks
 * and appends issues to the provided array.
 */
export async function checkEnvironmentHealth(basePath, issues, options) {
	let results = options?.includeRemote
		? runFullEnvironmentChecks(basePath)
		: runEnvironmentChecks(basePath);
	if (options?.includeBuild || options?.includeTests) {
		results.push(...runSlowEnvironmentChecks(basePath, options));
	}
	if (applyEnvironmentFixes(basePath, results, options)) {
		results = options?.includeRemote
			? runFullEnvironmentChecks(basePath)
			: runEnvironmentChecks(basePath);
		if (options?.includeBuild || options?.includeTests) {
			results.push(...runSlowEnvironmentChecks(basePath, options));
		}
	}
	issues.push(...environmentResultsToDoctorIssues(results));
}
/**
 * Check if emoji icons should be rendered.
 * Respects NO_COLOR env var and CI detection.
 */
function shouldShowEmojis() {
	// NO_COLOR disables all color and emoji output
	if (process.env.NO_COLOR) return false;
	// CI environments often don't support emoji rendering
	if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return false;
	return true;
}
/**
 * Format environment check results for display.
 */
export function formatEnvironmentReport(results) {
	if (results.length === 0) return "No environment checks applicable.";
	const lines = [];
	lines.push("Environment Health:");
	const useEmojis = shouldShowEmojis();
	for (const r of results) {
		const icon = useEmojis
			? r.status === "ok"
				? "\u2705"
				: r.status === "warning"
					? "\u26A0\uFE0F"
					: "\uD83D\uDED1"
			: r.status === "ok"
				? "\u2713"
				: r.status === "warning"
					? "\u26A0"
					: "\u2717";
		lines.push(`  ${icon} ${r.message}`);
		if (r.detail && r.status !== "ok") {
			lines.push(`     ${r.detail}`);
		}
	}
	return lines.join("\n");
}
