♻️ 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.
|
- 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 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.
|
- 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
|
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
|
# 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
|
- `SCRIPTS_UV` — path to the `uv` executable.
|
||||||
uv run --no-project python ${CLAUDE_PLUGIN_ROOT}/skills/project-uv/scripts/probe.py
|
- `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
|
- `managed-project-locked` — `uv run <cmd>`; `uv sync` if `.venv` missing.
|
||||||
- the project mode: managed project (`[project]` table) vs PEP 723 script mode vs neither
|
- `managed-project-unlocked` — `uv run <cmd>`; `uv sync` to create env.
|
||||||
- whether a `uv.lock` exists and is up to date
|
- `pep723-only` — `uv run path/to/script.py`; edit `# /// script` block for deps.
|
||||||
- whether `[tool.uv]` is configured, with notable keys
|
- `uv-tool-only` — `[tool.uv]` without `[project]`; ask before assuming.
|
||||||
- declared scripts: `[project.scripts]` console entrypoints
|
- `agent-tool-only` — uv exists but no python project; use for agent scratchpad in `scripts_store`.
|
||||||
- src layout: presence of `src/`, top-level packages
|
- `none` — no python here; stop.
|
||||||
- presence of sibling tooling: `Justfile`, `Makefile`, `mise.toml`, `.python-version`
|
|
||||||
|
|
||||||
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`):**
|
Does not install uv, modify `pyproject.toml`, or pick a Python version.
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -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
|
#!/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
|
Stdlib only, targets Python 3.7+. No TOML parser — pyproject.toml is
|
||||||
report intended for an LLM agent to read before suggesting uv commands.
|
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`
|
Output: KEY=VALUE lines on stdout. One key per line. Read keys, not prose.
|
||||||
or any system Python.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
|
||||||
import tomllib # Python 3.11+
|
def emit(key, value):
|
||||||
except ModuleNotFoundError: # pragma: no cover
|
if isinstance(value, bool):
|
||||||
import tomli as tomllib # type: ignore[no-redef]
|
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]:
|
def detect_platform():
|
||||||
uv = shutil.which("uv")
|
if sys.platform.startswith("win"):
|
||||||
if not uv:
|
return "windows"
|
||||||
return False, None
|
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:
|
try:
|
||||||
out = subprocess.run(
|
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
|
if out.returncode == 0:
|
||||||
except Exception:
|
return c, (out.stdout or out.stderr).strip()
|
||||||
return True, None
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
continue
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def load_pyproject(root: Path) -> dict | None:
|
def scan_pyproject(path):
|
||||||
pp = root / "pyproject.toml"
|
facts = {
|
||||||
if not pp.exists():
|
"has_project": False,
|
||||||
return None
|
"has_tool_uv": False,
|
||||||
|
"scripts": [],
|
||||||
|
"requires_python": "",
|
||||||
|
}
|
||||||
|
if not path.exists():
|
||||||
|
return facts
|
||||||
try:
|
try:
|
||||||
return tomllib.loads(pp.read_text(encoding="utf-8"))
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
except Exception as e:
|
except OSError:
|
||||||
print(f"WARN: failed to parse pyproject.toml: {e}", file=sys.stderr)
|
return facts
|
||||||
return None
|
|
||||||
|
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]:
|
def detect_pep723(root):
|
||||||
hits: list[str] = []
|
hits = []
|
||||||
for py in root.glob("*.py"):
|
for py in root.glob("*.py"):
|
||||||
try:
|
try:
|
||||||
head = py.read_text(encoding="utf-8", errors="replace")[:2000]
|
head = py.read_text(encoding="utf-8", errors="replace")[:2000]
|
||||||
except Exception:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
if "# /// script" in head:
|
if "# /// script" in head:
|
||||||
hits.append(py.name)
|
hits.append(py.name)
|
||||||
return hits
|
return hits
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def detect_venv_taint(venv):
|
||||||
ap = argparse.ArgumentParser()
|
if not venv.is_dir():
|
||||||
ap.add_argument("--root", default=".", help="project root (default: cwd)")
|
return False, []
|
||||||
args = ap.parse_args()
|
reasons = []
|
||||||
root = Path(args.root).resolve()
|
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()
|
def detect_default_packages(venv):
|
||||||
print(f"uv installed: {uv_present}")
|
if not venv.is_dir():
|
||||||
if uv_version:
|
return []
|
||||||
print(f"uv version: {uv_version}")
|
targets = {
|
||||||
print()
|
"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)
|
found = set()
|
||||||
has_pyproject = pyproject is not None
|
for sp in site_dirs:
|
||||||
print(f"pyproject.toml present: {has_pyproject}")
|
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
|
def detect_recipes(root):
|
||||||
has_project = "project" in pyproject
|
hits = []
|
||||||
tool_uv = pyproject.get("tool", {}).get("uv")
|
for name in ("Justfile", "justfile", ".justfile"):
|
||||||
scripts_table = pyproject.get("project", {}).get("scripts", {}) if has_project else {}
|
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"
|
venv = root / ".venv"
|
||||||
print(f"uv.lock present: {lock.exists()}")
|
emit("venv", venv.exists())
|
||||||
print(f".venv present: {venv.exists()}")
|
tainted, reasons = detect_venv_taint(venv)
|
||||||
print()
|
emit("venv_tainted", tainted)
|
||||||
|
emit("venv_taint", reasons)
|
||||||
|
|
||||||
if scripts_table:
|
pep723 = detect_pep723(root)
|
||||||
print("declared console scripts ([project.scripts]):")
|
emit("pep723", pep723)
|
||||||
for name, target in scripts_table.items():
|
|
||||||
print(f" {name} -> {target}")
|
|
||||||
else:
|
|
||||||
print("declared console scripts: none")
|
|
||||||
print()
|
|
||||||
|
|
||||||
src = root / "src"
|
src = root / "src"
|
||||||
print(f"src/ layout: {src.is_dir()}")
|
|
||||||
if src.is_dir():
|
if src.is_dir():
|
||||||
pkgs = [p.name for p in src.iterdir() if p.is_dir() and (p / "__init__.py").exists()]
|
pkgs = sorted(
|
||||||
print(f"src/ packages: {pkgs or 'none'}")
|
p.name for p in src.iterdir() if p.is_dir() and (p / "__init__.py").exists()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
top_pkgs = [
|
pkgs = sorted(
|
||||||
p.name for p in root.iterdir()
|
p.name
|
||||||
if p.is_dir() and (p / "__init__.py").exists() and not p.name.startswith(".")
|
for p in root.iterdir()
|
||||||
]
|
if p.is_dir()
|
||||||
print(f"top-level packages: {top_pkgs or 'none'}")
|
and (p / "__init__.py").exists()
|
||||||
print()
|
and not p.name.startswith(".")
|
||||||
|
)
|
||||||
|
emit("packages", pkgs)
|
||||||
|
|
||||||
siblings = {
|
emit(
|
||||||
"Justfile": (root / "Justfile").exists() or (root / "justfile").exists() or (root / ".justfile").exists(),
|
"justfile",
|
||||||
"Makefile": (root / "Makefile").exists(),
|
any((root / n).exists() for n in ("Justfile", "justfile", ".justfile")),
|
||||||
"mise.toml": (root / "mise.toml").exists() or (root / ".mise.toml").exists(),
|
)
|
||||||
".python-version": (root / ".python-version").exists(),
|
emit("makefile", (root / "Makefile").exists())
|
||||||
".env": (root / ".env").exists(),
|
emit("mise", any((root / n).exists() for n in ("mise.toml", ".mise.toml")))
|
||||||
}
|
|
||||||
print("sibling tooling:")
|
|
||||||
for name, present in siblings.items():
|
|
||||||
print(f" {name}: {present}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
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"
|
mode = "managed-project-locked"
|
||||||
elif has_project:
|
elif facts["has_project"]:
|
||||||
mode = "managed-project-unlocked"
|
mode = "managed-project-unlocked"
|
||||||
elif tool_uv is not None:
|
elif pep723:
|
||||||
|
mode = "pep723-only"
|
||||||
|
elif facts["has_tool_uv"]:
|
||||||
mode = "uv-tool-only"
|
mode = "uv-tool-only"
|
||||||
|
elif uv_path:
|
||||||
|
mode = "agent-tool-only"
|
||||||
else:
|
else:
|
||||||
mode = "metadata-only-or-other-backend"
|
mode = "none"
|
||||||
print(f"mode: {mode}")
|
emit("mode", mode)
|
||||||
|
|
||||||
return 0
|
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