diff --git a/ra_aid/env_inv.py b/ra_aid/env_inv.py new file mode 100644 index 0000000..93d7aac --- /dev/null +++ b/ra_aid/env_inv.py @@ -0,0 +1,620 @@ +import os +import platform +import shutil +import subprocess +from pathlib import Path + +class EnvDiscovery: + def __init__(self): + # Structured results dictionary. + self.results = { + "os": {}, + "cli_tools": {}, + "python": {"installations": [], "env_tools": {}}, + "package_managers": {}, + "libraries": {}, + "node": {} + } + # Common CLI tools. Added additional critical dev tools. + self._cli_tool_names = [ + "fd", "rg", "fzf", "git", "g++", "gcc", "clang", "cmake", "make", + "pkg-config", "ninja", "autoconf", "automake", "libtool", "meson", "scons" + ] + # Python environment tools. + self._py_env_tools = { + "virtualenv": "virtualenv", + "uv": "uv", + "pipenv": "pipenv", + "poetry": "poetry", + "conda": "conda", + "pyenv": "pyenv", + "pipx": "pipx" + } + # Package managers. + self._package_managers = [ + "apt", "apt-get", "dnf", "yum", "pacman", "paru", "zypper", + "brew", "winget", "choco" + ] + # Expanded libraries detection list. + # Each entry maps a library key to a dict with possible keys: + # - 'pkg': pkg-config name if available. + # - 'headers': list of header paths relative to common include directories. + self._libraries = { + # Graphics & Game Dev: + "SDL2": {"pkg": "sdl2", "headers": ["SDL2/SDL.h", "SDL.h"]}, + "OpenGL": {"pkg": "gl", "headers": ["GL/gl.h", "OpenGL/gl.h"]}, + "Vulkan": {"pkg": "vulkan", "headers": ["vulkan/vulkan.h"]}, + "DirectX": {"headers": []}, # Windows only; detection via headers is non-trivial. + "GLFW": {"pkg": "glfw3", "headers": ["GLFW/glfw3.h"]}, + "Raylib": {"pkg": "raylib", "headers": ["raylib.h"]}, + "SFML": {"headers": ["SFML/Graphics.hpp", "SFML/Window.hpp"]}, + "Allegro": {"pkg": "allegro", "headers": ["allegro5/allegro.h"]}, + "OGRE": {"headers": ["OGRE/Ogre.h"]}, + "Irrlicht": {"headers": ["irrlicht.h"]}, + "bgfx": {"headers": ["bgfx/bgfx.h"]}, + "Magnum": {"headers": ["Magnum/Platform/GlfwApplication.h"]}, + "Assimp": {"pkg": "assimp", "headers": ["assimp/Importer.hpp"]}, + "DearImGui": {"headers": ["imgui.h"]}, + "Cairo": {"pkg": "cairo", "headers": ["cairo.h"]}, + "NanoVG": {"headers": ["nanovg.h"]}, + # Physics Engines: + "Bullet": {"headers": ["bullet/btBulletDynamicsCommon.h"]}, + "PhysX": {"headers": []}, + "ODE": {"pkg": "ode", "headers": ["ode/ode.h"]}, + "Box2D": {"pkg": "box2d", "headers": ["box2d/box2d.h"]}, + "JoltPhysics": {"headers": ["Jolt/Jolt.h"]}, + "MuJoCo": {"headers": ["mujoco.h"]}, + "Newton": {"pkg": "newton", "headers": ["Newton/Newton.h"]}, + # Math & Linear Algebra: + "Eigen": {"headers": ["Eigen/Dense"]}, + "GLM": {"headers": ["glm/glm.hpp"]}, + "Armadillo": {"pkg": "armadillo", "headers": ["armadillo"]}, + "BLAS": {"headers": []}, + "LAPACK": {"headers": []}, + "OpenBLAS": {"headers": []}, + "IntelMKL": {"headers": []}, + "Boost_uBLAS": {"headers": ["boost/numeric/ublas/matrix.hpp"]}, + "Blaze": {"headers": ["blaze/Blaze.h"]}, + "Blitz++": {"headers": ["blitz/array.h"]}, + "xtensor": {"headers": ["xtensor/xarray.hpp"]}, + "GSL": {"pkg": "gsl", "headers": ["gsl/gsl_errno.h"]}, + # Machine Learning & AI: + "TensorFlow": {"pkg": "tensorflow", "headers": ["tensorflow/c/c_api.h"]}, + "PyTorch": {"pkg": "torch", "headers": []}, + "ONNX": {"pkg": "onnx", "headers": []}, + "OpenCV": {"pkg": "opencv", "headers": ["opencv2/opencv.hpp"]}, + "scikit-learn": {"headers": []}, + "Caffe": {"headers": ["caffe/caffe.hpp"]}, + "MXNet": {"headers": ["mxnet-cpp/MxNetCpp.h"]}, + "XGBoost": {"pkg": "xgboost", "headers": []}, + "LightGBM": {"headers": []}, + "dlib": {"pkg": "dlib", "headers": ["dlib/dlib.h"]}, + "OpenVINO": {"headers": []}, + "TensorRT": {"headers": []}, + # Networking & Communication: + "Boost_Asio": {"headers": ["boost/asio.hpp"]}, + "libcurl": {"pkg": "libcurl", "headers": ["curl/curl.h"]}, + "ZeroMQ": {"pkg": "libzmq", "headers": ["zmq.h"]}, + "gRPC": {"pkg": "grpc", "headers": ["grpc/grpc.h"]}, + "Thrift": {"headers": ["thrift/Thrift.h"]}, + "libevent": {"pkg": "libevent", "headers": ["event2/event.h"]}, + "libuv": {"pkg": "libuv", "headers": ["uv.h"]}, + "Boost_Beast": {"headers": ["boost/beast.hpp"]}, + "libwebsockets": {"pkg": "libwebsockets", "headers": ["libwebsockets.h"]}, + "MQTT": {"pkg": "paho-mqtt3c", "headers": ["MQTTClient.h"]}, + "APR": {"pkg": "apr-1", "headers": ["apr.h"]}, + "nng": {"pkg": "nng", "headers": ["nng/nng.h"]}, + # Compression & Encoding: + "zlib": {"pkg": "zlib", "headers": ["zlib.h"]}, + "LZ4": {"pkg": "lz4", "headers": ["lz4.h"]}, + "Zstd": {"pkg": "zstd", "headers": ["zstd.h"]}, + "Brotli": {"pkg": "brotli", "headers": ["brotli/decode.h"]}, + "bzip2": {"pkg": "bzip2", "headers": ["bzlib.h"]}, + "xz": {"pkg": "liblzma", "headers": ["lzma.h"]}, + "Snappy": {"pkg": "snappy", "headers": ["snappy.h"]}, + "libpng": {"pkg": "libpng", "headers": ["png.h"]}, + "libjpeg": {"pkg": "libjpeg", "headers": ["jpeglib.h"]}, + "libtiff": {"pkg": "libtiff-4", "headers": ["tiffio.h"]}, + "libwebp": {"pkg": "libwebp", "headers": ["webp/encode.h"]}, + "FFmpeg": {"pkg": "libavcodec", "headers": ["libavcodec/avcodec.h"]}, + "GStreamer": {"pkg": "gstreamer-1.0", "headers": ["gst/gst.h"]}, + "libogg": {"pkg": "libogg", "headers": ["ogg/ogg.h"]}, + "libvorbis": {"pkg": "vorbis", "headers": ["vorbis/codec.h"]}, + "libFLAC": {"pkg": "flac", "headers": ["FLAC/stream_encoder.h"]}, + # Databases & Data Storage: + "SQLite": {"pkg": "sqlite3", "headers": ["sqlite3.h"]}, + "PostgreSQL": {"pkg": "libpq", "headers": ["libpq-fe.h"]}, + "MySQL": {"pkg": "mysqlclient", "headers": ["mysql.h"]}, + "Redis": {"headers": []}, + "LevelDB": {"headers": ["leveldb/db.h"]}, + "RocksDB": {"headers": ["rocksdb/db.h"]}, + "BerkeleyDB": {"headers": ["db.h"]}, + "HDF5": {"pkg": "hdf5", "headers": ["hdf5.h"]}, + # Parallel Computing & GPU: + "OpenMP": {"headers": []}, + "MPI": {"pkg": "mpi", "headers": ["mpi.h"]}, + "CUDA": {"pkg": "cuda", "headers": ["cuda.h"]}, + "OpenCL": {"pkg": "OpenCL", "headers": ["CL/cl.h"]}, + "oneAPI": {"headers": []}, + "HIP": {"headers": []}, + "OpenACC": {"headers": []}, + "TBB": {"pkg": "tbb", "headers": ["tbb/tbb.h"]}, + "cuDNN": {"headers": []}, + "MicrosoftMPI": {"headers": []}, + # Cryptography & Security: + "OpenSSL": {"pkg": "openssl", "headers": ["openssl/ssl.h"]}, + "LibreSSL": {"pkg": "openssl", "headers": ["openssl/ssl.h"]}, + "BoringSSL": {"headers": []}, + "libsodium": {"pkg": "sodium", "headers": ["sodium.h"]}, + "Crypto++": {"headers": ["cryptopp/cryptlib.h"]}, + "Botan": {"headers": ["botan/botan.h"]}, + "GnuTLS": {"pkg": "gnutls", "headers": ["gnutls/gnutls.h"]}, + "mbedTLS": {"pkg": "mbedtls", "headers": ["mbedtls/ssl.h"]}, + "wolfSSL": {"pkg": "wolfssl", "headers": ["wolfssl/options.h"]}, + # Scripting & Embedding: + "Python_C_API": {"headers": ["Python.h"]}, + "Lua": {"pkg": "lua", "headers": ["lua.h"]}, + "LuaJIT": {"pkg": "luajit", "headers": ["luajit.h"]}, + "V8": {"headers": ["v8.h"]}, + "Duktape": {"headers": ["duktape.h"]}, + "SpiderMonkey": {"headers": ["jsapi.h"]}, + "JavaScriptCore": {"headers": ["JavaScriptCore/JavaScript.h"]}, + "ChakraCore": {"headers": ["ChakraCore.h"]}, + "Tcl": {"pkg": "tcl", "headers": ["tcl.h"]}, + "Guile": {"headers": ["libguile.h"]}, + "Mono": {"headers": ["mono/jit/jit.h"]}, + # Audio & Multimedia: + "OpenAL": {"pkg": "openal", "headers": ["AL/al.h"]}, + "PortAudio": {"pkg": "portaudio-2.0", "headers": ["portaudio.h"]}, + "FMOD": {"headers": []}, + "SoLoud": {"headers": ["soloud.h"]}, + "RtAudio": {"headers": ["RtAudio.h"]}, + "SDL_mixer": {"pkg": "SDL2_mixer", "headers": ["SDL2/SDL_mixer.h"]}, + "OpenAL_Soft": {"pkg": "openal", "headers": ["AL/al.h"]}, + "libsndfile": {"pkg": "sndfile", "headers": ["sndfile.h"]}, + "Jack": {"pkg": "jack", "headers": ["jack/jack.h"]}, + # Dev Utilities & Frameworks: + "Boost": {"headers": ["boost/config.hpp"]}, + "Qt": {"headers": ["QtCore/QtCore"]}, + "wxWidgets": {"headers": ["wx/wx.h"]}, + "GTK": {"pkg": "gtk+-3.0", "headers": ["gtk/gtk.h"]}, + "ncurses": {"pkg": "ncurses", "headers": ["ncurses.h"]}, + "Poco": {"headers": ["Poco/Foundation.h"]}, + "ICU": {"pkg": "icu-uc", "headers": ["unicode/utypes.h"]}, + "RapidJSON": {"headers": ["rapidjson/document.h"]}, + "nlohmann_json": {"headers": ["nlohmann/json.hpp"]}, + "json-c": {"pkg": "json-c", "headers": ["json-c/json.h"]}, + "YAML_cpp": {"headers": ["yaml-cpp/yaml.h"]}, + "spdlog": {"headers": ["spdlog/spdlog.h"]}, + "log4cxx": {"headers": ["log4cxx/logger.h"]}, + "glog": {"headers": ["glog/logging.h"]}, + "GoogleTest": {"headers": ["gtest/gtest.h"]}, + "BoostTest": {"headers": ["boost/test/unit_test.hpp"]}, + "pkg-config": {"headers": []}, + "CMake": {"headers": []}, + "GLib": {"pkg": "glib-2.0", "headers": ["glib.h"]} + } + # List of common include directories to search for headers. + # Expanded to cover multiple common Homebrew paths on macOS and Linuxbrew. + self._include_paths = [ + Path("/usr/include"), + Path("/usr/local/include"), + Path("/opt/homebrew/include"), + Path("/home/linuxbrew/.linuxbrew/include"), + Path("/usr/local/Homebrew/include") + ] + # Linux distribution info. + self._distro = {} + if platform.system() == "Linux": + self._distro = self._get_linux_distro() + + def _get_linux_distro(self): + distro = {} + try: + with open("/etc/os-release") as f: + for line in f: + if "=" not in line: + continue + key, val = line.strip().split("=", 1) + distro[key] = val.strip('"') + except FileNotFoundError: + pass + return distro + + def discover(self): + self._detect_os() + self._detect_cli_tools() + self._detect_python() + self._detect_python_env_tools() + self._detect_package_managers() + self._detect_libraries() + self._detect_node() + return self.results + + def _detect_os(self): + os_type = platform.system() + os_info = {} + if os_type == "Windows": + os_info["name"] = "Windows" + os_info["wsl"] = False + elif os_type == "Linux": + release = platform.uname().release + if "Microsoft" in release or release.lower().endswith("microsoft"): + os_info["name"] = "Linux (WSL)" + os_info["wsl"] = True + else: + os_info["name"] = "Linux" + os_info["wsl"] = False + if self._distro: + name = self._distro.get("PRETTY_NAME") or self._distro.get("NAME") + version = self._distro.get("VERSION_ID") or self._distro.get("VERSION") + if name: + os_info["distro"] = name + if version: + os_info["distro_version"] = version + elif os_type == "Darwin": + os_info["name"] = "macOS" + os_info["wsl"] = False + else: + os_info["name"] = os_type + os_info["wsl"] = False + self.results["os"] = os_info + + def _detect_cli_tools(self): + tools_found = {} + for tool in self._cli_tool_names: + path = shutil.which(tool) + if path: + version = None + if tool in ("g++", "gcc", "clang", "git"): + try: + out = subprocess.check_output([tool, "--version"], text=True, stderr=subprocess.STDOUT, timeout=1) + version = out.splitlines()[0].strip() + except Exception: + version = None + tools_found[tool] = {"found": True} + if version: + tools_found[tool]["version"] = version + else: + tools_found[tool] = {"found": False} + self.results["cli_tools"] = tools_found + + def _detect_python(self): + installations = [] + if platform.system() == "Windows": + launcher = shutil.which("py") + if launcher: + try: + out = subprocess.check_output([launcher, "-0p"], text=True, timeout=2) + for line in out.splitlines(): + line = line.strip() + if not line or not line.startswith("-V:"): + continue + after = line.split(":", 1)[1] + parts = after.strip().split(None, 1) + ver_str = parts[0].lstrip("v") + py_path = parts[1] if len(parts) > 1 else "" + installations.append({"version": ver_str, "path": py_path}) + except subprocess.CalledProcessError: + pass + if not installations: + try: + out = subprocess.check_output(["where", "python"], text=True, timeout=2) + for path in out.splitlines(): + path = path.strip() + if path and Path(path).name.lower().startswith("python"): + ver = self._get_python_version(path) + installations.append({"version": ver, "path": path}) + except Exception: + pass + else: + common_names = ["python3", "python", "python2"] + for major in [2, 3]: + for minor in range(0, 15): + common_names.append(f"python{major}.{minor}") + seen_paths = set() + for name in common_names: + path = shutil.which(name) + if path and path not in seen_paths: + seen_paths.add(path) + ver = self._get_python_version(path) + installations.append({"version": ver, "path": path}) + installations = sorted(installations, key=lambda x: x.get("version", "")) + self.results["python"]["installations"] = installations + + def _get_python_version(self, python_path): + try: + out = subprocess.check_output([python_path, "--version"], stderr=subprocess.STDOUT, text=True, timeout=1) + ver = out.strip().split()[1] + return ver + except Exception: + return None + + def _detect_python_env_tools(self): + env_tools_status = {} + venv_available = any(inst for inst in self.results["python"]["installations"] + if inst.get("version") and inst["version"][0] == '3') + env_tools_status["venv"] = {"available": venv_available, "built_in": True} + for tool, display_name in self._py_env_tools.items(): + found_path = shutil.which(tool) + if found_path: + version = None + try: + if tool == "pyenv": + out = subprocess.check_output([tool, "--version"], text=True, timeout=1) + version = out.strip().split()[-1] + elif tool in ("pipenv", "poetry", "conda", "pipx", "uv"): + out = subprocess.check_output([tool, "--version"], text=True, timeout=2) + version = out.strip().split()[-1] + elif tool == "virtualenv": + out = subprocess.check_output([tool, "--version"], text=True, timeout=2) + version = out.strip() + except Exception: + version = None + env_tools_status[display_name] = {"installed": True} + if version: + env_tools_status[display_name]["version"] = version + else: + env_tools_status[display_name] = {"installed": False} + self.results["python"]["env_tools"] = env_tools_status + + def _detect_package_managers(self): + pkg_status = {} + for mgr in self._package_managers: + if platform.system() == "Windows": + if mgr in ("apt", "apt-get", "dnf", "yum", "pacman", "paru", "zypper", "brew"): + continue + if platform.system() == "Darwin": + if mgr in ("apt", "apt-get", "dnf", "yum", "pacman", "paru", "zypper", "winget", "choco"): + continue + if platform.system() == "Linux" and self._distro: + distro_id = self._distro.get("ID", "").lower() + if distro_id: + if distro_id in ("debian", "ubuntu", "linuxmint"): + if mgr in ("pacman", "paru", "yum", "dnf", "zypper"): + continue + if distro_id in ("fedora", "centos", "rhel", "rocky", "alma"): + if mgr in ("apt", "apt-get", "pacman", "paru", "zypper"): + continue + if distro_id in ("arch", "manjaro", "endeavouros"): + if mgr in ("apt", "apt-get", "dnf", "yum", "zypper"): + continue + if distro_id in ("opensuse", "suse"): + if mgr in ("apt", "apt-get", "dnf", "yum", "pacman", "paru"): + continue + path = shutil.which(mgr) + pkg_status[mgr] = {"found": bool(path)} + if path: + version = None + try: + if mgr in ("brew", "winget", "choco"): + out = subprocess.check_output([mgr, "--version"], text=True, timeout=3) + version_line = out.splitlines()[0].strip() + version = version_line + elif mgr in ("apt", "apt-get", "pacman", "paru", "dnf", "yum", "zypper"): + out = subprocess.check_output([mgr, "--version"], text=True, timeout=2) + version_line = out.splitlines()[0].strip() + version = version_line + except Exception: + version = None + if version: + pkg_status[mgr]["version"] = version + self.results["package_managers"] = pkg_status + + def _detect_libraries(self): + libs_found = {} + have_pkg_config = bool(shutil.which("pkg-config")) + for lib, info in self._libraries.items(): + lib_info = {"found": False} + found = False + ver = None + cflags = None + libs_flags = None + header_paths = [] + if have_pkg_config and info.get("pkg"): + pkg_name = info["pkg"] + try: + subprocess.check_output(["pkg-config", "--exists", pkg_name], + stderr=subprocess.DEVNULL, timeout=1) + found = True + try: + ver = subprocess.check_output( + ["pkg-config", "--modversion", pkg_name], + text=True, timeout=1 + ).strip() + except Exception: + ver = None + try: + cflags = subprocess.check_output( + ["pkg-config", "--cflags", pkg_name], + text=True, timeout=1 + ).strip() + except Exception: + cflags = None + try: + libs_flags = subprocess.check_output( + ["pkg-config", "--libs", pkg_name], + text=True, timeout=1 + ).strip() + except Exception: + libs_flags = None + except subprocess.CalledProcessError: + found = False + if not found and info.get("headers"): + for header in info["headers"]: + for inc_dir in self._include_paths: + header_file = inc_dir / header + if header_file.exists(): + found = True + header_paths.append(str(header_file)) + lib_info["found"] = found + if ver: + lib_info["version"] = ver + if cflags: + lib_info["cflags"] = cflags + if libs_flags: + lib_info["libs"] = libs_flags + if header_paths: + lib_info["header_paths"] = header_paths + libs_found[lib] = lib_info + self.results["libraries"] = libs_found + + def _detect_node(self): + node_info = {} + node_path = shutil.which("node") + if node_path: + try: + out = subprocess.check_output(["node", "--version"], text=True, timeout=1) + node_info["node_version"] = out.strip() + except Exception: + node_info["node_version"] = "found" + else: + node_info["node_version"] = None + npm_path = shutil.which("npm") + if npm_path: + try: + out = subprocess.check_output(["npm", "--version"], text=True, timeout=1) + node_info["npm_version"] = out.strip() + except Exception: + node_info["npm_version"] = "found" + else: + node_info["npm_version"] = None + nvm_installed = False + nvm_version = None + if platform.system() == "Windows": + if shutil.which("nvm"): + nvm_installed = True + try: + out = subprocess.check_output(["nvm", "version"], text=True, timeout=2) + nvm_version = out.strip() + except Exception: + nvm_version = None + else: + if os.environ.get("NVM_DIR") or Path.home().joinpath(".nvm").exists(): + nvm_installed = True + node_info["nvm_installed"] = nvm_installed + if nvm_version: + node_info["nvm_version"] = nvm_version + self.results["node"] = node_info + + def format_markdown(self): + os_info = self.results.get("os", {}) + lines = [] + # OS Section + os_section = f"**Operating System:** {os_info.get('name')}" + if os_info.get("distro"): + os_section += f" ({os_info['distro']}" + if os_info.get("distro_version"): + os_section += f" {os_info['distro_version']}" + os_section += ")" + lines.append(os_section) + if os_info.get("wsl"): + lines.append("- Running under WSL") + lines.append("") + # CLI Tools Section - output as one list. + cli_found = [] + for tool, status in self.results.get("cli_tools", {}).items(): + if status.get("found"): + if status.get("version"): + cli_found.append(f"{tool} ({status['version']})") + else: + cli_found.append(tool) + if cli_found: + lines.append("**Found CLI developer tools:** " + ", ".join(cli_found)) + else: + lines.append("**Found CLI developer tools:** None") + lines.append("") + # Python Section + py_installs = self.results.get("python", {}).get("installations", []) + env_tools = self.results.get("python", {}).get("env_tools", {}) + lines.append("**Python Environments:**") + if py_installs: + for py in py_installs: + ver = py.get("version") or "unknown version" + path = py.get("path") + lines.append(f"- Python {ver} at `{path}`") + else: + lines.append("- No Python interpreter found") + for tool, info in env_tools.items(): + if tool == "venv": + available = info.get("available", False) + lines.append(f"- venv (builtin): {'available' if available else 'not available'}") + else: + installed = info.get("installed", False) + ver = info.get("version") + if installed: + if ver: + lines.append(f"- {tool}: installed (version {ver})") + else: + lines.append(f"- {tool}: installed") + else: + lines.append(f"- {tool}: not installed") + lines.append("") + # Package Managers Section + pkg_mgrs = self.results.get("package_managers", {}) + lines.append("**Package Managers:**") + any_pkg = False + for mgr, info in pkg_mgrs.items(): + if not info.get("found"): + continue + any_pkg = True + ver = info.get("version") + if ver: + lines.append(f"- {mgr}: found ({ver})") + else: + lines.append(f"- {mgr}: found") + if not any_pkg: + lines.append("- *(No common package managers found)*") + lines.append("") + # Libraries Section + libs = self.results.get("libraries", {}) + lines.append("**Developer Libraries:**") + found_libs = [] + not_found_libs = [] + for lib, info in libs.items(): + if info.get("found"): + line = f"- {lib}: installed" + if info.get("version"): + line += f" (version {info['version']})" + if info.get("cflags"): + line += f", cflags: `{info['cflags']}`" + if info.get("libs"): + line += f", libs: `{info['libs']}`" + if info.get("header_paths"): + line += f", headers: {', '.join(info['header_paths'])}" + found_libs.append(line) + else: + not_found_libs.append(lib) + lines.extend(found_libs) + if not_found_libs: + lines.append(f"- Not found: {', '.join(sorted(not_found_libs))}") + lines.append("") + # Node.js Section + node = self.results.get("node", {}) + lines.append("**Node.js and Related:**") + node_ver = node.get("node_version") + npm_ver = node.get("npm_version") + nvm_inst = node.get("nvm_installed") + nvm_ver = node.get("nvm_version") + if node_ver: + lines.append(f"- Node.js: {node_ver}") + else: + lines.append("- Node.js: not installed") + if npm_ver: + lines.append(f"- npm: version {npm_ver}") + else: + lines.append("- npm: not installed") + if nvm_inst: + if nvm_ver: + lines.append(f"- nvm: installed (version {nvm_ver})") + else: + lines.append("- nvm: installed") + else: + lines.append("- nvm: not installed") + lines.append("") + return "\n".join(lines) + +if __name__ == "__main__": + env = EnvDiscovery() + env.discover() + print(env.format_markdown())