diff --git a/AGENTS.md b/AGENTS.md index 91c50d4..dc1ba1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ This repo **is** a CLI tool manager, so the word "upgrade" is overloaded: ## Index of scoped AGENTS.md - [cli_audit/AGENTS.md](./cli_audit/AGENTS.md) — Python package (21 modules) -- [scripts/AGENTS.md](./scripts/AGENTS.md) — Installation scripts (Bash, 33 scripts) +- [scripts/AGENTS.md](./scripts/AGENTS.md) — Installation scripts (Bash, 34 scripts) - [tests/AGENTS.md](./tests/AGENTS.md) — Test suite (pytest, 14 test files) ## Commands (verified 2026-04-16) @@ -123,8 +123,8 @@ This repo **is** a CLI tool manager, so the word "upgrade" is overloaded: **AI CLI Preparation v2.0** — Tool version auditing and installation management for AI coding agents. - **Modules:** 21 Python modules under `cli_audit/` (see [`cli_audit/AGENTS.md`](./cli_audit/AGENTS.md)) -- **Catalog:** 97 JSON tool definitions under `catalog/` -- **Installers:** 33 Bash scripts under `scripts/` (see [`scripts/AGENTS.md`](./scripts/AGENTS.md)) +- **Catalog:** 98 JSON tool definitions under `catalog/` +- **Installers:** 34 Bash scripts under `scripts/` (see [`scripts/AGENTS.md`](./scripts/AGENTS.md)) - **Phase 1:** Detection & auditing (complete) - **Phase 2:** Installation & upgrade management (complete) - **Entry point:** `audit.py` → `cli_audit` package diff --git a/CHANGELOG.md b/CHANGELOG.md index 52804ad..7ae469d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- `wslu`/`wslview` catalog entry (`catalog/wslu.json` + `scripts/install_wslu.sh`), gated by a new `requires_wsl` catalog flag so it is only surfaced and installed under WSL. Its installer also points the xdg default browser at `wslview` (so links open in the Windows browser); opt out with `WSLU_SET_DEFAULT_BROWSER=0`. - Governance files: PR template, `CHANGELOG.md`. Security reporting is covered by the [org-level SECURITY.md](https://github.com/netresearch/.github/blob/main/SECURITY.md). - `.github/workflows/dependency-review.yml` and a dedicated `security.yml` (pip-audit + bandit + CycloneDX SBOM). CodeQL continues to run via GitHub's default code-scanning setup. - `.pre-commit-config.yaml` for local hook enforcement (flake8, black, isort, shellcheck). diff --git a/catalog/README.md b/catalog/README.md index 7dc84c6..77bbdad 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -71,7 +71,7 @@ Install tools via system package managers (apt, brew, dnf, pacman). 2. The tool will automatically be available via `make install-`, `make upgrade-`, `make uninstall-`, and `make reconcile-` 3. No need to create a custom install script! -Currently **97 tools** are cataloged. +Currently **98 tools** are cataloged. ## Environment Variables @@ -96,6 +96,6 @@ make upgrade ## Architecture -All 97 tools have catalog entries. The generic installer (`scripts/install_tool.sh`) reads a tool's catalog JSON and delegates to the appropriate method-specific installer under `scripts/installers/`. Tools with complex installation needs (python, node, docker, rust, etc.) use `install_method: "dedicated_script"` to route to their existing bespoke scripts. +All 98 tools have catalog entries. The generic installer (`scripts/install_tool.sh`) reads a tool's catalog JSON and delegates to the appropriate method-specific installer under `scripts/installers/`. Tools with complex installation needs (python, node, docker, rust, etc.) use `install_method: "dedicated_script"` to route to their existing bespoke scripts. See [ADR-007](../docs/adr/ADR-007-generic-tool-installation-architecture.md) for the full architectural decision record. diff --git a/catalog/wslu.json b/catalog/wslu.json new file mode 100644 index 0000000..f34bb7f --- /dev/null +++ b/catalog/wslu.json @@ -0,0 +1,17 @@ +{ + "name": "wslu", + "category": "platform", + "install_method": "dedicated_script", + "description": "WSL utilities; provides wslview to open URLs/files in the Windows default browser", + "homepage": "https://wslutiliti.es/wslu/", + "binary_name": "wslview", + "script": "install_wslu.sh", + "requires_wsl": true, + "skip_upstream": true, + "tags": ["platform", "wsl"], + "guide": { + "display_name": "wslu (wslview)", + "install_action": "install", + "order": 210 + } +} diff --git a/cli_audit/catalog.py b/cli_audit/catalog.py index 5e3e789..c3ce953 100644 --- a/cli_audit/catalog.py +++ b/cli_audit/catalog.py @@ -230,10 +230,21 @@ def all_tools(self) -> list[str]: def all_tool_definitions(self) -> list[Tool]: # noqa: F821 """Get all tools as Tool instances. + Tools flagged ``requires_wsl`` in their catalog JSON are only included + when running under WSL, so non-WSL machines are not prompted to install + (or shown as missing) a tool that has no meaning there. + Returns: List of Tool instances generated from catalog """ - return [entry.to_tool() for entry in self._entries.values()] + from cli_audit.collectors import is_wsl # lazy import: avoids import cycle + + on_wsl = is_wsl() + return [ + entry.to_tool() + for entry in self._entries.values() + if on_wsl or not self.get_raw_data(entry.name).get("requires_wsl") + ] def get_package_manager_tools(self) -> list[ToolCatalogEntry]: """Get tools that use package_manager install method. diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 86249e2..946f135 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -6,7 +6,7 @@ ## Overview -33 Bash scripts for installing developer tools with multiple actions: +34 Bash scripts for installing developer tools with multiple actions: - **install**: Fresh installation (default action) - **update**: Upgrade to latest version - **uninstall**: Remove installation @@ -28,6 +28,7 @@ - `install_ansible.sh`, `install_brew.sh`, `install_composer.sh` - `install_docker.sh`, `install_gem.sh`, `install_parallel.sh`, `install_yarn.sh` - `install_tmux.sh`, `install_tree.sh` (single-binary utilities) +- `install_wslu.sh`: WSL utilities (`wslview`) — installs only under WSL; optionally sets `wslview` as the default browser **Upgrade & management:** - `guide.sh`: Interactive upgrade guide (supports 'a' for auto-update) @@ -94,7 +95,7 @@ make scripts-perms # Ensure all scripts are executable ./scripts/install_node.sh reconcile ``` -**Via Make (works for all 97 cataloged tools):** +**Via Make (works for all 98 cataloged tools):** ```bash make install-python # Install Python toolchain (dedicated script) make upgrade-python # Upgrade Python toolchain diff --git a/scripts/install_wslu.sh b/scripts/install_wslu.sh new file mode 100755 index 0000000..e12e604 --- /dev/null +++ b/scripts/install_wslu.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# wslu provides `wslview`, which opens URLs/files in the Windows default browser. +# It is only meaningful under WSL, so every action is a graceful no-op elsewhere. + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/lib/common.sh" + +ACTION="${1:-install}" + +wslview_version() { have wslview && wslview --version 2>/dev/null | head -1 || true; } + +set_default_browser() { + # Point the xdg default browser at wslview so links (xdg-open, Python's + # webbrowser, MCP servers, etc.) open in the Windows browser instead of a + # WSLg Linux browser. Opt out with WSLU_SET_DEFAULT_BROWSER=0. + [ "${WSLU_SET_DEFAULT_BROWSER:-1}" = "1" ] || { + echo "[wslu] WSLU_SET_DEFAULT_BROWSER=0 - leaving default browser unchanged" + return 0 + } + have xdg-settings || { + echo "[wslu] xdg-settings not found - skipping default-browser setup" + return 0 + } + local current + current="$(xdg-settings get default-web-browser 2>/dev/null || true)" + if [ "$current" = "wslview.desktop" ]; then + echo "[wslu] default web browser already wslview.desktop" + return 0 + fi + if xdg-settings set default-web-browser wslview.desktop 2>/dev/null; then + echo "[wslu] default web browser set to wslview.desktop (was: ${current:-unset})" + else + echo "[wslu] could not set default browser (non-fatal); run: xdg-settings set default-web-browser wslview.desktop" + fi +} + +install_wslu() { + if ! is_wsl; then + echo "[wslu] Not running under WSL - nothing to do (wslu/wslview only applies on WSL)." + return 0 + fi + echo "[wslu] current: $(wslview_version)" + if have apt-get; then + apt_install_if_missing wslu || { echo "[wslu] apt install failed" >&2; return 1; } + elif have dnf; then + sudo dnf install -y wslu + elif have pacman; then + sudo pacman -S --noconfirm wslu + elif have zypper; then + sudo zypper install -y wslu + else + echo "[wslu] No supported package manager found." >&2 + echo "[wslu] Install wslu manually: https://wslutiliti.es/wslu/install.html" >&2 + return 1 + fi + echo "[wslu] installed: $(wslview_version)" + set_default_browser +} + +uninstall_wslu() { + if ! is_wsl; then + echo "[wslu] Not running under WSL - nothing to do." + echo "[wslu] If wslu was installed manually elsewhere, remove it with your package manager (e.g. sudo apt-get remove wslu)." + return 0 + fi + if have apt-get; then + apt_remove_if_present wslu + elif have dnf; then + sudo dnf remove -y wslu || true + elif have pacman; then + sudo pacman -R --noconfirm wslu || true + elif have zypper; then + sudo zypper remove -y wslu || true + else + echo "[wslu] Please remove wslu via your system package manager." >&2 + fi +} + +case "$ACTION" in + install|update) install_wslu ;; + uninstall) uninstall_wslu ;; + *) echo "Usage: $0 {install|update|uninstall}" >&2 ; exit 2 ;; +esac diff --git a/tests/test_wsl_gating.py b/tests/test_wsl_gating.py new file mode 100644 index 0000000..555ef24 --- /dev/null +++ b/tests/test_wsl_gating.py @@ -0,0 +1,38 @@ +"""Tests for WSL-conditional catalog tools (the ``requires_wsl`` gate). + +A tool flagged ``requires_wsl`` (currently ``wslu``/``wslview``) must only be +surfaced when running under WSL, so non-WSL machines are neither prompted to +install it nor shown it as missing. +""" + +from unittest.mock import patch + +from cli_audit.catalog import ToolCatalog + + +@patch("cli_audit.collectors.is_wsl", return_value=True) +def test_wsl_only_tool_present_on_wsl(_mock_is_wsl): + names = {t.name for t in ToolCatalog().all_tool_definitions()} + assert "wslu" in names + + +@patch("cli_audit.collectors.is_wsl", return_value=False) +def test_wsl_only_tool_hidden_off_wsl(_mock_is_wsl): + names = {t.name for t in ToolCatalog().all_tool_definitions()} + assert "wslu" not in names + + +@patch("cli_audit.collectors.is_wsl", return_value=False) +def test_non_wsl_tools_unaffected_off_wsl(_mock_is_wsl): + """Gating a WSL-only tool must not drop ordinary tools.""" + names = {t.name for t in ToolCatalog().all_tool_definitions()} + assert "ripgrep" in names + + +def test_wslu_catalog_entry_is_wsl_gated(): + """The wslu entry must stay WSL-gated, apt/skip-upstream, wslview-detected.""" + raw = ToolCatalog().get_raw_data("wslu") + assert raw.get("requires_wsl") is True + assert raw.get("skip_upstream") is True + assert raw.get("binary_name") == "wslview" + assert raw.get("install_method") == "dedicated_script"