From 2eaf058944ed6fca982292ab3ad84eb45840b30d Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 22 Sep 2026 21:33:03 +0200 Subject: [PATCH 1/4] feat(catalog): add the pi coding agent and the Google Jules CLI pi: installed from npm as @earendil-works/pi-coding-agent. The earlier @mariozechner/pi-coding-agent package is deprecated on npm ("please use @earendil-works/pi-coding-agent instead going forward") and stops at 0.73.1. Upstream version from GitHub earendil-works/pi (v0.87.0), like the other npm agents that carry a github_repo. `pi --help` offers no completion command. jules: @google/jules, the CLI for Google's asynchronous coding agent. The version comes from `jules version` ("Version: v0.1.42"; `--version` is rejected). No public source repository (the npm repository field points to an awesome list), so the upstream version comes from npm. `jules completion bash` registers `jules` and is declared. herdr and the Google Gemini CLI were already in the catalog. Also updates the catalog count in AGENTS.md, scripts/AGENTS.md, README.md and docs/INDEX.md to the 107 files now present (they said 97 or 98 while 105 were present), the completion lists in COVERAGE.md, and docs/TOOL_ECOSYSTEM.md. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_0173iU8K2MEQvsnkdM6BQH89 Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- AGENTS.md | 2 +- CHANGELOG.md | 2 ++ README.md | 4 ++-- catalog/COVERAGE.md | 6 +++--- catalog/jules.json | 22 ++++++++++++++++++++++ catalog/pi.json | 20 ++++++++++++++++++++ docs/INDEX.md | 4 ++-- docs/TOOL_ECOSYSTEM.md | 16 ++++++++++++++++ scripts/AGENTS.md | 2 +- 9 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 catalog/jules.json create mode 100644 catalog/pi.json diff --git a/AGENTS.md b/AGENTS.md index 977c44d..4252bb6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -136,7 +136,7 @@ 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:** 98 JSON tool definitions under `catalog/` +- **Catalog:** 107 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) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d9f598..299c30d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- `pi` catalog entry: the pi coding agent, installed from npm as `@earendil-works/pi-coding-agent` (the earlier `@mariozechner/pi-coding-agent` package is deprecated); upstream version from GitHub `earendil-works/pi`. +- `jules` catalog entry: the Google Jules CLI (`@google/jules`), Google's asynchronous coding agent, with bash completion from `jules completion bash`. - `herdr` catalog entry (`catalog/herdr.json`): terminal multiplexer for coding agents, installed as a raw linux binary from GitHub releases (x86_64, aarch64) with bash completion. - `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). diff --git a/README.md b/README.md index 73f10f7..db37448 100644 --- a/README.md +++ b/README.md @@ -468,7 +468,7 @@ make test-parallel # Parallel via pytest-xdist ## Installation scripts -All 97 cataloged tools can be installed, upgraded, uninstalled, or reconciled via generic Make targets: +All 107 cataloged tools can be installed, upgraded, uninstalled, or reconciled via generic Make targets: ```bash # Generic pattern targets - work for ANY cataloged tool @@ -539,7 +539,7 @@ When ambiguous, the audit may report a generic bucket (e.g., `~/.local/bin`). Th ### Actions: install, upgrade, uninstall, reconcile -All pattern targets (`install-%`, `upgrade-%`, `uninstall-%`, `reconcile-%`) work for any of the 97 cataloged tools. They use a three-step fallback: dedicated script, then generic installer, then error. +All pattern targets (`install-%`, `upgrade-%`, `uninstall-%`, `reconcile-%`) work for any of the 107 cataloged tools. They use a three-step fallback: dedicated script, then generic installer, then error. ```bash # Upgrade any tool diff --git a/catalog/COVERAGE.md b/catalog/COVERAGE.md index ffe0c90..3093178 100644 --- a/catalog/COVERAGE.md +++ b/catalog/COVERAGE.md @@ -71,11 +71,11 @@ Every catalog entry was audited for a bash-completion generator (sweep of `complete -…` / `compgen ` / `COMPREPLY`, then checked to confirm it registers the entry's own `binary_name`). -**40 entries declare `bash_completion`** — 39 `command`, 1 `source_path` (rbenv). +**41 entries declare `bash_completion`** — 40 `command`, 1 `source_path` (rbenv). Declared (`command`): ast-grep, bat, black, codex, composer, dasel, delta, dive, docker, fd, fx, gh, git-absorb, git-lfs, gitleaks, glab, golangci-lint, gup, jj, -just, kubectl, mlr, node, npm, parallel, pip, pipx, pnpm, poetry, ripgrep, ruff, +jules, just, kubectl, mlr, node, npm, parallel, pip, pipx, pnpm, poetry, ripgrep, ruff, scc, symfony, trivy, uv, vhs, watchexec, yq, zellij Declared (`source_path`): rbenv (`completions/rbenv.bash`, under its `clone_path`) @@ -99,7 +99,7 @@ The completion file is named after `binary_name`, so a script that registers a actionlint, ansible-core, aws, bandit, claude, ctags, curlie, difftastic, direnv, dust, entr, eslint, flake8, gam, gem, gemini, gh-aw-firewall, git, git-branchless, git-filter-repo, go, google-workspace-cli, gosec, httpie, -hyperfine, isort, jq, ninja, opengrep, php, pre-commit, prename, prettier, +hyperfine, isort, jq, ninja, opengrep, php, pi, pre-commit, prename, prettier, python, qsv, rename.ul, ruby, ruby-build, sd, semgrep, shellcheck, shfmt, sponge, templ, terraform, tfsec, tmux, tokei, tree, vault, wslu, xsv, yarn diff --git a/catalog/jules.json b/catalog/jules.json new file mode 100644 index 0000000..d9360bc --- /dev/null +++ b/catalog/jules.json @@ -0,0 +1,22 @@ +{ + "name": "jules", + "category": "ai", + "install_method": "npm_global", + "description": "Google Jules CLI - terminal client for Jules, Google's asynchronous coding agent", + "homepage": "https://jules.google", + "package_name": "@google/jules", + "binary_name": "jules", + "requires": [ + "node" + ], + "version_command": "jules version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1", + "guide": { + "display_name": "Google Jules CLI", + "install_action": "upgrade", + "order": 405 + }, + "notes": "Sessions run remotely (`jules remote`); `jules teleport` applies a session's changes locally. Sign in with `jules login`. Requires Node.js >= 18.", + "bash_completion": { + "command": "jules completion bash" + } +} diff --git a/catalog/pi.json b/catalog/pi.json new file mode 100644 index 0000000..c28460d --- /dev/null +++ b/catalog/pi.json @@ -0,0 +1,20 @@ +{ + "name": "pi", + "category": "ai", + "install_method": "npm_global", + "description": "pi - coding agent CLI with read, bash, edit and write tools and session management", + "homepage": "https://github.com/earendil-works/pi", + "github_repo": "earendil-works/pi", + "package_name": "@earendil-works/pi-coding-agent", + "binary_name": "pi", + "requires": [ + "node" + ], + "version_command": "pi --version 2>/dev/null | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1", + "guide": { + "display_name": "pi coding agent", + "install_action": "upgrade", + "order": 404 + }, + "notes": "Published as @earendil-works/pi-coding-agent; the earlier @mariozechner/pi-coding-agent package is deprecated. Requires Node.js >= 20.6." +} diff --git a/docs/INDEX.md b/docs/INDEX.md index 44feabe..19f2d7b 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -7,7 +7,7 @@ AI CLI Preparation is a specialized environment audit tool designed to ensure AI coding agents (like Claude Code) have access to all necessary developer tools. This documentation provides comprehensive technical details for developers, contributors, and integrators. -**Architecture:** Modular design with 21 specialized Python modules and 97 JSON tool catalog entries, evolved from a 3,387-line monolith to a maintainable, extensible system. +**Architecture:** Modular design with 21 specialized Python modules and 107 JSON tool catalog entries, evolved from a 3,387-line monolith to a maintainable, extensible system. **Project Status:** - **Phase 1 (Detection & Auditing):** ✅ Complete - Modular refactoring complete (v2.0.0) @@ -34,7 +34,7 @@ AI CLI Preparation is a specialized environment audit tool designed to ensure AI 2. **[CATALOG_GUIDE.md](CATALOG_GUIDE.md)** - JSON Catalog System ⭐ **NEW** - Tool definition schema and structure - - 97 JSON catalog entries + - 107 JSON catalog entries - Creating and managing catalog entries - ToolCatalog API and usage patterns - Community contribution workflows diff --git a/docs/TOOL_ECOSYSTEM.md b/docs/TOOL_ECOSYSTEM.md index 117ceeb..b180a5a 100644 --- a/docs/TOOL_ECOSYSTEM.md +++ b/docs/TOOL_ECOSYSTEM.md @@ -579,6 +579,22 @@ make audit-offline-data-core - **Install:** `npm install -g @google/gemini-cli` - **Upgrade:** `npm update -g @google/gemini-cli` +### jules +- **Purpose:** Google Jules CLI - terminal client for Jules, Google's asynchronous coding agent +- **Executable:** `jules` +- **Upstream:** npm (@google/jules) +- **Use Case:** Assign coding sessions that run remotely and apply their changes locally (`jules teleport`) +- **Install:** `npm install -g @google/jules` +- **Upgrade:** `npm update -g @google/jules` + +### pi +- **Purpose:** pi - coding agent CLI with read, bash, edit and write tools and session management +- **Executable:** `pi` +- **Upstream:** GitHub (earendil-works/pi), published on npm as @earendil-works/pi-coding-agent +- **Use Case:** Minimal terminal coding agent +- **Install:** `npm install -g @earendil-works/pi-coding-agent` +- **Upgrade:** `npm update -g @earendil-works/pi-coding-agent` + ### claude - **Purpose:** Anthropic Claude Code CLI - **Executable:** `claude` diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index 63e9eb9..a98a8a9 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -123,7 +123,7 @@ make scripts-perms # Ensure all scripts are executable ./scripts/install_node.sh reconcile ``` -**Via Make (works for all 98 cataloged tools):** +**Via Make (works for all 107 cataloged tools):** ```bash make install-python # Install Python toolchain (dedicated script) make upgrade-python # Upgrade Python toolchain From 683dc3c7e7528ac50db0e9cee23f5a1bcbd75b1e Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 22 Sep 2026 21:41:41 +0200 Subject: [PATCH 2/4] fix(scripts): uninstall npm packages by their package name remove_installation ran `npm uninstall -g `. For pi, installed as @earendil-works/pi-coding-agent, `make uninstall-pi` therefore left the agent in place and named an unrelated npm package `pi`. The npm handler now reads the package from the binary's symlink into node_modules and falls back to the tool name only outside node_modules. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_0173iU8K2MEQvsnkdM6BQH89 Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- CHANGELOG.md | 1 + scripts/lib/reconcile.sh | 20 ++++++++- tests/test_npm_uninstall.py | 85 +++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 tests/test_npm_uninstall.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 299c30d..76b6d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,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 +- `make uninstall-` removed an npm global package by the tool name. For `pi` (package `@earendil-works/pi-coding-agent`) that ran `npm uninstall -g pi`, which leaves the agent installed and names an unrelated npm package `pi`. The package name is now read from the binary's symlink into `node_modules`. - The network-free refresh that `make upgrade` runs first (`audit.py --update-local`) skipped every multi-version row, so `python@3.14`, `node@26` and the other cycles kept the version they were last written with. After a successful upgrade the guide still offered the same upgrade until an install ran again. The refresh re-detects the cycle rows, the same way the post-install merge refresh already did. - 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. diff --git a/scripts/lib/reconcile.sh b/scripts/lib/reconcile.sh index d9427b2..9bb86dc 100755 --- a/scripts/lib/reconcile.sh +++ b/scripts/lib/reconcile.sh @@ -65,8 +65,24 @@ remove_installation() { ;; npm) if command -v npm >/dev/null 2>&1; then - echo "[$tool] Uninstalling npm global package: $tool" >&2 - npm uninstall -g "$tool" 2>/dev/null || true + # The package name can differ from the tool name (pi is + # @earendil-works/pi-coding-agent, and an unrelated package `pi` + # exists), so read it from the bin symlink: node_modules//... + local npm_pkg="$tool" npm_path npm_target + npm_path="${known_path:-$(command -v "$binary" 2>/dev/null || echo "")}" + npm_target="$(readlink -f "$npm_path" 2>/dev/null || echo "")" + case "$npm_target" in + */node_modules/@*/*) + npm_pkg="${npm_target##*/node_modules/}" + npm_pkg="$(echo "$npm_pkg" | cut -d/ -f1-2)" + ;; + */node_modules/*) + npm_pkg="${npm_target##*/node_modules/}" + npm_pkg="${npm_pkg%%/*}" + ;; + esac + echo "[$tool] Uninstalling npm global package: $npm_pkg" >&2 + npm uninstall -g "$npm_pkg" 2>/dev/null || true fi ;; nvm) diff --git a/tests/test_npm_uninstall.py b/tests/test_npm_uninstall.py new file mode 100644 index 0000000..994ca06 --- /dev/null +++ b/tests/test_npm_uninstall.py @@ -0,0 +1,85 @@ +"""Tests for the npm handler of remove_installation: the package name comes +from the binary's symlink into node_modules, not from the tool name.""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +from pathlib import Path + +import pytest + +skip_on_windows = pytest.mark.skipif(sys.platform == "win32", reason="Shell script tests require POSIX shell") + +SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" + + +def _write_stub(stub_dir: Path, name: str, body: str) -> Path: + """Create an executable stub command in stub_dir.""" + stub = stub_dir / name + stub.write_text(f"#!/usr/bin/env bash\n{body}\n") + stub.chmod(0o755) + return stub + + +@skip_on_windows +class TestRemoveInstallationNpmPackageName: + """The npm handler must uninstall the package that owns the binary.""" + + def _remove(self, tmpdir: str, bin_target: str, tool: str) -> str: + """Link /bin/ to bin_target and return the npm calls.""" + prefix = Path(tmpdir) / "prefix" + target = prefix / "lib" / bin_target + target.parent.mkdir(parents=True) + target.write_text("#!/usr/bin/env node\n") + (prefix / "bin").mkdir() + link = prefix / "bin" / tool + link.symlink_to(Path("..") / "lib" / bin_target) + + stub_dir = Path(tmpdir) / "stubs" + stub_dir.mkdir() + log_file = stub_dir / "calls.log" + log_file.touch() + _write_stub(stub_dir, "npm", f'echo "npm $*" >> "{log_file}"') + + result = subprocess.run( + [ + "bash", + "-c", + f""" +set -euo pipefail +source "{SCRIPTS_DIR}/lib/reconcile.sh" +export PATH="{stub_dir}:$PATH" +hash -r +remove_installation "{tool}" "npm" "{tool}" "{link}" +""", + ], + capture_output=True, + text=True, + timeout=10, + ) + assert result.returncode == 0, result.stderr + return log_file.read_text() + + def test_scoped_package_is_read_from_the_bin_symlink(self): + # The layout of a real `npm install -g @earendil-works/pi-coding-agent`. + # A bare `npm uninstall -g pi` would target the unrelated package `pi`. + with tempfile.TemporaryDirectory() as tmpdir: + log = self._remove( + tmpdir, + "node_modules/@earendil-works/pi-coding-agent/dist/bundle/cli.js", + "pi", + ) + assert log == "npm uninstall -g @earendil-works/pi-coding-agent\n" + + def test_unscoped_package_is_read_from_the_bin_symlink(self): + with tempfile.TemporaryDirectory() as tmpdir: + # `tsc` is the bin of the package `typescript` + log = self._remove(tmpdir, "node_modules/typescript/bin/tsc", "tsc") + assert log == "npm uninstall -g typescript\n" + + def test_binary_outside_node_modules_falls_back_to_tool_name(self): + with tempfile.TemporaryDirectory() as tmpdir: + log = self._remove(tmpdir, "other/sometool", "sometool") + assert log == "npm uninstall -g sometool\n" From beb33b6a35d513a1855972cff1e0c3111f0a1de5 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 22 Sep 2026 21:41:46 +0200 Subject: [PATCH 3/4] docs(catalog): correct coverage counts and the pi Node.js floor COVERAGE.md lists the install methods of all 107 entries, with the dedicated_script and auto routing as the installers implement it. The completion section counts herdr (42 entries, 41 command). pi 0.87.0 requires Node.js >= 22.19. The remaining catalog counts in CATALOG_GUIDE.md, API_REFERENCE.md and catalog/README.md state 107. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_0173iU8K2MEQvsnkdM6BQH89 Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- catalog/COVERAGE.md | 88 +++++++++++-------------------------------- catalog/README.md | 4 +- catalog/pi.json | 2 +- docs/API_REFERENCE.md | 4 +- docs/CATALOG_GUIDE.md | 2 +- 5 files changed, 29 insertions(+), 71 deletions(-) diff --git a/catalog/COVERAGE.md b/catalog/COVERAGE.md index 3093178..c4b82ad 100644 --- a/catalog/COVERAGE.md +++ b/catalog/COVERAGE.md @@ -1,68 +1,26 @@ # Catalog Coverage -This file documents which tools have catalog entries and which use dedicated install scripts. - -## Tools with Catalog Entries (57) - -These tools use the catalog-based installation system with generic installers: - -- ansible, ast-grep, aws, bandit, bat, black, codex, composer, curlie, dasel -- delta, direnv, dive, entr, fd, flake8, fx, fzf, gem, gemini, gh, git-absorb -- git-branchless, git-lfs, gitleaks, glab, golangci-lint, httpie, isort, just -- kubectl, ninja, npm, opengrep, parallel, pip, pipx, pnpm, poetry, pre-commit -- prettier, rga, ripgrep, ruff, sd, semgrep, shellcheck, shfmt, sponge, terraform -- tfsec, trivy, vault, watchexec, xsv, yarn, yq - -## Tools with Dedicated Install Scripts - -### Runtime Environments -These have their own complex installers in `scripts/`: -- **go** - `install_go.sh` -- **rust** - `install_rust.sh` -- **python** - `install_python.sh` -- **node** - `install_node.sh` - -### Package Managers -Most now in catalog, one dedicated script: -- **uv** - `install_uv.sh` (special bootstrap installer) -- All others (pip, pipx, npm, pnpm, yarn, gem, composer, poetry, sponge) - Now in catalog! - -### Docker Tools -- **docker** - `install_docker.sh` (uses official Docker install script) -- **docker-compose** - Typically installed with Docker - -### System Tools -- **git** - System package (apt/dnf/brew) -- **ctags** - System package -- **sponge** - Part of moreutils package -- **prename** - System package (Perl rename) -- **rename.ul** - System package (util-linux rename) - -### Other -- **gam** - Google Apps Manager (special installation) -- **claude** - Claude CLI (special installation) -- **ansible-core** - Subset of ansible package -- **eslint** - Node.js package (installed via npm) - -## Installation Method Distribution - -- **github_release_binary**: 32 tools -- **uv_tool**: 8 tools (Python CLI tools) -- **package_manager**: 10 tools (pip, pipx, poetry, npm, pnpm, yarn, gem, composer, sponge, entr) -- **hashicorp_zip**: 2 tools (terraform, vault) -- **aws_installer**: 1 tool (aws) -- **npm_global**: 1 tool (prettier) -- **script**: 1 tool (parallel) -- **dedicated_script**: 10 tools (runtimes: go, rust, python, node; special: uv, docker, git, ctags, gam) -- **system_package**: 2 tools (cscope, rename variants) - -## Total: 72 tools tracked - -- **57 tools** have catalog entries -- **10 tools** use dedicated scripts (runtimes + special cases) -- **5 tools** are system packages only - -All installable tools either have catalog entries or use appropriate dedicated scripts. +This file documents which tools have catalog entries and how each one is installed. + +## Catalog entries (107) + +Counted from `catalog/*.json` on 2026-09-22. Every tool the audit tracks has a catalog entry; its `install_method` selects the installer (`scripts/installers/.sh`). `dedicated_script` runs the script named in the entry's `script` field. `auto` goes through the reconciliation system (`reconcile_tool` in `scripts/lib/reconcile.sh`). + +| install_method | Count | Tools | +|---|---|---| +| `github_release_binary` | 38 | ast-grep, curlie, dasel, direnv, dive, fx, fzf, gh, gh-aw, gh-aw-firewall, git-absorb, git-branchless, git-lfs, gitleaks, glab, golangci-lint, google-workspace-cli, gosec, herdr, jq, just, kubectl, mlr, ninja, opengrep, qsv, rga, sd, shellcheck, shfmt, symfony, tfsec, trivy, vhs, watchexec, xsv, yq, zellij | +| `dedicated_script` | 18 | blesh, byobu, claude, composer, docker, gem, go, node, parallel, pip, python, ruby, rust, tmux, tree, uv, wslu, yarn | +| `auto` | 13 | actionlint, bat, delta, difftastic, dust, fd, gup, hyperfine, jj, pnpm, ripgrep, scc, tokei | +| `uv_tool` | 12 | ansible-core, bandit, black, flake8, gam, git-filter-repo, httpie, isort, pre-commit, ruff, semgrep, trustmux | +| `package_manager` | 10 | bwrap, ctags, entr, git, php, pipx, poetry, prename, rename.ul, sponge | +| `npm_global` | 7 | bw, codex, eslint, gemini, jules, pi, prettier | +| `github_clone` | 2 | rbenv, ruby-build | +| `hashicorp_zip` | 2 | terraform, vault | +| `aws_installer` | 1 | aws | +| `docker_plugin` | 1 | compose | +| `gcloud_installer` | 1 | gcloud | +| `go_install` | 1 | templ | +| `npm_self_update` | 1 | npm | ## Bash completion coverage @@ -71,10 +29,10 @@ Every catalog entry was audited for a bash-completion generator (sweep of `complete -…` / `compgen ` / `COMPREPLY`, then checked to confirm it registers the entry's own `binary_name`). -**41 entries declare `bash_completion`** — 40 `command`, 1 `source_path` (rbenv). +**42 entries declare `bash_completion`** — 41 `command`, 1 `source_path` (rbenv). Declared (`command`): ast-grep, bat, black, codex, composer, dasel, delta, dive, -docker, fd, fx, gh, git-absorb, git-lfs, gitleaks, glab, golangci-lint, gup, jj, +docker, fd, fx, gh, git-absorb, git-lfs, gitleaks, glab, golangci-lint, gup, herdr, jj, jules, just, kubectl, mlr, node, npm, parallel, pip, pipx, pnpm, poetry, ripgrep, ruff, scc, symfony, trivy, uv, vhs, watchexec, yq, zellij diff --git a/catalog/README.md b/catalog/README.md index 455c093..8dde570 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 **103 tools** are cataloged. +Currently **107 tools** are cataloged. ## Environment Variables @@ -96,6 +96,6 @@ make upgrade ## Architecture -All 103 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 107 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/pi.json b/catalog/pi.json index c28460d..c1d7668 100644 --- a/catalog/pi.json +++ b/catalog/pi.json @@ -16,5 +16,5 @@ "install_action": "upgrade", "order": 404 }, - "notes": "Published as @earendil-works/pi-coding-agent; the earlier @mariozechner/pi-coding-agent package is deprecated. Requires Node.js >= 20.6." + "notes": "Published as @earendil-works/pi-coding-agent; the earlier @mariozechner/pi-coding-agent package is deprecated. Requires Node.js >= 22.19." } diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 1ed4bc3..e93e4ff 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -122,7 +122,7 @@ class ToolCatalog: ```python from cli_audit import ToolCatalog -# Load catalog (97 tools) +# Load catalog (107 tools) catalog = ToolCatalog() entries = catalog.load_all() @@ -137,7 +137,7 @@ if catalog.has("fzf"): fzf = catalog.get("fzf") # List all tools -all_tools = catalog.list_all() # Returns list of 97 tool names +all_tools = catalog.list_all() # Returns list of 107 tool names ``` #### `cli_audit.collectors` - Upstream Version Collection diff --git a/docs/CATALOG_GUIDE.md b/docs/CATALOG_GUIDE.md index aacfd2a..d7f4c39 100644 --- a/docs/CATALOG_GUIDE.md +++ b/docs/CATALOG_GUIDE.md @@ -12,7 +12,7 @@ The catalog system allows tool definitions to be managed as JSON files instead o - **Clear Separation**: Data (JSON) vs Logic (Python) **Location**: `catalog/` directory at project root -**Current Size**: 97 JSON tool definitions +**Current Size**: 107 JSON tool definitions ## Quick Start From e2211a8599c1abe752f388253b0cbfa97f4a20ae Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 22 Sep 2026 21:51:42 +0200 Subject: [PATCH 4/4] fix(scripts): read the npm package from one symlink level `npm link` and `npm install -g ` make the package directory a symlink too. readlink -f resolved past node_modules, and the handler fell back to `npm uninstall -g pi`. The handler now resolves only the bin link and takes the package after the first node_modules, so a bundled dependency cannot stand in for the owning package. The tests run with a PATH that holds only the npm stub and the system directories, so they do not depend on tools installed on the host. COVERAGE.md names the entries its completion audit did not cover and the entries checked after the sweep. Assisted-by: claude-code:claude-opus-5-5 Agent-Session: https://claude.ai/code/session_0173iU8K2MEQvsnkdM6BQH89 Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel --- catalog/COVERAGE.md | 11 +++++++---- scripts/lib/reconcile.sh | 21 ++++++++++++--------- tests/test_npm_uninstall.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/catalog/COVERAGE.md b/catalog/COVERAGE.md index c4b82ad..03b308f 100644 --- a/catalog/COVERAGE.md +++ b/catalog/COVERAGE.md @@ -24,10 +24,13 @@ Counted from `catalog/*.json` on 2026-09-22. Every tool the audit tracks has a c ## Bash completion coverage -Every catalog entry was audited for a bash-completion generator (sweep of -2026-07-22; each generator was executed and its output validated against -`complete -…` / `compgen ` / `COMPREPLY`, then checked to confirm it registers -the entry's own `binary_name`). +A sweep on 2026-07-22 (commit 9b6c053) audited the entries present in that +commit for a bash-completion generator: each generator was executed and its +output validated against `complete -…` / `compgen ` / `COMPREPLY`, then +checked to confirm it registers the entry's own `binary_name`. codex, pip and +pipx were not installed then; see the end of this section. herdr, jules, pi +and vault were checked when they were added. Five entries are not audited: +blesh, bw, bwrap, byobu, trustmux. **42 entries declare `bash_completion`** — 41 `command`, 1 `source_path` (rbenv). diff --git a/scripts/lib/reconcile.sh b/scripts/lib/reconcile.sh index 9bb86dc..7b61cda 100755 --- a/scripts/lib/reconcile.sh +++ b/scripts/lib/reconcile.sh @@ -68,17 +68,20 @@ remove_installation() { # The package name can differ from the tool name (pi is # @earendil-works/pi-coding-agent, and an unrelated package `pi` # exists), so read it from the bin symlink: node_modules//... - local npm_pkg="$tool" npm_path npm_target + # One level only: `npm link` and folder installs make the package + # directory a symlink too, and readlink -f would resolve past it. + local npm_pkg="$tool" npm_path npm_target npm_rest npm_path="${known_path:-$(command -v "$binary" 2>/dev/null || echo "")}" - npm_target="$(readlink -f "$npm_path" 2>/dev/null || echo "")" + npm_target="$(readlink "$npm_path" 2>/dev/null || echo "")" case "$npm_target" in - */node_modules/@*/*) - npm_pkg="${npm_target##*/node_modules/}" - npm_pkg="$(echo "$npm_pkg" | cut -d/ -f1-2)" - ;; - */node_modules/*) - npm_pkg="${npm_target##*/node_modules/}" - npm_pkg="${npm_pkg%%/*}" + */node_modules/*|node_modules/*) + # First node_modules = the global prefix; a later one would be + # a bundled dependency of the package + npm_rest="${npm_target#*node_modules/}" + case "$npm_rest" in + @*/*) npm_pkg="$(echo "$npm_rest" | cut -d/ -f1-2)" ;; + *) npm_pkg="${npm_rest%%/*}" ;; + esac ;; esac echo "[$tool] Uninstalling npm global package: $npm_pkg" >&2 diff --git a/tests/test_npm_uninstall.py b/tests/test_npm_uninstall.py index 994ca06..b3f5de2 100644 --- a/tests/test_npm_uninstall.py +++ b/tests/test_npm_uninstall.py @@ -27,11 +27,22 @@ def _write_stub(stub_dir: Path, name: str, body: str) -> Path: class TestRemoveInstallationNpmPackageName: """The npm handler must uninstall the package that owns the binary.""" - def _remove(self, tmpdir: str, bin_target: str, tool: str) -> str: - """Link /bin/ to bin_target and return the npm calls.""" + def _remove(self, tmpdir: str, bin_target: str, tool: str, linked_pkg: str = "") -> str: + """Link /bin/ to bin_target and return the npm calls. + + linked_pkg: make node_modules/ a symlink to a source + folder, the layout of `npm link` and `npm install -g `. + """ prefix = Path(tmpdir) / "prefix" target = prefix / "lib" / bin_target - target.parent.mkdir(parents=True) + if linked_pkg: + pkg_dir = prefix / "lib" / "node_modules" / linked_pkg + source = Path(tmpdir) / "src" + (source / Path(bin_target).relative_to(Path("node_modules") / linked_pkg)).parent.mkdir(parents=True) + pkg_dir.parent.mkdir(parents=True, exist_ok=True) + pkg_dir.symlink_to(source) + else: + target.parent.mkdir(parents=True) target.write_text("#!/usr/bin/env node\n") (prefix / "bin").mkdir() link = prefix / "bin" / tool @@ -50,7 +61,7 @@ def _remove(self, tmpdir: str, bin_target: str, tool: str) -> str: f""" set -euo pipefail source "{SCRIPTS_DIR}/lib/reconcile.sh" -export PATH="{stub_dir}:$PATH" +export PATH="{stub_dir}:/usr/bin:/bin" hash -r remove_installation "{tool}" "npm" "{tool}" "{link}" """, @@ -79,6 +90,23 @@ def test_unscoped_package_is_read_from_the_bin_symlink(self): log = self._remove(tmpdir, "node_modules/typescript/bin/tsc", "tsc") assert log == "npm uninstall -g typescript\n" + def test_linked_package_directory_is_not_resolved_past(self): + # `npm link` / `npm install -g `: the package directory is a + # symlink too, so a full resolution would leave node_modules behind + with tempfile.TemporaryDirectory() as tmpdir: + log = self._remove( + tmpdir, + "node_modules/@earendil-works/pi-coding-agent/dist/cli.js", + "pi", + linked_pkg="@earendil-works/pi-coding-agent", + ) + assert log == "npm uninstall -g @earendil-works/pi-coding-agent\n" + + def test_nested_scoped_dependency_names_the_owning_package(self): + with tempfile.TemporaryDirectory() as tmpdir: + log = self._remove(tmpdir, "node_modules/owner/node_modules/@d/e/bin/x", "x") + assert log == "npm uninstall -g owner\n" + def test_binary_outside_node_modules_falls_back_to_tool_name(self): with tempfile.TemporaryDirectory() as tmpdir: log = self._remove(tmpdir, "other/sometool", "sometool")