mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-29 01:15:59 +00:00
fix: align pnpm consumers with Corepack fallback (#4405)
* fix: align pnpm consumers with Corepack fallback * fix: run pnpm helper from frontend workspace * fix: preserve Corepack resolution hint
This commit is contained in:
parent
9bb8225079
commit
4e44938551
@ -113,6 +113,8 @@ cd frontend && pnpm test # Unit tests
|
||||
Rule of thumb: **root `make` = the full application**; **`backend/Makefile` and `frontend/`
|
||||
(`pnpm`) = per-module work.**
|
||||
|
||||
Host-side pnpm consumers, including the root/frontend Makefiles and local diagnostic scripts, must run through `scripts/pnpm.py`. The runner preserves direct `pnpm`/`pnpm.cmd` priority, falls back to `corepack pnpm`, and is invoked from `frontend/` so Corepack honors the package-manager version pinned by that project.
|
||||
|
||||
## Where to Go Next
|
||||
|
||||
- Backend work → **[backend/AGENTS.md](backend/AGENTS.md)**
|
||||
|
||||
4
Makefile
4
Makefile
@ -16,6 +16,8 @@ else
|
||||
RUN_WITH_GIT_BASH =
|
||||
endif
|
||||
|
||||
FRONTEND_PNPM = $(PYTHON) ../scripts/pnpm.py
|
||||
|
||||
help:
|
||||
@echo "DeerFlow Development Commands:"
|
||||
@echo " make setup - Interactive setup wizard (recommended for new users)"
|
||||
@ -80,7 +82,7 @@ install:
|
||||
@echo "Installing backend dependencies..."
|
||||
@cd backend && uv sync
|
||||
@echo "Installing frontend dependencies..."
|
||||
@cd frontend && pnpm install
|
||||
@cd frontend && $(FRONTEND_PNPM) install
|
||||
@echo "Installing pre-commit hooks..."
|
||||
@uv tool install pre-commit
|
||||
@pre-commit install --overwrite
|
||||
|
||||
@ -306,6 +306,8 @@ On Windows, run the local development flow from Git Bash. Native `cmd.exe` and P
|
||||
make check # Verifies Node.js 22+, pnpm, uv, nginx
|
||||
```
|
||||
|
||||
The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm`/`pnpm.cmd` executable when available and otherwise fall back to `corepack pnpm`. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required.
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
make install # Install backend + frontend dependencies + pre-commit hooks
|
||||
|
||||
@ -1,20 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
CHECK_SCRIPT_PATH = REPO_ROOT / "scripts" / "check.py"
|
||||
PNPM_SCRIPT_PATH = REPO_ROOT / "scripts" / "pnpm.py"
|
||||
|
||||
|
||||
spec = importlib.util.spec_from_file_location("deerflow_check_script", CHECK_SCRIPT_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
check_script = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(check_script)
|
||||
def _load_script(path: Path, name: str):
|
||||
assert path.exists(), f"{path} must exist"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_find_pnpm_command_prefers_resolved_executable(monkeypatch):
|
||||
pnpm_script = _load_script(PNPM_SCRIPT_PATH, "deerflow_pnpm_script_direct")
|
||||
|
||||
def fake_which(name: str) -> str | None:
|
||||
if name == "pnpm":
|
||||
return r"C:\Users\tester\AppData\Roaming\npm\pnpm.CMD"
|
||||
@ -22,26 +29,30 @@ def test_find_pnpm_command_prefers_resolved_executable(monkeypatch):
|
||||
return r"C:\Users\tester\AppData\Roaming\npm\pnpm.cmd"
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(check_script.shutil, "which", fake_which)
|
||||
monkeypatch.setattr(pnpm_script.shutil, "which", fake_which)
|
||||
|
||||
assert check_script.find_pnpm_command() == [r"C:\Users\tester\AppData\Roaming\npm\pnpm.CMD"]
|
||||
assert pnpm_script.find_pnpm_command() == [r"C:\Users\tester\AppData\Roaming\npm\pnpm.CMD"]
|
||||
|
||||
|
||||
def test_find_pnpm_command_falls_back_to_corepack(monkeypatch):
|
||||
pnpm_script = _load_script(PNPM_SCRIPT_PATH, "deerflow_pnpm_script_corepack")
|
||||
|
||||
def fake_which(name: str) -> str | None:
|
||||
if name == "corepack":
|
||||
return r"C:\Program Files\nodejs\corepack.exe"
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(check_script.shutil, "which", fake_which)
|
||||
monkeypatch.setattr(pnpm_script.shutil, "which", fake_which)
|
||||
|
||||
assert check_script.find_pnpm_command() == [
|
||||
assert pnpm_script.find_pnpm_command() == [
|
||||
r"C:\Program Files\nodejs\corepack.exe",
|
||||
"pnpm",
|
||||
]
|
||||
|
||||
|
||||
def test_find_pnpm_command_falls_back_to_corepack_cmd(monkeypatch):
|
||||
pnpm_script = _load_script(PNPM_SCRIPT_PATH, "deerflow_pnpm_script_corepack_cmd")
|
||||
|
||||
def fake_which(name: str) -> str | None:
|
||||
if name == "corepack":
|
||||
return None
|
||||
@ -49,9 +60,77 @@ def test_find_pnpm_command_falls_back_to_corepack_cmd(monkeypatch):
|
||||
return r"C:\Program Files\nodejs\corepack.cmd"
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(check_script.shutil, "which", fake_which)
|
||||
monkeypatch.setattr(pnpm_script.shutil, "which", fake_which)
|
||||
|
||||
assert check_script.find_pnpm_command() == [
|
||||
assert pnpm_script.find_pnpm_command() == [
|
||||
r"C:\Program Files\nodejs\corepack.cmd",
|
||||
"pnpm",
|
||||
]
|
||||
|
||||
|
||||
def test_check_script_uses_shared_pnpm_runner():
|
||||
check_script = CHECK_SCRIPT_PATH.read_text(encoding="utf-8")
|
||||
|
||||
assert 'Path(__file__).with_name("pnpm.py")' in check_script
|
||||
|
||||
|
||||
def test_check_script_preserves_runner_failure_diagnostics(monkeypatch):
|
||||
check_script = _load_script(CHECK_SCRIPT_PATH, "deerflow_check_script_failure")
|
||||
call_kwargs = {}
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
call_kwargs.update(kwargs)
|
||||
return subprocess.CompletedProcess(
|
||||
args=["python", "pnpm.py", "-v"],
|
||||
returncode=42,
|
||||
stdout="partial pnpm output\n",
|
||||
stderr="Error: pnpm command failed with exit status 42.\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(check_script.subprocess, "run", fake_run)
|
||||
|
||||
assert check_script.run_pnpm_version() == (
|
||||
None,
|
||||
False,
|
||||
"Error: pnpm command failed with exit status 42.\npartial pnpm output",
|
||||
)
|
||||
assert call_kwargs["cwd"] == REPO_ROOT / "frontend"
|
||||
|
||||
|
||||
def test_check_script_preserves_corepack_resolution_hint(monkeypatch):
|
||||
check_script = _load_script(CHECK_SCRIPT_PATH, "deerflow_check_script_corepack")
|
||||
|
||||
def fake_run(*args, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
args=["python", "pnpm.py", "-v"],
|
||||
returncode=0,
|
||||
stdout="10.26.2\n",
|
||||
stderr="Using pnpm via Corepack.\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(check_script.subprocess, "run", fake_run)
|
||||
|
||||
assert check_script.run_pnpm_version() == ("10.26.2", True, None)
|
||||
|
||||
|
||||
def test_check_status_labels_corepack_fallback(monkeypatch, capsys):
|
||||
check_script = _load_script(CHECK_SCRIPT_PATH, "deerflow_check_script_status")
|
||||
|
||||
monkeypatch.setattr(check_script.shutil, "which", lambda name: f"/fake/{name}")
|
||||
monkeypatch.setattr(
|
||||
check_script,
|
||||
"run_command",
|
||||
lambda command: {
|
||||
"node": "v22.0.0",
|
||||
"uv": "uv 0.11.31",
|
||||
"nginx": "nginx/1.31.3",
|
||||
}[command[0]],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
check_script,
|
||||
"run_pnpm_version",
|
||||
lambda: ("10.26.2", True, None),
|
||||
)
|
||||
|
||||
assert check_script.main() == 0
|
||||
assert "OK pnpm 10.26.2 (via Corepack)" in capsys.readouterr().out
|
||||
|
||||
@ -22,6 +22,50 @@ class TestCheckPython:
|
||||
assert result.status == "ok"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check_pnpm
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckPnpm:
|
||||
def test_uses_shared_runner_from_frontend(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
captured["cmd"] = cmd
|
||||
captured["kwargs"] = kwargs
|
||||
return doctor.subprocess.CompletedProcess(cmd, 0, stdout="10.26.2\n", stderr="")
|
||||
|
||||
monkeypatch.setattr(doctor.subprocess, "run", fake_run)
|
||||
|
||||
result = doctor.check_pnpm()
|
||||
|
||||
expected_runner = doctor.Path(doctor.__file__).with_name("pnpm.py")
|
||||
assert result.status == "ok"
|
||||
assert result.detail == "10.26.2"
|
||||
assert captured["cmd"] == [sys.executable, str(expected_runner), "-v"]
|
||||
assert captured["kwargs"]["cwd"] == expected_runner.parent.parent / "frontend"
|
||||
assert captured["kwargs"]["shell"] is False
|
||||
assert captured["kwargs"]["check"] is False
|
||||
|
||||
def test_runner_failure_is_reported_as_failure(self, monkeypatch):
|
||||
def fake_run(cmd, **kwargs):
|
||||
return doctor.subprocess.CompletedProcess(
|
||||
cmd,
|
||||
42,
|
||||
stdout="",
|
||||
stderr="Error: pnpm command failed with exit status 42.\n",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(doctor.subprocess, "run", fake_run)
|
||||
|
||||
result = doctor.check_pnpm()
|
||||
|
||||
assert result.status == "fail"
|
||||
assert "exit status 42" in result.detail
|
||||
assert result.fix is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check_config_exists
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
142
backend/tests/test_pnpm_script.py
Normal file
142
backend/tests/test_pnpm_script.py
Normal file
@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
PNPM_SCRIPT = REPO_ROOT / "scripts" / "pnpm.py"
|
||||
FRONTEND_DIR = REPO_ROOT / "frontend"
|
||||
|
||||
|
||||
def _write_fake_command(bin_dir: Path, name: str, label: str, exit_code: int = 0) -> Path:
|
||||
if os.name == "nt":
|
||||
path = bin_dir / f"{name}.cmd"
|
||||
path.write_text(
|
||||
f"@echo off\r\necho {label}^|%CD%^|%*\r\nexit /b {exit_code}\r\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
else:
|
||||
path = bin_dir / name
|
||||
path.write_text(
|
||||
f"#!/bin/sh\nprintf '%s|%s|%s\\n' '{label}' \"$PWD\" \"$*\"\nexit {exit_code}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
path.chmod(0o755)
|
||||
return path
|
||||
|
||||
|
||||
def _run_pnpm(
|
||||
path: Path,
|
||||
*args: str,
|
||||
cwd: Path = FRONTEND_DIR,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(path)
|
||||
return subprocess.run(
|
||||
[sys.executable, str(PNPM_SCRIPT), *args],
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
|
||||
|
||||
def test_runner_prefers_direct_pnpm_and_forwards_arguments(tmp_path: Path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
_write_fake_command(bin_dir, "pnpm", "direct")
|
||||
_write_fake_command(bin_dir, "corepack", "corepack")
|
||||
|
||||
result = _run_pnpm(bin_dir, "run", "dev", "--host", "127.0.0.1")
|
||||
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == f"direct|{FRONTEND_DIR}|run dev --host 127.0.0.1"
|
||||
assert "via Corepack" not in result.stderr
|
||||
|
||||
|
||||
def test_runner_uses_corepack_pnpm_from_frontend_directory(tmp_path: Path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
_write_fake_command(bin_dir, "corepack", "corepack")
|
||||
|
||||
result = _run_pnpm(bin_dir, "--version")
|
||||
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == f"corepack|{FRONTEND_DIR}|pnpm --version"
|
||||
assert result.stderr.strip() == "Using pnpm via Corepack."
|
||||
|
||||
package_json = json.loads((FRONTEND_DIR / "package.json").read_text(encoding="utf-8"))
|
||||
assert package_json["packageManager"] == "pnpm@10.26.2"
|
||||
|
||||
|
||||
def test_runner_uses_frontend_directory_when_called_from_repo_root(tmp_path: Path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
_write_fake_command(bin_dir, "corepack", "corepack")
|
||||
|
||||
result = _run_pnpm(bin_dir, "--version", cwd=REPO_ROOT)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == f"corepack|{FRONTEND_DIR}|pnpm --version"
|
||||
|
||||
|
||||
def test_runner_reports_actionable_error_when_pnpm_and_corepack_are_missing(tmp_path: Path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
|
||||
result = _run_pnpm(bin_dir, "--version")
|
||||
|
||||
assert result.returncode == 127
|
||||
assert "Neither pnpm nor Corepack is available" in result.stderr
|
||||
assert "ensure 'corepack' is on PATH" in result.stderr
|
||||
|
||||
|
||||
def test_runner_propagates_selected_pnpm_failure(tmp_path: Path):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
_write_fake_command(bin_dir, "pnpm", "broken-direct", exit_code=42)
|
||||
_write_fake_command(bin_dir, "corepack", "unused-corepack")
|
||||
|
||||
result = _run_pnpm(bin_dir, "install", "--frozen-lockfile")
|
||||
|
||||
assert result.returncode == 42
|
||||
assert result.stdout.strip() == f"broken-direct|{FRONTEND_DIR}|install --frozen-lockfile"
|
||||
assert "pnpm command failed with exit status 42" in result.stderr
|
||||
|
||||
|
||||
def test_official_entrypoints_route_pnpm_through_shared_runner():
|
||||
root_makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
||||
frontend_makefile = (FRONTEND_DIR / "Makefile").read_text(encoding="utf-8")
|
||||
serve_script = (REPO_ROOT / "scripts" / "serve.sh").read_text(encoding="utf-8")
|
||||
doctor_script = (REPO_ROOT / "scripts" / "doctor.py").read_text(encoding="utf-8")
|
||||
support_bundle_script = (REPO_ROOT / "scripts" / "support_bundle.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "cd frontend && $(FRONTEND_PNPM) install" in root_makefile
|
||||
assert "PNPM = $(PYTHON) ../scripts/pnpm.py" in frontend_makefile
|
||||
assert '"$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" install --silent' in serve_script
|
||||
assert 'DEERFLOW_PNPM_RUNNER="$REPO_ROOT/scripts/pnpm.py"' in serve_script
|
||||
assert 'FRONTEND_CMD=\'"$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev\'' in serve_script
|
||||
assert '"\\$DEERFLOW_PNPM_RUNNER\\" run preview"' in serve_script
|
||||
assert 'Path(__file__).with_name("pnpm.py")' in doctor_script
|
||||
assert 'project_root / "scripts" / "pnpm.py"' in support_bundle_script
|
||||
|
||||
|
||||
def test_make_install_dry_run_does_not_invoke_bare_pnpm():
|
||||
result = subprocess.run(
|
||||
["make", "-n", "install"],
|
||||
cwd=REPO_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "cd frontend && " in result.stdout
|
||||
assert "../scripts/pnpm.py install" in result.stdout
|
||||
assert "cd frontend && pnpm install" not in result.stdout
|
||||
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
@ -14,6 +15,27 @@ def _zip_text(zip_path, name: str) -> str:
|
||||
return zf.read(name).decode("utf-8")
|
||||
|
||||
|
||||
def test_collect_environment_routes_pnpm_through_shared_runner(tmp_path, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_version_command(name, args, cwd):
|
||||
calls.append((name, args, cwd))
|
||||
return {"name": name, "ok": True, "stdout": "version", "stderr": ""}
|
||||
|
||||
monkeypatch.setattr(support_bundle, "_version_command", fake_version_command)
|
||||
|
||||
support_bundle.collect_environment(tmp_path)
|
||||
|
||||
pnpm_calls = [call for call in calls if call[0] == "pnpm"]
|
||||
assert pnpm_calls == [
|
||||
(
|
||||
"pnpm",
|
||||
[sys.executable, str(tmp_path / "scripts" / "pnpm.py"), "--version"],
|
||||
tmp_path / "frontend",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_redact_data_recursively_masks_secret_like_keys():
|
||||
data = {
|
||||
"models": [
|
||||
|
||||
@ -1,24 +1,32 @@
|
||||
ifeq ($(OS),Windows_NT)
|
||||
PYTHON ?= python
|
||||
else
|
||||
PYTHON ?= python3
|
||||
endif
|
||||
|
||||
PNPM = $(PYTHON) ../scripts/pnpm.py
|
||||
|
||||
install:
|
||||
pnpm install
|
||||
$(PNPM) install
|
||||
|
||||
build:
|
||||
pnpm build
|
||||
$(PNPM) build
|
||||
|
||||
dev:
|
||||
pnpm dev
|
||||
$(PNPM) dev
|
||||
|
||||
test:
|
||||
pnpm test
|
||||
$(PNPM) test
|
||||
|
||||
test-e2e:
|
||||
pnpm test:e2e
|
||||
$(PNPM) test:e2e
|
||||
|
||||
lint:
|
||||
pnpm lint
|
||||
$(PNPM) lint
|
||||
|
||||
format:
|
||||
pnpm format:write
|
||||
$(PNPM) format:write
|
||||
|
||||
build-static:
|
||||
NEXT_CONFIG_BUILD_OUTPUT=standalone SKIP_ENV_VALIDATION=1 NEXT_PUBLIC_STATIC_WEBSITE_ONLY=true pnpm build
|
||||
NEXT_CONFIG_BUILD_OUTPUT=standalone SKIP_ENV_VALIDATION=1 NEXT_PUBLIC_STATIC_WEBSITE_ONLY=true $(PNPM) build
|
||||
@if [ -d .next/static ]; then mkdir -p .next/standalone/.next && cp -R .next/static .next/standalone/.next/static; fi
|
||||
|
||||
@ -8,6 +8,10 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PNPM_SCRIPT_PATH = Path(__file__).with_name("pnpm.py")
|
||||
FRONTEND_DIR = PNPM_SCRIPT_PATH.parent.parent / "frontend"
|
||||
COREPACK_NOTICE = "Using pnpm via Corepack."
|
||||
|
||||
|
||||
def configure_stdio() -> None:
|
||||
"""Prefer UTF-8 output so Unicode status markers render on Windows."""
|
||||
@ -23,28 +27,43 @@ def configure_stdio() -> None:
|
||||
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)
|
||||
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))]
|
||||
def run_pnpm_version() -> tuple[str | None, bool, str | None]:
|
||||
"""Return the pnpm version, resolution source, and failure message."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(PNPM_SCRIPT_PATH), "-v"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
except OSError as exc:
|
||||
return None, False, f"Unable to launch the pnpm runner: {exc}"
|
||||
|
||||
pnpm_cmd_path = shutil.which("pnpm.cmd")
|
||||
if pnpm_cmd_path:
|
||||
return [str(Path(pnpm_cmd_path))]
|
||||
stdout = result.stdout.strip()
|
||||
stderr_lines = result.stderr.splitlines()
|
||||
via_corepack = COREPACK_NOTICE in stderr_lines
|
||||
stderr = "\n".join(line for line in stderr_lines if line != COREPACK_NOTICE).strip()
|
||||
if result.returncode == 0 and (stdout or stderr):
|
||||
return stdout or stderr, via_corepack, None
|
||||
|
||||
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
|
||||
diagnostics = "\n".join(part for part in (stderr, stdout) if part)
|
||||
if diagnostics:
|
||||
return None, via_corepack, diagnostics
|
||||
return (
|
||||
None,
|
||||
via_corepack,
|
||||
f"The pnpm runner exited with status {result.returncode} without output.",
|
||||
)
|
||||
|
||||
|
||||
def parse_node_major(version_text: str) -> int | None:
|
||||
@ -91,22 +110,15 @@ def main() -> int:
|
||||
|
||||
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
|
||||
pnpm_version, pnpm_via_corepack, pnpm_error = run_pnpm_version()
|
||||
if pnpm_version:
|
||||
resolution_hint = " (via Corepack)" if pnpm_via_corepack else ""
|
||||
print(f" OK pnpm {pnpm_version}{resolution_hint}")
|
||||
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")
|
||||
print(" FAIL pnpm is unavailable or failed to run")
|
||||
if pnpm_error:
|
||||
for line in pnpm_error.splitlines():
|
||||
print(f" {line}")
|
||||
failed = True
|
||||
|
||||
print()
|
||||
@ -115,7 +127,9 @@ def main() -> int:
|
||||
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
|
||||
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")
|
||||
|
||||
@ -24,6 +24,8 @@ from typing import Literal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Status = Literal["ok", "warn", "fail", "skip"]
|
||||
PNPM_SCRIPT_PATH = Path(__file__).with_name("pnpm.py")
|
||||
FRONTEND_DIR = PNPM_SCRIPT_PATH.parent.parent / "frontend"
|
||||
|
||||
|
||||
def _supports_color() -> bool:
|
||||
@ -165,18 +167,41 @@ def check_node() -> CheckResult:
|
||||
|
||||
|
||||
def check_pnpm() -> CheckResult:
|
||||
candidates = [["pnpm"], ["pnpm.cmd"]]
|
||||
if shutil.which("corepack"):
|
||||
candidates.append(["corepack", "pnpm"])
|
||||
for cmd in candidates:
|
||||
if shutil.which(cmd[0]):
|
||||
out = _run([*cmd, "-v"]) or ""
|
||||
return CheckResult("pnpm", "ok", out)
|
||||
return CheckResult(
|
||||
"pnpm",
|
||||
"fail",
|
||||
fix="npm install -g pnpm (or: corepack enable)",
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(PNPM_SCRIPT_PATH), "-v"],
|
||||
cwd=FRONTEND_DIR,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
shell=False,
|
||||
)
|
||||
except OSError as exc:
|
||||
return CheckResult(
|
||||
"pnpm",
|
||||
"fail",
|
||||
f"Unable to run pnpm resolver: {exc}",
|
||||
fix="Install pnpm, or install Corepack and ensure it is on PATH",
|
||||
)
|
||||
|
||||
stdout = (result.stdout or "").strip()
|
||||
stderr = (result.stderr or "").strip()
|
||||
if result.returncode != 0:
|
||||
detail = "\n".join(part for part in (stderr, stdout) if part)
|
||||
return CheckResult(
|
||||
"pnpm",
|
||||
"fail",
|
||||
detail or f"pnpm resolver exited with status {result.returncode}",
|
||||
fix="Install pnpm, or install Corepack and ensure it is on PATH",
|
||||
)
|
||||
if not stdout:
|
||||
return CheckResult(
|
||||
"pnpm",
|
||||
"fail",
|
||||
stderr or "pnpm resolver returned no version",
|
||||
fix="Install pnpm, or install Corepack and ensure it is on PATH",
|
||||
)
|
||||
return CheckResult("pnpm", "ok", stdout)
|
||||
|
||||
|
||||
def check_uv() -> CheckResult:
|
||||
|
||||
76
scripts/pnpm.py
Normal file
76
scripts/pnpm.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run pnpm directly when available, otherwise run it through Corepack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
|
||||
COREPACK_NOTICE = "Using pnpm via Corepack."
|
||||
|
||||
|
||||
def find_pnpm_command() -> list[str] | None:
|
||||
"""Return the preferred pnpm-compatible command for 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 run_pnpm(arguments: Sequence[str]) -> int:
|
||||
"""Run pnpm with the supplied arguments and propagate its exit status."""
|
||||
command = find_pnpm_command()
|
||||
if command is None:
|
||||
print(
|
||||
"Error: Neither pnpm nor Corepack is available on PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"Install pnpm, or install Corepack and ensure 'corepack' is on PATH.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 127
|
||||
|
||||
if Path(command[0]).stem.lower() == "corepack":
|
||||
print(COREPACK_NOTICE, file=sys.stderr)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[*command, *arguments],
|
||||
check=False,
|
||||
shell=False,
|
||||
cwd=FRONTEND_DIR,
|
||||
)
|
||||
except OSError as exc:
|
||||
print(f"Error: Failed to run pnpm via {command[0]}: {exc}", file=sys.stderr)
|
||||
return 126
|
||||
|
||||
exit_code = 128 - result.returncode if result.returncode < 0 else result.returncode
|
||||
if exit_code != 0:
|
||||
print(
|
||||
f"Error: pnpm command failed with exit status {exit_code}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return exit_code
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
return run_pnpm(sys.argv[1:] if argv is None else argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@ -293,15 +293,20 @@ if $DAEMON_MODE; then
|
||||
MODE_LABEL="$MODE_LABEL [daemon]"
|
||||
fi
|
||||
|
||||
# Resolve pnpm through the same runner used by make check/install. Exporting
|
||||
# these values keeps paths with spaces intact when run_service invokes sh -c.
|
||||
if ! DEERFLOW_PNPM_PYTHON="$(_pick_python)"; then
|
||||
echo "Python 3 is required to run pnpm."
|
||||
exit 1
|
||||
fi
|
||||
DEERFLOW_PNPM_RUNNER="$REPO_ROOT/scripts/pnpm.py"
|
||||
export DEERFLOW_PNPM_PYTHON DEERFLOW_PNPM_RUNNER
|
||||
|
||||
# Frontend command
|
||||
if $DEV_MODE; then
|
||||
FRONTEND_CMD="pnpm run dev"
|
||||
FRONTEND_CMD='"$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev'
|
||||
else
|
||||
if ! PYTHON_BIN="$(_pick_python)"; then
|
||||
echo "Python is required to generate BETTER_AUTH_SECRET."
|
||||
exit 1
|
||||
fi
|
||||
FRONTEND_CMD="env BETTER_AUTH_SECRET=$($PYTHON_BIN -c 'import secrets; print(secrets.token_hex(16))') pnpm run preview"
|
||||
FRONTEND_CMD="env BETTER_AUTH_SECRET=$($DEERFLOW_PNPM_PYTHON -c 'import secrets; print(secrets.token_hex(16))') \"\$DEERFLOW_PNPM_PYTHON\" \"\$DEERFLOW_PNPM_RUNNER\" run preview"
|
||||
fi
|
||||
|
||||
# Runtime path defaults. Local `make dev` launches Gateway from `backend/`,
|
||||
@ -386,7 +391,7 @@ if ! $SKIP_INSTALL; then
|
||||
# in particular). Required for postgres extras — see PR #2584.
|
||||
# Intentionally unquoted to splat multiple `--extra X` pairs.
|
||||
(cd backend && uv sync --quiet --all-packages $UV_EXTRAS_FLAGS) || { echo "✗ Backend dependency install failed"; exit 1; }
|
||||
(cd frontend && pnpm install --silent) || { echo "✗ Frontend dependency install failed"; exit 1; }
|
||||
(cd frontend && "$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" install --silent) || { echo "✗ Frontend dependency install failed"; exit 1; }
|
||||
echo "✓ Dependencies synced"
|
||||
else
|
||||
echo "⏩ Skipping dependency install (--skip-install)"
|
||||
|
||||
@ -210,7 +210,15 @@ def collect_environment(project_root: Path) -> dict[str, Any]:
|
||||
},
|
||||
"commands": [
|
||||
_version_command("node", ["node", "--version"], project_root),
|
||||
_version_command("pnpm", ["pnpm", "--version"], project_root),
|
||||
_version_command(
|
||||
"pnpm",
|
||||
[
|
||||
sys.executable,
|
||||
str(project_root / "scripts" / "pnpm.py"),
|
||||
"--version",
|
||||
],
|
||||
project_root / "frontend",
|
||||
),
|
||||
_version_command("uv", ["uv", "--version"], project_root),
|
||||
_version_command("nginx", ["nginx", "-v"], project_root),
|
||||
_version_command("docker", ["docker", "--version"], project_root),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user