Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9a1124e
fix(audit): skip virtualenv bin dirs when detecting installations
CybotTM Sep 21, 2026
e72a8d7
fix(audit): drop the detected-path prefix from the version_command PATH
CybotTM Sep 21, 2026
4d9e381
fix(audit): require a directory boundary after venv/bin patterns
CybotTM Sep 21, 2026
3d70af9
fix(audit): use the venv-free lookup for install checks too
CybotTM Sep 21, 2026
e8937d6
fix(reconcile): keep uv-tool and pipx installs, resolve the active co…
CybotTM Sep 21, 2026
c8fd0f7
fix(reconcile): classify uv-tool and pipx installs by their directory
CybotTM Sep 21, 2026
76b3cda
fix(reconcile): resolve relocated tool dirs, uninstall by package and…
CybotTM Sep 21, 2026
c3cfe39
fix(reconcile): exact tool-venv layout, manual global pipx, precise r…
CybotTM Sep 21, 2026
3824ebc
fix(audit): one environment rule for audit and reconcile
CybotTM Sep 21, 2026
6677fe9
fix(reconcile): only a tool's own entry points count as its installation
CybotTM Sep 21, 2026
fb892a4
fix(audit): read a tool record only inside its manager's root
CybotTM Sep 21, 2026
254e934
fix(audit): keep dependency copies out of every lookup
CybotTM Sep 21, 2026
d4c4d42
fix(audit): build a tool record path from a known root and a checked …
CybotTM Sep 21, 2026
3e3c4ec
test(integration): stub the prerequisite lookup, not only the install…
CybotTM Sep 21, 2026
d5e0251
fix(audit): one rule for foreign binaries, and keep the lookup cheap
CybotTM Sep 22, 2026
253eef9
fix(audit): resolve a PATH entry before classifying it
CybotTM Sep 22, 2026
0d35bf2
refactor(reconcile): take the audit's filtered PATH instead of its ow…
CybotTM Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
- Binary-probe fallback in `guide.sh` when the post-install snapshot refresh is stale.

### Fixed
- Audit detection skips virtualenv/conda bin dirs, like reconcile already did. An activated `~/.venv` made the audit report its own copy (`~/.venv/bin/black` 25.11.0) instead of the installation (`uv tool` black 26.5.1), so every upgrade of black, isort and python@3.14 looked like a no-op. Catalog `version_command`s run with the same filtered PATH. Reconcile no longer drops uv-tool and pipx installations, whose per-tool directories also carry a `pyvenv.cfg`. A tool that exists only inside a virtualenv or conda environment is now reported as not installed; the bulk missing-tool check and the post-install validation use the same lookup.
- `make upgrade` hid every pinned tool, whatever the pin. A release skipped with `s` ("ask again when newer patch available") hid the tool for good. A pin now hides a tool only while it is `never`, equals the target release (`s`), equals the installed version (`p`), or equals the cycle.
- `make upgrade` auto-update no longer reports an upgrade as "Updated" just because the install script exited 0. The version is compared after the re-audit, the same check the interactive `Y`/`a` answers use; an unchanged version counts as "Failed" with the old and target version. A package manager without a newer package (`bwrap` on apt) and a binary identical to the target release with a stale version string (`sd` 1.1.0 reports 1.0.0) count as "Skipped". "Held back" requires the install command to succeed and, for apt, the installed package to be the candidate and to own the binary found on PATH; otherwise the unchanged version counts as "Failed". Without an upstream version the result is reported as unverified ("Skipped").
- difftastic 0.71.0 puts the version into its release file names (`difft-0.71.0-x86_64-unknown-linux-gnu.tar.gz`); the catalog download URL now includes it. byobu is tagged `trustmux-v7.19` since the trustmux rename, and those tags fill the first page of the tags API, so the installer found no stable tag; it now accepts both tag forms.
Expand Down
75 changes: 44 additions & 31 deletions cli_audit/bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
import threading
Expand All @@ -21,6 +20,7 @@

from .common import vlog
from .config import Config
from .detection import _which
from .environment import Environment
from .installer import InstallResult, install_tool
from .package_managers import select_package_manager
Expand All @@ -38,6 +38,7 @@ class ToolSpec:
language: Tool language/ecosystem (e.g., "python", "rust")
dependencies: Tool names that must be installed first
"""

tool_name: str
package_name: str
target_version: str = "latest"
Expand Down Expand Up @@ -65,6 +66,7 @@ class ProgressTracker:
_progress: Progress state for each tool
_callbacks: Callbacks to invoke on progress updates
"""

_lock: threading.Lock = field(default_factory=threading.Lock)
_progress: dict[str, dict] = field(default_factory=dict)
_callbacks: list[Callable[[str, str, str], None]] = field(default_factory=list)
Expand Down Expand Up @@ -132,6 +134,7 @@ class BulkInstallResult:
duration_seconds: Total execution time
rollback_script: Path to generated rollback script (if any)
"""

tools_attempted: tuple[str, ...]
successes: tuple[InstallResult, ...]
failures: tuple[InstallResult, ...]
Expand Down Expand Up @@ -164,7 +167,8 @@ def get_missing_tools(tool_names: Sequence[str], verbose: bool = False) -> list[
"""
missing = []
for tool_name in tool_names:
binary_path = shutil.which(tool_name)
# Same lookup as the audit: a copy inside an activated venv is no installation
binary_path = _which(tool_name)
if not binary_path:
missing.append(tool_name)
vlog(f"Tool not found: {tool_name}", verbose)
Expand Down Expand Up @@ -252,26 +256,30 @@ def get_tools_to_install(
return []
for name in tool_names:
tool_config = config.get_tool_config(name)
specs.append(ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
))
specs.append(
ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
)
)

elif mode == "missing":
all_tools = list(config.tools.keys())
missing = get_missing_tools(all_tools, verbose)
for name in missing:
tool_config = config.get_tool_config(name)
specs.append(ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
))
specs.append(
ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
)
)

elif mode == "preset":
if not preset_name or not hasattr(config, "presets"):
Expand All @@ -280,25 +288,29 @@ def get_tools_to_install(
preset_tools = getattr(config.presets, preset_name, [])
for name in preset_tools:
tool_config = config.get_tool_config(name)
specs.append(ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
))
specs.append(
ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
)
)

elif mode == "all":
all_tools = list(config.tools.keys())
for name in all_tools:
tool_config = config.get_tool_config(name)
specs.append(ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
))
specs.append(
ToolSpec(
tool_name=name,
package_name=name,
target_version=tool_config.version if tool_config else "latest",
language=None,
dependencies=(),
)
)

vlog(f"Mode '{mode}' resolved to {len(specs)} tools", verbose)
return specs
Expand Down Expand Up @@ -502,6 +514,7 @@ def bulk_install(
# Determine max workers
if max_workers is None:
import os

max_workers = min(16, os.cpu_count() or 4 + 4)

# Execute installations level by level
Expand Down Expand Up @@ -551,7 +564,7 @@ def bulk_install(
# Stop if fail-fast triggered
if fail_fast and failures:
# Mark remaining tools as skipped
for level in levels[level_idx + 1:]:
for level in levels[level_idx + 1 :]:
for spec in level:
skipped.append(spec.tool_name)
progress_tracker.update(spec.tool_name, "skipped", "Skipped due to fail-fast")
Expand Down
Loading
Loading