from __future__ import annotations import argparse import json import os import re import shutil import subprocess import sys import tempfile from collections.abc import Callable, Sequence from dataclasses import asdict, dataclass from pathlib import Path from typing import Any from urllib.parse import urlsplit, urlunsplit MIN_PYTHON = (3, 12) DEFAULT_ENV_NAME = "nb-mcp" SERVER_NAME = "nocobase-crm" REQUIRED_PROJECT_FILES = ("pyproject.toml", "uv.lock", "config.example.json") ENV_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") ANSI_ESCAPE_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") Which = Callable[[str], str | None] Runner = Callable[..., subprocess.CompletedProcess[str]] class InstallerError(RuntimeError): """Raised when a deterministic installation step cannot finish safely.""" @dataclass(frozen=True) class Check: name: str ok: bool required: bool detail: str def _project_root(path: str | Path | None) -> Path: candidate = Path(path) if path is not None else Path(__file__).resolve().parent.parent return candidate.expanduser().resolve() def _load_json_object(path: Path) -> dict[str, Any]: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: raise InstallerError(f"Could not read valid JSON from {path}: {error}") from error if not isinstance(value, dict): raise InstallerError(f"Expected a JSON object in {path}") return value __all__ = [ "ANSI_ESCAPE_PATTERN", "Any", "Callable", "Check", "DEFAULT_ENV_NAME", "ENV_NAME_PATTERN", "InstallerError", "MIN_PYTHON", "Path", "REQUIRED_PROJECT_FILES", "Runner", "SERVER_NAME", "Sequence", "Which", "_load_json_object", "_project_root", "argparse", "asdict", "json", "os", "re", "shutil", "subprocess", "sys", "tempfile", "urlsplit", "urlunsplit", ]