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:
ShitK 2026-07-29 08:08:33 +08:00 committed by GitHub
parent 9bb8225079
commit 4e44938551
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 500 additions and 71 deletions

View File

@ -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)**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
# ---------------------------------------------------------------------------

View 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

View File

@ -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": [

View File

@ -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

View File

@ -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")

View File

@ -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
View 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())

View File

@ -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)"

View File

@ -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),