Skip to content

Commit d3c1986

Browse files
committed
fix: avoid recursive promptfoo wrapper
1 parent 25d4ef7 commit d3c1986

File tree

1 file changed

+49
-7
lines changed

1 file changed

+49
-7
lines changed

src/promptfoo/cli.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
CLI wrapper for promptfoo
33
44
This module provides a thin wrapper around the promptfoo Node.js CLI tool.
5-
It executes the npx promptfoo command and passes through all arguments.
5+
It executes a global promptfoo binary when available, falling back to npx.
66
"""
77

8+
import os
89
import shutil
910
import subprocess
1011
import sys
11-
from typing import NoReturn
12+
from typing import NoReturn, Optional
13+
14+
_WRAPPER_ENV = "PROMPTFOO_PY_WRAPPER"
1215

1316

1417
def check_node_installed() -> bool:
@@ -34,6 +37,42 @@ def print_installation_help() -> None:
3437
print(" https://github.com/nvm-sh/nvm", file=sys.stderr)
3538

3639

40+
def _normalize_path(path: str) -> str:
41+
return os.path.normcase(os.path.abspath(path))
42+
43+
44+
def _resolve_argv0() -> Optional[str]:
45+
if not sys.argv:
46+
return None
47+
argv0 = sys.argv[0]
48+
if not argv0:
49+
return None
50+
if os.path.sep in argv0 or (os.path.altsep and os.path.altsep in argv0):
51+
return _normalize_path(argv0)
52+
resolved = shutil.which(argv0)
53+
if resolved:
54+
return _normalize_path(resolved)
55+
return None
56+
57+
58+
def _find_external_promptfoo() -> Optional[str]:
59+
promptfoo_path = shutil.which("promptfoo")
60+
if not promptfoo_path:
61+
return None
62+
argv0_path = _resolve_argv0()
63+
if argv0_path and _normalize_path(promptfoo_path) == argv0_path:
64+
wrapper_dir = _normalize_path(os.path.dirname(promptfoo_path))
65+
path_entries = [
66+
entry
67+
for entry in os.environ.get("PATH", "").split(os.pathsep)
68+
if entry and _normalize_path(entry) != wrapper_dir
69+
]
70+
if not path_entries:
71+
return None
72+
return shutil.which("promptfoo", path=os.pathsep.join(path_entries))
73+
return promptfoo_path
74+
75+
3776
def main() -> NoReturn:
3877
"""
3978
Main entry point for the promptfoo CLI wrapper.
@@ -45,19 +84,22 @@ def main() -> NoReturn:
4584
print_installation_help()
4685
sys.exit(1)
4786

48-
# Build command: try global promptfoo first, fall back to npx
49-
if shutil.which("promptfoo"):
50-
cmd = ["promptfoo"] + sys.argv[1:]
87+
# Build command: try external promptfoo first, fall back to npx
88+
promptfoo_path = None if os.environ.get(_WRAPPER_ENV) else _find_external_promptfoo()
89+
if promptfoo_path:
90+
cmd = [promptfoo_path] + sys.argv[1:]
91+
env = os.environ.copy()
92+
env[_WRAPPER_ENV] = "1"
93+
result = subprocess.run(cmd, env=env)
5194
elif shutil.which("npx"):
5295
cmd = ["npx", "-y", "promptfoo@latest"] + sys.argv[1:]
96+
result = subprocess.run(cmd)
5397
else:
5498
print("ERROR: Neither promptfoo nor npx is available.", file=sys.stderr)
5599
print("Please install promptfoo: npm install -g promptfoo", file=sys.stderr)
56100
print("Or ensure Node.js is properly installed.", file=sys.stderr)
57101
sys.exit(1)
58102

59-
# Execute with absolute minimal configuration
60-
result = subprocess.run(cmd)
61103
sys.exit(result.returncode)
62104

63105

0 commit comments

Comments
 (0)