Files
ZY-Agent/scripts/check.py
T
1445043649 83b680b2ea feat(check): postgres preflight in scripts/check.py
新增 check_postgres_preflight() —— make dev 链 (check.py → serve.sh) 的最后一道
preflight:
  - config.yaml 不存在 → silent skip(让 setup_wizard 引导)
  - backend != postgres → silent skip
  - DATABASE_URL 未设 → FAIL with hint
  - postgres 设了但 host:port 3s socket 不通 → FAIL with 启 docker 提示
  - 通则 OK + 显示 host:port

不在 serve.sh 里加:Makefile 已经把 check.py 串在 serve.sh 之前,FAIL 会
自然阻断启动;避免 bash + python 两处实现 PG 探测。

doctor.py 的 check_database 是事后诊断(make doctor);本 check 是事前
preflight(make dev/start)—— 互补。

Stage 0 PR2 T2.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:17:19 +08:00

260 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""Cross-platform dependency checker for DeerFlow."""
from __future__ import annotations
import os
import shutil
import socket
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlparse
def configure_stdio() -> None:
"""Prefer UTF-8 output so Unicode status markers render on Windows."""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
if hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
except (OSError, ValueError):
continue
def run_command(command: list[str]) -> str | None:
"""Run a command and return trimmed stdout, or None on failure."""
try:
result = subprocess.run(command, capture_output=True, text=True, check=True, shell=False)
except (OSError, subprocess.CalledProcessError):
return None
return result.stdout.strip() or result.stderr.strip()
def find_pnpm_command() -> list[str] | None:
"""Return a pnpm-compatible command that exists on this machine."""
pnpm_path = shutil.which("pnpm")
if pnpm_path:
return [str(Path(pnpm_path))]
pnpm_cmd_path = shutil.which("pnpm.cmd")
if pnpm_cmd_path:
return [str(Path(pnpm_cmd_path))]
corepack_path = shutil.which("corepack")
if not corepack_path:
corepack_path = shutil.which("corepack.cmd")
if corepack_path:
return [str(Path(corepack_path)), "pnpm"]
return None
def parse_node_major(version_text: str) -> int | None:
version = version_text.strip()
if version.startswith("v"):
version = version[1:]
major_str = version.split(".", 1)[0]
if not major_str.isdigit():
return None
return int(major_str)
def check_postgres_preflight() -> tuple[bool, str | None]:
"""If config.yaml selects postgres, verify the URL is set + host:port reachable.
Returns ``(ok, message)``. Skips silently when:
- config.yaml does not exist (user hasn't run 'make setup' yet)
- database.backend is not 'postgres' (sqlite/memory don't need preflight)
- DATABASE_URL env var is not set (user hasn't filled .env yet)
Fails when database.backend is 'postgres' AND DATABASE_URL parses but the
host:port socket cannot be opened in 3 seconds.
"""
repo_root = Path(__file__).resolve().parent.parent
config_path = repo_root / "config.yaml"
if not config_path.exists():
return True, None # No config yet — let setup_wizard handle it
try:
import yaml # type: ignore[import-not-found]
except ImportError:
return True, "PyYAML not installed; skipping Postgres preflight"
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
except Exception as exc:
return False, f"Failed to parse config.yaml: {exc}"
db = data.get("database") or {}
backend = (db.get("backend") or "sqlite").lower()
if backend != "postgres":
return True, None # not on PG, no preflight needed
raw_url = db.get("postgres_url") or db.get("url") or ""
if isinstance(raw_url, str) and raw_url.startswith("$"):
env_name = raw_url[1:]
# Load .env if available so this preflight matches what serve.sh sees
env_path = repo_root / ".env"
if env_path.exists():
try:
from dotenv import load_dotenv # type: ignore[import-not-found]
load_dotenv(env_path, override=False)
except ImportError:
pass
url = os.environ.get(env_name, "")
if not url:
return False, f"database.backend=postgres but {env_name} is not set in .env"
else:
url = raw_url
if not url:
return False, "database.backend=postgres but no postgres_url is configured"
parsed = urlparse(url.replace("postgresql+asyncpg://", "postgresql://"))
host = parsed.hostname
port = parsed.port or 5432
if not host:
return False, f"Cannot parse host from DATABASE_URL: {url[:60]}..."
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
try:
sock.connect((host, port))
return True, f"Postgres reachable at {host}:{port}"
except OSError as exc:
return (
False,
f"Postgres unreachable at {host}:{port} ({exc}). "
f"Run 'docker compose -f docker/docker-compose-dev.yaml up -d postgres' "
f"or check DATABASE_URL.",
)
finally:
sock.close()
def main() -> int:
configure_stdio()
print("==========================================")
print(" Checking Required Dependencies")
print("==========================================")
print()
failed = False
print("Checking Node.js...")
node_path = shutil.which("node")
if node_path:
node_version = run_command(["node", "-v"])
if node_version:
major = parse_node_major(node_version)
if major is not None and major >= 22:
print(f" OK Node.js {node_version.lstrip('v')} (>= 22 required)")
else:
print(
f" FAIL Node.js {node_version.lstrip('v')} found, but version 22+ is required"
)
print(" Install from: https://nodejs.org/")
failed = True
else:
print(" INFO Unable to determine Node.js version")
print(" Install from: https://nodejs.org/")
failed = True
else:
print(" FAIL Node.js not found (version 22+ required)")
print(" Install from: https://nodejs.org/")
failed = True
print()
print("Checking pnpm...")
pnpm_command = find_pnpm_command()
if pnpm_command:
pnpm_version = run_command([*pnpm_command, "-v"])
if pnpm_version:
if Path(pnpm_command[0]).stem.lower() == "corepack":
print(f" OK pnpm {pnpm_version} (via Corepack)")
else:
print(f" OK pnpm {pnpm_version}")
else:
print(" INFO Unable to determine pnpm version")
failed = True
else:
print(" FAIL pnpm not found")
print(" Install: npm install -g pnpm")
print(" Or enable Corepack: corepack enable")
print(" Or visit: https://pnpm.io/installation")
failed = True
print()
print("Checking uv...")
if shutil.which("uv"):
uv_version_text = run_command(["uv", "--version"])
if uv_version_text:
uv_version_parts = uv_version_text.split()
uv_version = uv_version_parts[1] if len(uv_version_parts) > 1 else uv_version_text
print(f" OK uv {uv_version}")
else:
print(" INFO Unable to determine uv version")
failed = True
else:
print(" FAIL uv not found")
print(" Visit the official installation guide for your platform:")
print(" https://docs.astral.sh/uv/getting-started/installation/")
failed = True
print()
print("Checking nginx...")
if shutil.which("nginx"):
nginx_version_text = run_command(["nginx", "-v"])
if nginx_version_text and "/" in nginx_version_text:
nginx_version = nginx_version_text.split("/", 1)[1]
print(f" OK nginx {nginx_version}")
else:
print(" INFO nginx (version unknown)")
else:
print(" FAIL nginx not found")
print(" macOS: brew install nginx")
print(" Ubuntu: sudo apt install nginx")
print(" Windows: use WSL for local mode or use Docker mode")
print(" Or visit: https://nginx.org/en/download.html")
failed = True
print()
print("Checking Postgres preflight...")
pg_ok, pg_msg = check_postgres_preflight()
if pg_ok:
if pg_msg:
print(f" OK {pg_msg}")
else:
print(" -- skipped (config.yaml missing or backend != postgres)")
else:
print(f" FAIL {pg_msg}")
failed = True
print()
if not failed:
print("==========================================")
print(" OK All dependencies are installed!")
print("==========================================")
print()
print("You can now run:")
print(" make install - Install project dependencies")
print(" make setup - Create a minimal working config (recommended)")
print(" make config - Copy the full config template (manual setup)")
print(" make doctor - Verify config and dependency health")
print(" make dev - Start development server")
print(" make start - Start production server")
return 0
print("==========================================")
print(" FAIL Some dependencies are missing")
print("==========================================")
print()
print("Please install the missing tools and run 'make check' again.")
return 1
if __name__ == "__main__":
sys.exit(main())