Skip to content

Commit 66c739c

Browse files
mldangeloclaude
andcommitted
fix: add robust fallback from global to npx execution
When the global promptfoo executable fails to run (OSError, PermissionError), automatically fall back to npx. This handles edge cases like: - Resource temporarily unavailable (errno 35/EAGAIN on macOS) - Executable not ready immediately after npm install -g - Permission issues - Any other execution failures The wrapper now works reliably in all scenarios: 1. Global install exists and works: use it (fastest) 2. Global install exists but fails: fall back to npx (reliable) 3. No global install: use npx directly (works whether promptfoo is cached or not) This ensures the wrapper works whether promptfoo is pre-installed or being installed for the first time via npx. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a7ea6b1 commit 66c739c

File tree

1 file changed

+35
-20
lines changed

1 file changed

+35
-20
lines changed

src/promptfoo/cli.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,53 +39,68 @@ def main() -> NoReturn:
3939
"""
4040
Main entry point for the promptfoo CLI wrapper.
4141
42-
Tries to use globally installed promptfoo first, falls back to npx.
42+
Tries to use globally installed promptfoo first, falls back to npx if needed.
4343
Exits with the same exit code as the underlying promptfoo command.
4444
"""
4545
# Check for Node.js installation
4646
if not check_node_installed():
4747
print_installation_help()
4848
sys.exit(1)
4949

50-
# Try to find a globally installed promptfoo first (fastest, most reliable)
50+
# Try to find a globally installed promptfoo first (fastest when it works)
5151
# This avoids npm cache issues and download delays with npx
5252
promptfoo_path = shutil.which("promptfoo")
53+
used_global = False
5354

5455
if promptfoo_path:
55-
# Use the globally installed version (preferred)
56-
cmd = [promptfoo_path] + sys.argv[1:]
57-
else:
58-
# Fall back to npx if no global installation
59-
# This is crucial for Windows where npx is actually npx.cmd
60-
# Using the full path works cross-platform with shell=False
61-
npx_path = shutil.which("npx")
62-
if not npx_path:
56+
try:
57+
# Try the globally installed version first (preferred for speed)
58+
cmd = [promptfoo_path] + sys.argv[1:]
59+
result = subprocess.run(
60+
cmd,
61+
env=os.environ.copy(),
62+
stdin=subprocess.DEVNULL, # Prevent prompts from blocking
63+
check=False, # Don't raise exception on non-zero exit
64+
shell=False, # Keep shell=False for security
65+
)
66+
sys.exit(result.returncode)
67+
except (OSError, PermissionError) as e:
68+
# Global executable exists but failed to run (resource issues, permissions, etc.)
69+
# Fall through to npx fallback for reliability
70+
# Common on CI where executable may not be ready immediately after install
71+
used_global = True
72+
73+
# Fall back to npx if:
74+
# 1. No global installation found, OR
75+
# 2. Global installation failed to execute (OSError, PermissionError, etc.)
76+
npx_path = shutil.which("npx")
77+
if not npx_path:
78+
if used_global:
79+
print("ERROR: Global promptfoo found but failed to execute, and npx is not available.", file=sys.stderr)
80+
else:
6381
print("ERROR: Neither promptfoo nor npx is available.", file=sys.stderr)
64-
print("Please install promptfoo: npm install -g promptfoo", file=sys.stderr)
65-
print("Or ensure Node.js is properly installed.", file=sys.stderr)
66-
sys.exit(1)
82+
print("Please install promptfoo: npm install -g promptfoo", file=sys.stderr)
83+
print("Or ensure Node.js is properly installed.", file=sys.stderr)
84+
sys.exit(1)
6785

68-
# Build the npx fallback command
86+
try:
87+
# Build and execute the npx fallback command
6988
# Use -y (short form) which is more widely supported than --yes
7089
cmd = [npx_path, "-y", "promptfoo@latest"] + sys.argv[1:]
71-
72-
try:
73-
# Execute the command and inherit stdio
74-
# stdin=DEVNULL prevents npx from blocking on prompts like "Ok to proceed? (y)"
7590
result = subprocess.run(
7691
cmd,
7792
env=os.environ.copy(),
7893
stdin=subprocess.DEVNULL, # Prevent prompts from blocking
7994
check=False, # Don't raise exception on non-zero exit
80-
shell=False, # Keep shell=False for security - works on all platforms with full path
95+
shell=False, # Keep shell=False for security
8196
)
8297
sys.exit(result.returncode)
8398
except KeyboardInterrupt:
8499
# Handle Ctrl+C gracefully
85100
print("\nInterrupted by user", file=sys.stderr)
86101
sys.exit(130)
87102
except Exception as e:
88-
print(f"ERROR: Failed to execute promptfoo: {e}", file=sys.stderr)
103+
print(f"ERROR: Failed to execute promptfoo via npx: {e}", file=sys.stderr)
89104
sys.exit(1)
90105

91106

0 commit comments

Comments
 (0)