Files
zemion 2ffdb23f69
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m30s
feat(devkit): add resumable workspace automation and UI review tooling
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:17 +02:00

233 lines
9.7 KiB
Python
Executable File

"""Read-only environment preflight with actionable, never automatic repairs."""
from __future__ import annotations
from dataclasses import replace
import os
from pathlib import Path
import socket
from .common import now
from .environment import execution_environment, resolve_tools, tool_version
from .workspace import inspect_repository, load_project, selected_repositories
from .process import require_capture
def _required_tools(project, workspace_root, selected, *, filtered, profile=None):
"""Use the catalog's selected commands, including dependencies, without running them."""
from .catalog import _custom_stages
if profile is None:
# Doctor covers all declared profiles by default, or all declared checks
# when the project only uses the context/doctor commands so far.
profiles = project.config.get("profiles", {})
records = project.config.get("checks", [])
if not isinstance(profiles, dict) or not isinstance(records, list):
raise ValueError(
"Project profiles and checks must have their declared shapes"
)
identities = []
if profiles:
for values in profiles.values():
if not isinstance(values, list) or any(
not isinstance(value, str) for value in values
):
raise ValueError("A project profile must be a list of check IDs")
identities.extend(values)
else:
if any(not isinstance(record, dict) for record in records):
raise ValueError("Project checks must be objects")
identities = [record.get("id") for record in records]
if any(not isinstance(identity, str) for identity in identities):
raise ValueError("Project checks must have string IDs")
project = replace(
project,
config={
**project.config,
"profiles": {"quick": list(dict.fromkeys(identities))},
},
)
profile = "quick"
stages = _custom_stages(project, workspace_root, profile, selected, filtered)
# Python is required by the runner and its environment fingerprint even if
# all selected check commands use another interpreter. Explicitly configured
# tools also declare requirements for commands hidden inside project scripts.
required = {"python", *project.config.get("tools", {})}
for stage in stages:
argv = stage["argv"]
for name in ("node", "npm", "python"):
if any("{" + name + "}" in value for value in argv):
required.add(name)
executable = Path(argv[0]).name
if executable in {"node", "nodejs"}:
required.add("node")
elif executable in {"npm", "npx"}:
required.update({"npm", "node"})
if "npm" in required:
required.add("node")
return required
def diagnose(args) -> dict:
project = load_project(args.workspace_root, args.project)
tools = resolve_tools(args.workspace_root, project)
env = execution_environment(args.workspace_root, project, tools)
selected = selected_repositories(project, args.repo)
required = (
_required_tools(
project,
args.workspace_root,
selected,
filtered=bool(args.repo),
profile=getattr(args, "profile", None),
)
if args.project
else set(tools)
)
checks = []
for name, executable in tools.items():
if name not in required:
checks.append(
{
"id": name,
"status": "not_required",
"detail": "Not required by the selected declared project checks; not probed.",
"path": executable,
"repair": "For indirect dependencies inside scripts, declare the tool under project tools.",
}
)
continue
version = tool_version(executable, env)
checks.append(
{
"id": name,
"status": "passed" if version != "unavailable" else "blocked",
"detail": version,
"path": executable,
"repair": f"Install/configure {name}; set {name.upper()} or project tools.{name}. No installation was attempted.",
}
)
for repo in selected:
snapshot = inspect_repository(repo)
checks.append(
{
"id": repo.name,
"status": "blocked" if snapshot["errors"] else "passed",
"detail": "; ".join(snapshot["errors"])
or "Git checkout readable (dirty work is allowed).",
"repair": "Use tools/repo/bootstrap-repositories.py --check; review missing checkouts before cloning.",
}
)
for package_dir in (repo.path, repo.path / "webui"):
if (package_dir / "package.json").is_file():
present = (package_dir / "node_modules").is_dir()
checks.append(
{
"id": f"dependencies:{repo.name}:{package_dir.name}",
"status": "passed" if present else "warning",
"detail": "Dependency directory exists; availability is not a full dependency audit."
if present
else "No local node_modules directory; workspace-hoisted packages may still resolve.",
"repair": "Inspect the owning package lock and install instructions before running npm ci.",
}
)
if not args.project:
script = args.workspace_root / "govoplan/tools/repo/sync-python-environment.py"
if script.is_file():
result = require_capture(
[
tools["python"],
str(script),
"--check",
"--requirements",
str(args.workspace_root / "govoplan/requirements-dev.txt"),
"--python",
tools["python"],
],
timeout=60,
max_stdout=65536,
env=env,
)
checks.append(
{
"id": "python-environment-sync",
"status": "passed" if result.returncode == 0 else "warning",
"detail": "Environment synchronization fingerprint is current."
if result.returncode == 0
else "Environment sync check did not pass; inspect its --dry-run output.",
"repair": "./.venv/bin/python tools/repo/sync-python-environment.py --dry-run --requirements requirements-dev.txt --python ./.venv/bin/python",
}
)
browser_roots = [
Path.home() / ".cache/ms-playwright",
Path.home() / ".var/app/com.vscodium.codium/cache/ms-playwright",
]
explicit_browser = os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH")
browser_available = (
(Path(explicit_browser).is_file() and os.access(explicit_browser, os.X_OK))
if explicit_browser
else any(
root.is_dir() and any(root.glob("chromium-*/chrome-linux*/chrome"))
for root in browser_roots
)
)
checks.append(
{
"id": "browser",
"status": "passed" if browser_available else "warning",
"detail": "Configured/cached Chromium found."
if browser_available
else "Chromium executable not found; check PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH or install the pinned Playwright browser.",
"repair": "Follow Core WebUI conformance setup; do not start a development server to repair this.",
}
)
with socket.socket() as probe:
probe.settimeout(0.25)
busy = probe.connect_ex(("127.0.0.1", 4174)) == 0
checks.append(
{
"id": "browser-test-port",
"status": "warning" if busy else "passed",
"detail": "Port 4174 is occupied; do not kill another run."
if busy
else "Port 4174 is free now (not a reservation).",
"repair": "Wait for the owning check run; use devkit resource coordination.",
}
)
summary = [
f"Environment preflight: {sum(item['status'] == 'blocked' for item in checks)} blockers, {sum(item['status'] == 'warning' for item in checks)} warnings."
]
summary.extend(
f"{item['id']}: {item['status']}{item['detail']}"
for item in checks
if item["status"] != "passed"
)
summary.append(
"Read-only: no installations, configuration repairs or servers started."
)
if args.project:
summary.append(
"Required tools follow declared checks and explicit project tools; indirect script dependencies must be declared explicitly."
)
return {
"schema_version": 1,
"generated_at": now(),
"checks": checks,
"required_tools": sorted(required),
"summary": summary,
"_exit_code": 1 if any(item["status"] == "blocked" for item in checks) else 0,
}
def register(subparsers):
parser = subparsers.add_parser(
"doctor", help="Read-only environment preflight and exact repair guidance"
)
parser.add_argument("--repo", action="append", default=[])
parser.add_argument(
"--profile",
choices=("quick", "ui", "backend", "full"),
help="For portable projects, limit tool requirements to this check profile (default: all declared profiles)",
)
parser.set_defaults(handler=diagnose)