#!/usr/bin/env python3 """Read-only, bounded diagnostics for a Vite project.""" from __future__ import annotations import argparse import json import os import shutil import subprocess import sys from pathlib import Path LOCKFILES = { "package-lock.json": "npm", "pnpm-lock.yaml": "pnpm", "yarn.lock": "yarn", "bun.lock": "bun", "bun.lockb": "bun", } MANAGERS = { "npm": ["npm", "--version"], "pnpm": ["pnpm", "--version"], "yarn": ["yarn", "--version"], "bun": ["bun", "--version"], } def bounded_version(command: list[str], timeout: float) -> dict[str, object]: executable = shutil.which(command[0]) result: dict[str, object] = {"available": executable is not None} if executable is None: return result try: completed = subprocess.run( command, capture_output=True, text=True, timeout=timeout, check=False ) except subprocess.TimeoutExpired: result["error"] = "timeout" return result value = (completed.stdout or completed.stderr).strip().splitlines() if value: result["version"] = value[0][:200] result["exit_code"] = completed.returncode return result def resolved_vite(project: Path, timeout: float) -> dict[str, object]: """Report local package metadata and local .bin behavior without installing.""" package_path = project / "node_modules" / "vite" / "package.json" package: dict[str, object] = {"available": package_path.is_file()} if package_path.is_file(): try: data = json.loads(package_path.read_text(encoding="utf-8")) if isinstance(data, dict): version = data.get("version") if isinstance(version, str) and version: package["version"] = version package["path"] = str(package_path) except (OSError, json.JSONDecodeError) as exc: package["error"] = f"cannot read installed package: {exc}" binary_path = project / "node_modules" / ".bin" / "vite" binary: dict[str, object] = {"available": binary_path.is_file()} if binary_path.is_file(): binary["path"] = str(binary_path) binary.update(bounded_version([str(binary_path), "--version"], timeout)) return {"package": package, "executable": binary} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project", default=".", help="Vite project directory") parser.add_argument("--timeout", type=float, default=3.0, help="Version probe timeout") parser.add_argument("--json", action="store_true", help="Emit JSON (default)") args = parser.parse_args() if args.timeout <= 0: parser.error("--timeout must be positive") project = Path(args.project).expanduser().resolve() if not project.is_dir(): print(json.dumps({"error": f"project is not a directory: {project}"}), file=sys.stderr) return 1 package_file = project / "package.json" package_data: dict[str, object] = {} errors: list[str] = [] if package_file.is_file(): try: loaded = json.loads(package_file.read_text(encoding="utf-8")) if isinstance(loaded, dict): package_data = loaded else: errors.append("package.json is not an object") except (OSError, json.JSONDecodeError) as exc: errors.append(f"cannot read package.json: {exc}") else: errors.append("package.json not found") lockfiles = [name for name in LOCKFILES if (project / name).is_file()] manager_name = LOCKFILES[lockfiles[0]] if lockfiles else None configs = sorted( path.name for path in project.iterdir() if path.is_file() and path.name.startswith("vite.config") ) dependencies: dict[str, object] = {} for section in ("dependencies", "devDependencies", "optionalDependencies"): values = package_data.get(section) if isinstance(values, dict) and "vite" in values: dependencies[section] = values["vite"] payload = { "project": str(project), "package_json": package_file.is_file(), "package_manager": { "name": manager_name, "lockfiles": lockfiles, "version": bounded_version(MANAGERS[manager_name], args.timeout) if manager_name else None, }, "node": bounded_version(["node", "--version"], args.timeout), "vite_dependency": dependencies, "vite_resolved": resolved_vite(project, args.timeout), "config_files": configs, "env_file_names": sorted( path.name for path in project.iterdir() if path.is_file() and path.name.startswith(".env") ), "errors": errors, } print(json.dumps(payload, indent=2, sort_keys=True)) return 1 if errors else 0 if __name__ == "__main__": raise SystemExit(main())