108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from .auth import connect_with_personal_key, normalize_api_base_url
|
|
from .common import (
|
|
DEFAULT_ENV_NAME,
|
|
ENV_NAME_PATTERN,
|
|
SERVER_NAME,
|
|
Any,
|
|
Callable,
|
|
InstallerError,
|
|
Path,
|
|
Runner,
|
|
Which,
|
|
_project_root,
|
|
shutil,
|
|
subprocess,
|
|
sys,
|
|
)
|
|
from .diagnostics import doctor_project
|
|
from .project import inspect_project, prepare_project, render_client_config
|
|
|
|
|
|
def install_project(
|
|
project_root: str | Path | None,
|
|
api_base_url: str,
|
|
*,
|
|
env_name: str = DEFAULT_ENV_NAME,
|
|
enable_writes: bool = False,
|
|
runner: Runner = subprocess.run,
|
|
which: Which = shutil.which,
|
|
python_version: tuple[int, int, int] | None = None,
|
|
stdin_isatty: Callable[[], bool] = sys.stdin.isatty,
|
|
) -> dict[str, Any]:
|
|
"""Prepare, authenticate, diagnose, and describe one local installation.
|
|
|
|
The returned client configuration is intentionally not written here: the
|
|
installation Agent must merge it into its own client-specific format after
|
|
showing the destination and creating a recoverable backup.
|
|
"""
|
|
|
|
root = _project_root(project_root)
|
|
normalized_url = normalize_api_base_url(api_base_url)
|
|
if not ENV_NAME_PATTERN.fullmatch(env_name):
|
|
raise InstallerError(
|
|
"Environment name must start with a letter or digit and contain only letters, digits, dot, underscore, or dash"
|
|
)
|
|
if not stdin_isatty():
|
|
raise InstallerError(
|
|
"Secure API Key entry requires a user-controlled local terminal. "
|
|
"Do not paste the Key into Agent chat or pass it with --access-token."
|
|
)
|
|
|
|
inspection = inspect_project(root, which=which, python_version=python_version)
|
|
failed_prerequisites = [check["detail"] for check in inspection["checks"] if check["required"] and not check["ok"]]
|
|
if failed_prerequisites:
|
|
raise InstallerError("Installation prerequisites are not ready: " + "; ".join(failed_prerequisites))
|
|
|
|
prepared = prepare_project(
|
|
root,
|
|
runner=runner,
|
|
which=which,
|
|
python_version=python_version,
|
|
)
|
|
authentication = connect_with_personal_key(
|
|
root,
|
|
normalized_url,
|
|
env_name=env_name,
|
|
runner=runner,
|
|
which=which,
|
|
stdin_isatty=stdin_isatty,
|
|
)
|
|
diagnosis = doctor_project(
|
|
root,
|
|
runner=runner,
|
|
which=which,
|
|
python_version=python_version,
|
|
)
|
|
client_config = render_client_config(root, enable_writes=enable_writes, which=which)
|
|
local_ready = bool(diagnosis["ok"])
|
|
return {
|
|
"ok": local_ready,
|
|
"status": "ready-for-client-registration" if local_ready else "local-checks-failed",
|
|
"projectRoot": str(root),
|
|
"mode": "project-writes" if enable_writes else "read-only",
|
|
"prepared": {
|
|
"environmentPath": prepared["environmentPath"],
|
|
"dependencyProfile": prepared["dependencies"],
|
|
"configCreated": prepared["configCreated"],
|
|
},
|
|
"authentication": authentication,
|
|
"checks": diagnosis["checks"],
|
|
"clientConfig": client_config,
|
|
"clientRegistration": {
|
|
"required": True,
|
|
"serverName": SERVER_NAME,
|
|
"backupRequired": True,
|
|
"action": "merge the generated server entry, reload the client, then call crm_health",
|
|
},
|
|
"remainingActions": [
|
|
"Merge clientConfig into the current MCP client without replacing other servers.",
|
|
"Reload or restart the MCP client.",
|
|
"Call crm_health and require ok=true before reporting installation complete.",
|
|
],
|
|
}
|
|
|
|
|
|
__all__ = ["install_project"]
|