♻️ Rework project-uv probe for cross-platform determinism
Probe now emits KEY=VALUE so the agent reads facts instead of inferring from prose. Stdlib-only (no tomllib) so it runs on whatever Python the launcher reaches. Adds sh/ps1 launcher pair, SCRIPTS_UV/SCRIPTS_DIR/ SCRIPTS_STORE env-var contract, tainted-venv + recipe + default-package detection, and broader mode classification (incl. agent-tool-only). SKILL.md trimmed and gated by pyproject.toml glob.
This commit is contained in:
@@ -25,3 +25,5 @@
|
||||
- it should also therefore get the idea of where a local scratch pad (SCRIPTS_STORE), project scripts folder (SCRIPTS_DIR), etc. might exist, and can therefore substitute this also as knowledge if it has to work with other tools, that want files or scripts written, but do not define where a certain folder is. These directories build good fallbacks for such concerns, and python therefore a solid tool to support the developer in tasks.
|
||||
- for full python projects, it is very likely, that the configured uv is also capable of running actual project code, so namespaces should be respected.
|
||||
- for rust projects, the uv configuration is almost certainly the local python environment for the agent.
|
||||
|
||||
- best would be if we would simply run a universal shell that every skill should support and print out env variables that are maybe relevant immediately.
|
||||
|
||||
@@ -1,67 +1,44 @@
|
||||
---
|
||||
name: project-uv
|
||||
description: Use whenever you are about to run, install, or modify Python code in a project whose root contains a `pyproject.toml` — especially before invoking `python`, `pip`, `uv run`, or any test/lint command. Detects how uv is wired up here (project vs script mode, lockfile, declared scripts, src/ layout, tool table) and tells you the right invocation pattern *for this repo* before you guess. Trigger words: "run this script", "install this dependency", "test", "sync", any mention of `uv`, `pip`, `python -m`, or running a `.py` file.
|
||||
description: Probe for python once per conversation, before the first `python`, `pip`, `uv run`, or `.py` invocation. Skip if the correct uv invocation for this repo is already established in the conversation.
|
||||
globs:
|
||||
- pyproject.toml
|
||||
---
|
||||
|
||||
# project-uv
|
||||
|
||||
The point of this skill is to **stop guessing how to run Python in this project**. `uv` supports several distinct workflows (project-managed, script-with-inline-deps, ad-hoc venv) and the right invocation differs accordingly. Probe first, then run.
|
||||
Probe once per conversation. Read keys, don't guess.
|
||||
|
||||
## Activation
|
||||
## Run
|
||||
|
||||
This skill applies when the current project root contains a `pyproject.toml`. If there is no `pyproject.toml`, this skill is not relevant — fall back to whatever Python tooling the user already established.
|
||||
POSIX: `sh "$CLAUDE_PLUGIN_ROOT/skills/project-uv/scripts/probe.sh"`
|
||||
Windows: `& "$env:CLAUDE_PLUGIN_ROOT\skills\project-uv\scripts\probe.ps1"`
|
||||
|
||||
## Step 0: Probe the project
|
||||
Successful execution is the proof python works here. If it fails, stop and report.
|
||||
|
||||
**MANDATORY FIRST STEP** when this skill is invoked for the first time in a conversation. Run the probe script:
|
||||
## Env-var contract
|
||||
|
||||
```bash
|
||||
uv run --no-project python ${CLAUDE_PLUGIN_ROOT}/skills/project-uv/scripts/probe.py
|
||||
```
|
||||
- `SCRIPTS_UV` — path to the `uv` executable.
|
||||
- `SCRIPTS_DIR` — project script dir (default `scripts/`).
|
||||
- `SCRIPTS_STORE` — scratch dir for throwaway scripts (default: platform temp).
|
||||
|
||||
(Or copy `scripts/probe.py` and run it directly with any available Python — it has no dependencies.)
|
||||
Output echoes each as a key — empty value means unset or invalid. Use the `uv=` value if `scripts_uv=` is empty.
|
||||
|
||||
The probe prints a short structured report describing:
|
||||
## Acting on `mode`
|
||||
|
||||
- whether `uv` is installed and its version
|
||||
- the project mode: managed project (`[project]` table) vs PEP 723 script mode vs neither
|
||||
- whether a `uv.lock` exists and is up to date
|
||||
- whether `[tool.uv]` is configured, with notable keys
|
||||
- declared scripts: `[project.scripts]` console entrypoints
|
||||
- src layout: presence of `src/`, top-level packages
|
||||
- presence of sibling tooling: `Justfile`, `Makefile`, `mise.toml`, `.python-version`
|
||||
- `managed-project-locked` — `uv run <cmd>`; `uv sync` if `.venv` missing.
|
||||
- `managed-project-unlocked` — `uv run <cmd>`; `uv sync` to create env.
|
||||
- `pep723-only` — `uv run path/to/script.py`; edit `# /// script` block for deps.
|
||||
- `uv-tool-only` — `[tool.uv]` without `[project]`; ask before assuming.
|
||||
- `agent-tool-only` — uv exists but no python project; use for agent scratchpad in `scripts_store`.
|
||||
- `none` — no python here; stop.
|
||||
|
||||
Treat the probe output as ground truth for the rest of the conversation. Don't re-probe unless files relevant to the probe have changed.
|
||||
## Other keys
|
||||
|
||||
## Invocation rules of thumb
|
||||
- `venv_tainted=true` — warn the user; uv isn't fully embraced here.
|
||||
- `recipes=` — if a `just:`/`make:` recipe covers the task, prefer it.
|
||||
- `default_packages=` — already importable; no install needed.
|
||||
|
||||
Once you know the project mode, apply these defaults:
|
||||
## Out of scope
|
||||
|
||||
**Managed project (`[project]` table present, optionally with `uv.lock`):**
|
||||
- Sync first if `uv.lock` is out of date or `.venv` is missing: `uv sync`
|
||||
- Run anything inside the env via `uv run`, e.g. `uv run pytest`, `uv run python -m mypkg.cli`
|
||||
- Add deps with `uv add <pkg>`, not `pip install`
|
||||
- Declared console scripts are runnable as `uv run <name>`
|
||||
|
||||
**PEP 723 inline-deps script:**
|
||||
- Run with `uv run path/to/script.py` — uv will install inline deps in an ephemeral env
|
||||
- Do not `uv add` to a non-project file; edit the `# /// script` block instead
|
||||
|
||||
**Neither (raw `pyproject.toml` with no `[project]` and no inline deps):**
|
||||
- The user probably has another build backend or only metadata. Ask before assuming.
|
||||
|
||||
## Composing with other tooling
|
||||
|
||||
If the probe shows a `Justfile` / `Makefile`, **prefer the recipe over the raw uv command** whenever a recipe exists for the task — that's the user's preferred entry point. Only fall through to raw `uv run` when no recipe covers it.
|
||||
|
||||
If the probe shows `mise.toml` and the agent has access to `mise`, defer to mise for tool versions (Python version in particular).
|
||||
|
||||
## What this skill does NOT do
|
||||
|
||||
- It does not install `uv` for the user. If uv is missing, report that and stop.
|
||||
- It does not modify `pyproject.toml`. Adding/removing deps is a separate, user-driven decision — surface the recommended command, let the user accept.
|
||||
- It does not pick a Python version. That's `.python-version` / `mise.toml` / `[tool.uv]` territory.
|
||||
|
||||
## Future granularity (pilot note)
|
||||
|
||||
This skill is intentionally narrow. Companion skills like `run-uv-with-just` are planned: they would activate only when both `pyproject.toml` and `Justfile` are present, and would teach the agent the just-recipe vocabulary in this specific repo. The granularity is meant to compose, not duplicate.
|
||||
Does not install uv, modify `pyproject.toml`, or pick a Python version.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
$probe = Join-Path $PSScriptRoot "probe.py"
|
||||
|
||||
if ($env:SCRIPTS_UV -and (Test-Path $env:SCRIPTS_UV)) {
|
||||
& $env:SCRIPTS_UV run --no-project python $probe
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
$uv = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if ($uv) {
|
||||
& $uv.Source run --no-project python $probe
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
foreach ($name in @("python", "python3", "py")) {
|
||||
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
& $cmd.Source $probe
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
Write-Error "probe: no python found"
|
||||
exit 1
|
||||
@@ -1,149 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe a Python project to detect how uv is wired up.
|
||||
"""project-uv probe: detect uv wiring and prove python works here.
|
||||
|
||||
Run from the project root (or pass --root). Output is a short structured
|
||||
report intended for an LLM agent to read before suggesting uv commands.
|
||||
Stdlib only, targets Python 3.7+. No TOML parser — pyproject.toml is
|
||||
line-grepped for the handful of facts we need, so this probe runs on any
|
||||
Python the launcher can reach. Successful execution is itself the proof
|
||||
that python works in this project.
|
||||
|
||||
No third-party dependencies; stdlib only so it can run with `uv run --no-project`
|
||||
or any system Python.
|
||||
Output: KEY=VALUE lines on stdout. One key per line. Read keys, not prose.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib # Python 3.11+
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
import tomli as tomllib # type: ignore[no-redef]
|
||||
|
||||
def emit(key, value):
|
||||
if isinstance(value, bool):
|
||||
value = "true" if value else "false"
|
||||
elif isinstance(value, (list, tuple)):
|
||||
value = ",".join(str(v) for v in value) if value else ""
|
||||
elif value is None:
|
||||
value = ""
|
||||
value = str(value).replace("\n", " ").replace("\r", " ")
|
||||
print("{}={}".format(key, value))
|
||||
|
||||
|
||||
def detect_uv() -> tuple[bool, str | None]:
|
||||
uv = shutil.which("uv")
|
||||
if not uv:
|
||||
return False, None
|
||||
def detect_platform():
|
||||
if sys.platform.startswith("win"):
|
||||
return "windows"
|
||||
if sys.platform == "darwin":
|
||||
return "macos"
|
||||
if sys.platform.startswith("linux"):
|
||||
return "linux"
|
||||
return sys.platform
|
||||
|
||||
|
||||
def detect_uv(preferred):
|
||||
candidates = []
|
||||
if preferred:
|
||||
candidates.append(preferred)
|
||||
found = shutil.which("uv")
|
||||
if found and found not in candidates:
|
||||
candidates.append(found)
|
||||
for c in candidates:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[uv, "--version"], capture_output=True, text=True, timeout=5, check=False
|
||||
[c, "--version"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
return True, out.stdout.strip() or out.stderr.strip() or None
|
||||
except Exception:
|
||||
return True, None
|
||||
if out.returncode == 0:
|
||||
return c, (out.stdout or out.stderr).strip()
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def load_pyproject(root: Path) -> dict | None:
|
||||
pp = root / "pyproject.toml"
|
||||
if not pp.exists():
|
||||
return None
|
||||
def scan_pyproject(path):
|
||||
facts = {
|
||||
"has_project": False,
|
||||
"has_tool_uv": False,
|
||||
"scripts": [],
|
||||
"requires_python": "",
|
||||
}
|
||||
if not path.exists():
|
||||
return facts
|
||||
try:
|
||||
return tomllib.loads(pp.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"WARN: failed to parse pyproject.toml: {e}", file=sys.stderr)
|
||||
return None
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
return facts
|
||||
|
||||
section = None
|
||||
for raw in text.splitlines():
|
||||
line = raw.strip()
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
section = line[1:-1].strip()
|
||||
if section == "project":
|
||||
facts["has_project"] = True
|
||||
elif section == "tool.uv" or section.startswith("tool.uv."):
|
||||
facts["has_tool_uv"] = True
|
||||
continue
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if section == "project" and line.startswith("requires-python"):
|
||||
m = re.search(r'"([^"]+)"', line)
|
||||
if m:
|
||||
facts["requires_python"] = m.group(1)
|
||||
elif section == "project.scripts":
|
||||
m = re.match(r"([A-Za-z0-9_.\-]+)\s*=", line)
|
||||
if m:
|
||||
facts["scripts"].append(m.group(1))
|
||||
return facts
|
||||
|
||||
|
||||
def detect_pep723_scripts(root: Path) -> list[str]:
|
||||
hits: list[str] = []
|
||||
def detect_pep723(root):
|
||||
hits = []
|
||||
for py in root.glob("*.py"):
|
||||
try:
|
||||
head = py.read_text(encoding="utf-8", errors="replace")[:2000]
|
||||
except Exception:
|
||||
except OSError:
|
||||
continue
|
||||
if "# /// script" in head:
|
||||
hits.append(py.name)
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--root", default=".", help="project root (default: cwd)")
|
||||
args = ap.parse_args()
|
||||
root = Path(args.root).resolve()
|
||||
def detect_venv_taint(venv):
|
||||
if not venv.is_dir():
|
||||
return False, []
|
||||
reasons = []
|
||||
if not (venv / "pyvenv.cfg").exists():
|
||||
reasons.append("no-pyvenv.cfg")
|
||||
expected = {
|
||||
"pyvenv.cfg",
|
||||
"bin",
|
||||
"Scripts",
|
||||
"Lib",
|
||||
"lib",
|
||||
"Lib64",
|
||||
"lib64",
|
||||
"Include",
|
||||
"include",
|
||||
"share",
|
||||
".gitignore",
|
||||
".lock",
|
||||
"uv",
|
||||
}
|
||||
try:
|
||||
extras = sorted(p.name for p in venv.iterdir() if p.name not in expected)
|
||||
except OSError:
|
||||
extras = []
|
||||
if extras:
|
||||
reasons.append("extra:" + "|".join(extras[:5]))
|
||||
libdir = venv / "lib"
|
||||
if libdir.is_dir():
|
||||
try:
|
||||
py_dirs = [p.name for p in libdir.iterdir() if p.name.startswith("py")]
|
||||
except OSError:
|
||||
py_dirs = []
|
||||
if len(py_dirs) > 1:
|
||||
reasons.append("multi-python:" + "|".join(py_dirs))
|
||||
return bool(reasons), reasons
|
||||
|
||||
print(f"# project-uv probe")
|
||||
print(f"root: {root}")
|
||||
print()
|
||||
|
||||
uv_present, uv_version = detect_uv()
|
||||
print(f"uv installed: {uv_present}")
|
||||
if uv_version:
|
||||
print(f"uv version: {uv_version}")
|
||||
print()
|
||||
def detect_default_packages(venv):
|
||||
if not venv.is_dir():
|
||||
return []
|
||||
targets = {
|
||||
"requests",
|
||||
"httpx",
|
||||
"python-dotenv",
|
||||
"pydantic",
|
||||
"pytest",
|
||||
"pyyaml",
|
||||
"tomli",
|
||||
"mcp",
|
||||
}
|
||||
site_dirs = []
|
||||
libdir = venv / "lib"
|
||||
if libdir.is_dir():
|
||||
try:
|
||||
for child in libdir.iterdir():
|
||||
sp = child / "site-packages"
|
||||
if sp.is_dir():
|
||||
site_dirs.append(sp)
|
||||
except OSError:
|
||||
pass
|
||||
win_sp = venv / "Lib" / "site-packages"
|
||||
if win_sp.is_dir():
|
||||
site_dirs.append(win_sp)
|
||||
|
||||
pyproject = load_pyproject(root)
|
||||
has_pyproject = pyproject is not None
|
||||
print(f"pyproject.toml present: {has_pyproject}")
|
||||
found = set()
|
||||
for sp in site_dirs:
|
||||
try:
|
||||
for entry in sp.iterdir():
|
||||
if not entry.name.endswith(".dist-info"):
|
||||
continue
|
||||
stem = entry.name[: -len(".dist-info")]
|
||||
pkg = stem.rsplit("-", 1)[0].lower().replace("_", "-")
|
||||
if pkg in targets:
|
||||
found.add(pkg)
|
||||
except OSError:
|
||||
continue
|
||||
if sys.version_info >= (3, 11):
|
||||
found.add("tomllib(stdlib)")
|
||||
return sorted(found)
|
||||
|
||||
if not has_pyproject:
|
||||
scripts = detect_pep723_scripts(root)
|
||||
print(f"PEP 723 inline-dep scripts at root: {scripts or 'none'}")
|
||||
print("mode: no-pyproject (skill does not apply unless inline-dep scripts found)")
|
||||
return 0
|
||||
|
||||
assert pyproject is not None
|
||||
has_project = "project" in pyproject
|
||||
tool_uv = pyproject.get("tool", {}).get("uv")
|
||||
scripts_table = pyproject.get("project", {}).get("scripts", {}) if has_project else {}
|
||||
def detect_recipes(root):
|
||||
hits = []
|
||||
for name in ("Justfile", "justfile", ".justfile"):
|
||||
p = root / name
|
||||
if p.exists():
|
||||
try:
|
||||
for line in p.read_text(
|
||||
encoding="utf-8", errors="replace"
|
||||
).splitlines():
|
||||
m = re.match(r"^([a-zA-Z0-9_\-]+)\s*:", line)
|
||||
if m:
|
||||
hits.append("just:" + m.group(1))
|
||||
except OSError:
|
||||
pass
|
||||
break
|
||||
mk = root / "Makefile"
|
||||
if mk.exists():
|
||||
try:
|
||||
for line in mk.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
m = re.match(r"^([a-zA-Z0-9_\-]+)\s*:", line)
|
||||
if m and m.group(1) not in ("PHONY",):
|
||||
hits.append("make:" + m.group(1))
|
||||
except OSError:
|
||||
pass
|
||||
return hits
|
||||
|
||||
print(f"[project] table: {has_project}")
|
||||
print(f"[tool.uv] table: {tool_uv is not None}")
|
||||
if tool_uv:
|
||||
notable = sorted(tool_uv.keys())
|
||||
print(f"[tool.uv] keys: {notable}")
|
||||
print()
|
||||
|
||||
lock = root / "uv.lock"
|
||||
def main():
|
||||
root = Path(os.environ.get("PROJECT_ROOT", ".")).resolve()
|
||||
emit("platform", detect_platform())
|
||||
emit("root", root)
|
||||
|
||||
scripts_uv = os.environ.get("SCRIPTS_UV", "").strip()
|
||||
scripts_dir = os.environ.get("SCRIPTS_DIR", "scripts/").strip()
|
||||
scripts_store = os.environ.get("SCRIPTS_STORE", tempfile.gettempdir()).strip()
|
||||
|
||||
scripts_uv_valid = (
|
||||
bool(scripts_uv)
|
||||
and Path(scripts_uv).exists()
|
||||
and os.access(scripts_uv, os.X_OK)
|
||||
)
|
||||
scripts_dir_valid = (root / scripts_dir).is_dir()
|
||||
scripts_store_valid = Path(scripts_store).is_dir() and os.access(
|
||||
scripts_store, os.W_OK
|
||||
)
|
||||
emit("scripts_uv", scripts_uv if scripts_uv_valid else "")
|
||||
emit("scripts_dir", scripts_dir if scripts_dir_valid else "")
|
||||
emit("scripts_store", scripts_store if scripts_store_valid else "")
|
||||
|
||||
uv_path, _ = detect_uv(scripts_uv if scripts_uv_valid else None)
|
||||
emit("uv", uv_path or "")
|
||||
|
||||
pp = root / "pyproject.toml"
|
||||
facts = scan_pyproject(pp)
|
||||
emit("pyproject_scripts", facts["scripts"])
|
||||
emit("uv_lock", (root / "uv.lock").exists())
|
||||
emit("uv_toml", (root / "uv.toml").exists())
|
||||
emit("requirements", sorted(p.name for p in root.glob("requirements*.txt")))
|
||||
|
||||
venv = root / ".venv"
|
||||
print(f"uv.lock present: {lock.exists()}")
|
||||
print(f".venv present: {venv.exists()}")
|
||||
print()
|
||||
emit("venv", venv.exists())
|
||||
tainted, reasons = detect_venv_taint(venv)
|
||||
emit("venv_tainted", tainted)
|
||||
emit("venv_taint", reasons)
|
||||
|
||||
if scripts_table:
|
||||
print("declared console scripts ([project.scripts]):")
|
||||
for name, target in scripts_table.items():
|
||||
print(f" {name} -> {target}")
|
||||
else:
|
||||
print("declared console scripts: none")
|
||||
print()
|
||||
pep723 = detect_pep723(root)
|
||||
emit("pep723", pep723)
|
||||
|
||||
src = root / "src"
|
||||
print(f"src/ layout: {src.is_dir()}")
|
||||
if src.is_dir():
|
||||
pkgs = [p.name for p in src.iterdir() if p.is_dir() and (p / "__init__.py").exists()]
|
||||
print(f"src/ packages: {pkgs or 'none'}")
|
||||
pkgs = sorted(
|
||||
p.name for p in src.iterdir() if p.is_dir() and (p / "__init__.py").exists()
|
||||
)
|
||||
else:
|
||||
top_pkgs = [
|
||||
p.name for p in root.iterdir()
|
||||
if p.is_dir() and (p / "__init__.py").exists() and not p.name.startswith(".")
|
||||
]
|
||||
print(f"top-level packages: {top_pkgs or 'none'}")
|
||||
print()
|
||||
pkgs = sorted(
|
||||
p.name
|
||||
for p in root.iterdir()
|
||||
if p.is_dir()
|
||||
and (p / "__init__.py").exists()
|
||||
and not p.name.startswith(".")
|
||||
)
|
||||
emit("packages", pkgs)
|
||||
|
||||
siblings = {
|
||||
"Justfile": (root / "Justfile").exists() or (root / "justfile").exists() or (root / ".justfile").exists(),
|
||||
"Makefile": (root / "Makefile").exists(),
|
||||
"mise.toml": (root / "mise.toml").exists() or (root / ".mise.toml").exists(),
|
||||
".python-version": (root / ".python-version").exists(),
|
||||
".env": (root / ".env").exists(),
|
||||
}
|
||||
print("sibling tooling:")
|
||||
for name, present in siblings.items():
|
||||
print(f" {name}: {present}")
|
||||
print()
|
||||
emit(
|
||||
"justfile",
|
||||
any((root / n).exists() for n in ("Justfile", "justfile", ".justfile")),
|
||||
)
|
||||
emit("makefile", (root / "Makefile").exists())
|
||||
emit("mise", any((root / n).exists() for n in ("mise.toml", ".mise.toml")))
|
||||
|
||||
if has_project and lock.exists():
|
||||
emit("recipes", detect_recipes(root))
|
||||
emit("default_packages", detect_default_packages(venv))
|
||||
|
||||
if facts["has_project"] and (root / "uv.lock").exists():
|
||||
mode = "managed-project-locked"
|
||||
elif has_project:
|
||||
elif facts["has_project"]:
|
||||
mode = "managed-project-unlocked"
|
||||
elif tool_uv is not None:
|
||||
elif pep723:
|
||||
mode = "pep723-only"
|
||||
elif facts["has_tool_uv"]:
|
||||
mode = "uv-tool-only"
|
||||
elif uv_path:
|
||||
mode = "agent-tool-only"
|
||||
else:
|
||||
mode = "metadata-only-or-other-backend"
|
||||
print(f"mode: {mode}")
|
||||
mode = "none"
|
||||
emit("mode", mode)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROBE="$SCRIPT_DIR/probe.py"
|
||||
|
||||
if [ -n "$SCRIPTS_UV" ] && [ -x "$SCRIPTS_UV" ]; then
|
||||
exec "$SCRIPTS_UV" run --no-project python "$PROBE"
|
||||
fi
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
exec uv run --no-project python "$PROBE"
|
||||
fi
|
||||
for name in python3 python; do
|
||||
if command -v "$name" >/dev/null 2>&1; then
|
||||
exec "$name" "$PROBE"
|
||||
fi
|
||||
done
|
||||
echo "probe: no python found" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user