mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
Milestone v2.1: Contract-Driven Architecture & Engineering Hardening - Stop the Bleeding: version sync checker, PyYAML hardening, install docs unification - Contract Layer: PFResult/PFError dataclasses, ErrorCode enum, --json wrapping - Service Extraction: sync.py decomposed into 3 adapters + SyncService - State Machine: PdfStatus/OcrStatus/Lifecycle enums, transitions, field registry - Setup Modularization: setup_wizard.py decomposed into 6 classes 173 tests passing, 0 regressions.
103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""RuntimeInstaller -- pip install with version pinning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from paperforge.core.errors import ErrorCode
|
|
from paperforge.setup import SetupStepResult
|
|
|
|
|
|
ProgressCallback = Callable[[str], None]
|
|
|
|
|
|
class RuntimeInstaller:
|
|
"""Install PaperForge Python package with version pinning."""
|
|
|
|
def __init__(
|
|
self,
|
|
vault: Path,
|
|
version: str | None = None,
|
|
progress_callback: ProgressCallback | None = None,
|
|
):
|
|
self.vault = vault
|
|
self.version = version
|
|
self.progress_callback = progress_callback
|
|
|
|
def _log(self, message: str) -> None:
|
|
if self.progress_callback:
|
|
self.progress_callback(message)
|
|
|
|
def _pip_install(
|
|
self, package_spec: str
|
|
) -> tuple[bool, str, str]:
|
|
"""Run pip install and return (ok, stdout, stderr)."""
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", package_spec],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
return (result.returncode == 0, result.stdout, result.stderr)
|
|
except subprocess.TimeoutExpired:
|
|
return (False, "", "pip install timed out after 120s")
|
|
except Exception as e:
|
|
return (False, "", str(e))
|
|
|
|
def install(self) -> SetupStepResult:
|
|
"""Install paperforge via pip with optional version pin."""
|
|
self._log("Installing PaperForge...")
|
|
|
|
if self.version:
|
|
package_spec = (
|
|
f"git+https://github.com/LLLin000/PaperForge.git@{self.version}"
|
|
)
|
|
else:
|
|
package_spec = "git+https://github.com/LLLin000/PaperForge.git"
|
|
|
|
ok, stdout, stderr = self._pip_install(package_spec)
|
|
|
|
if ok:
|
|
return SetupStepResult(
|
|
step="runtime_installer",
|
|
ok=True,
|
|
message=(
|
|
f"PaperForge installed successfully{(' (' + self.version + ')') if self.version else ''}"
|
|
),
|
|
details={"version": self.version or "latest", "stdout": stdout[:500]},
|
|
)
|
|
else:
|
|
error_code = ErrorCode.INTERNAL_ERROR
|
|
if "pip" in stderr.lower():
|
|
error_code = ErrorCode.INTERNAL_ERROR
|
|
elif "connection" in stderr.lower() or "timeout" in stderr.lower():
|
|
error_code = ErrorCode.INTERNAL_ERROR
|
|
|
|
return SetupStepResult(
|
|
step="runtime_installer",
|
|
ok=False,
|
|
message="Failed to install PaperForge",
|
|
error=f"[{error_code.value}] {stderr[:500]}",
|
|
details={"version": self.version or "latest", "stderr": stderr[:500]},
|
|
)
|
|
|
|
def check_installed(self) -> bool:
|
|
"""Check if paperforge is already installed."""
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"from paperforge import __version__; print(__version__)",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
return result.returncode == 0 and bool(result.stdout.strip())
|
|
except Exception:
|
|
return False
|