From 876c9f742d4d559ee0b9dca51880b9ff6646af50 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 16 Jun 2026 19:25:54 +0900 Subject: [PATCH 1/3] feat(install): curl | sh one-liner installer for vouch-kb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh a posix sh script (shellcheck-clean, dash-tested) that: * picks a python >= 3.11 from common candidates (python3.13/3.12/3.11/python3) * installs pipx into user site if missing, no sudo * `pipx install vouch-kb` (or upgrade-in-place when already present) * smoke-tests with `vouch --version` * detects ~/.claude or the `claude` CLI on PATH and points at `vouch install-mcp claude-code` for the per-project wiring step flags: --version X.Y.Z to pin, --no-claude to skip the nudge, --quiet, --help. honours NO_COLOR. re-runnable. ci workflow .github/workflows/install-sh.yml runs shellcheck + a dash syntax check + an end-to-end smoke install on ubuntu-latest that actually exercises the published pypi wheel — catches install-path breakage that the regular pytest run misses. readme install section updated to lead with the one-liner. --- .github/workflows/install-sh.yml | 63 ++++++++ README.md | 10 +- install.sh | 264 +++++++++++++++++++++++++++++++ 3 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/install-sh.yml create mode 100755 install.sh diff --git a/.github/workflows/install-sh.yml b/.github/workflows/install-sh.yml new file mode 100644 index 00000000..f956685c --- /dev/null +++ b/.github/workflows/install-sh.yml @@ -0,0 +1,63 @@ +name: install-sh + +# Validates install.sh on every push that touches it: +# * shellcheck (lint) +# * POSIX-syntax check via dash +# * end-to-end smoke run on a fresh ubuntu-latest — installs the published +# vouch-kb wheel via pipx and verifies `vouch --version` +# +# The smoke run intentionally exercises the published PyPI artifact, not the +# in-repo source, so we catch installation breakage that doesn't show up in +# the regular pytest suite (e.g. a stale [web] extra reference). + +on: + push: + branches: [main, release/*] + paths: + - "install.sh" + - ".github/workflows/install-sh.yml" + pull_request: + paths: + - "install.sh" + - ".github/workflows/install-sh.yml" + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: shellcheck + run: | + sudo apt-get update -qq + sudo apt-get install -y shellcheck dash + shellcheck --version + shellcheck install.sh + + - name: posix syntax (dash -n) + run: dash -n install.sh + + smoke: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: --help works + run: sh ./install.sh --help + + - name: end-to-end install + smoke + run: | + set -e + sh ./install.sh --no-claude + # pipx's bin dir isn't necessarily on PATH for the verification + # step — re-export from pipx itself. + PIPX_BIN=$(python -m pipx environment --value PIPX_BIN_DIR) + export PATH="$PIPX_BIN:$PATH" + vouch --version + vouch capabilities | head -20 diff --git a/README.md b/README.md index a3f298c9..fe13bca5 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,21 @@ Skip it if: ## Install ```bash -# from PyPI (published as vouch-kb; the command is still `vouch`) +# one-liner (Linux + macOS) — picks a Python, ensures pipx, installs vouch-kb +curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh + +# …or directly via pipx (vouch-kb on PyPI; the command stays `vouch`) pipx install vouch-kb # …or from the cloned repo, in a venv pip install -e '.[dev]' ``` +The one-liner is POSIX `sh`, never needs `sudo`, and detects an existing +Claude Code install to point you at the next step (`vouch install-mcp +claude-code`). Inspect it first if you'd like — it's [`install.sh`](install.sh) +at the repo root. + ## Quick start ```bash diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..16184bf8 --- /dev/null +++ b/install.sh @@ -0,0 +1,264 @@ +#!/bin/sh +# vouch installer — pipx-backed, no sudo. +# +# curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh +# +# What this does, in order: +# 1. Pick a Python interpreter (>=3.11) — exits with a hint if none found. +# 2. Make sure pipx is installed (offers a user-scope install if missing). +# 3. Install or upgrade the `vouch-kb` package via pipx. +# 4. Smoke-test: run `vouch --version` and report success. +# 5. If Claude Code config is detected, point you at `vouch install-mcp`. +# +# Safe to re-run. Nothing requires sudo. No network calls beyond pipx's +# normal PyPI fetch. +# +# Flags: +# --version pin a vouch-kb version (default: latest) +# --no-claude skip the Claude Code detection nudge +# --quiet only print errors + the final summary +# --help print this message + +set -eu + +# --- knobs --------------------------------------------------------------- + +PKG_NAME="vouch-kb" +PIN_VERSION="" +SKIP_CLAUDE_CHECK=0 +QUIET=0 +MIN_PY_MAJOR=3 +MIN_PY_MINOR=11 +REPO_URL="https://github.com/vouchdev/vouch" + +# --- pretty output ------------------------------------------------------- + +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + C_BOLD=$(printf '\033[1m') + C_DIM=$(printf '\033[2m') + C_RED=$(printf '\033[31m') + C_GREEN=$(printf '\033[32m') + C_YELLOW=$(printf '\033[33m') + C_BLUE=$(printf '\033[34m') + C_RESET=$(printf '\033[0m') +else + C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_RESET="" +fi + +info() { + [ "$QUIET" -eq 1 ] && return 0 + printf '%s▸%s %s\n' "$C_BLUE" "$C_RESET" "$1" +} + +warn() { + printf '%s!%s %s\n' "$C_YELLOW" "$C_RESET" "$1" 1>&2 +} + +err() { + printf '%s✗%s %s\n' "$C_RED" "$C_RESET" "$1" 1>&2 +} + +ok() { + [ "$QUIET" -eq 1 ] && return 0 + printf '%s✓%s %s\n' "$C_GREEN" "$C_RESET" "$1" +} + +has_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +usage() { + cat <=3.${MIN_PY_MINOR} + 2. installs pipx (user scope) if missing + 3. ${C_BOLD}pipx install ${PKG_NAME}${C_RESET} (or upgrade if already present) + 4. smoke-tests with ${C_BOLD}vouch --version${C_RESET} + 5. points you at Claude Code wiring if applicable + +Re-run safely. No sudo. No network beyond pipx + PyPI. + +Source: ${REPO_URL}/blob/main/install.sh +EOF +} + +# --- arg parsing --------------------------------------------------------- + +while [ $# -gt 0 ]; do + case "$1" in + --version) + shift + [ $# -gt 0 ] || { err "--version needs a value"; exit 2; } + PIN_VERSION="$1" + ;; + --no-claude) + SKIP_CLAUDE_CHECK=1 + ;; + --quiet) + QUIET=1 + ;; + --help|-h) + usage + exit 0 + ;; + *) + err "unknown flag: $1" + usage + exit 2 + ;; + esac + shift +done + +# --- phase 1: pick a Python ---------------------------------------------- + +pick_python() { + # Try, in order: python3.13, 3.12, 3.11, then bare python3. + for cand in python3.13 python3.12 python3.11 python3; do + if has_cmd "$cand"; then + ver=$("$cand" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) || continue + major=${ver%%.*} + minor=${ver##*.} + if [ "$major" -gt "$MIN_PY_MAJOR" ] || \ + { [ "$major" -eq "$MIN_PY_MAJOR" ] && [ "$minor" -ge "$MIN_PY_MINOR" ]; }; then + printf '%s\n' "$cand" + return 0 + fi + fi + done + return 1 +} + +# --- phase 2: ensure pipx ------------------------------------------------- + +ensure_pipx() { + py="$1" + if has_cmd pipx; then + ok "pipx already installed ($(pipx --version 2>/dev/null | head -1))" + return 0 + fi + info "pipx not found — installing into user site (no sudo)" + if ! "$py" -m pip install --user --upgrade pipx >/dev/null 2>&1; then + err "pip install --user pipx failed — try installing it manually" + err " sudo apt install pipx # Debian/Ubuntu" + err " brew install pipx # macOS" + return 1 + fi + "$py" -m pipx ensurepath >/dev/null 2>&1 || true + + # `ensurepath` edits ~/.profile / ~/.zshrc to add pipx's bin dir, but the + # current process's PATH is already fixed. Add it explicitly so the rest + # of this script can find `vouch`. + pipx_bin=$("$py" -m pipx environment --value PIPX_BIN_DIR 2>/dev/null || true) + if [ -z "$pipx_bin" ]; then + pipx_bin="$HOME/.local/bin" + fi + case ":$PATH:" in + *":$pipx_bin:"*) ;; + *) PATH="$pipx_bin:$PATH"; export PATH ;; + esac + + if ! has_cmd pipx; then + err "pipx installed but not on PATH — restart your shell, then re-run" + return 1 + fi + ok "pipx ready at $(command -v pipx)" + return 0 +} + +# --- phase 3: install vouch-kb ------------------------------------------- + +install_vouch() { + target="$PKG_NAME" + if [ -n "$PIN_VERSION" ]; then + target="${PKG_NAME}==${PIN_VERSION}" + fi + + if pipx list 2>/dev/null | grep -q "package $PKG_NAME"; then + info "upgrading existing $PKG_NAME" + if ! pipx upgrade --pip-args="" "$PKG_NAME" >/dev/null 2>&1; then + warn "pipx upgrade failed — falling back to reinstall" + pipx install --force "$target" >/dev/null + fi + else + info "installing $target" + pipx install "$target" >/dev/null + fi + ok "$PKG_NAME installed" +} + +# --- phase 4: smoke test -------------------------------------------------- + +smoke_test() { + if ! has_cmd vouch; then + err "vouch command not found after install — check pipx's bin dir is on PATH" + return 1 + fi + if ! ver=$(vouch --version 2>&1); then + err "vouch installed but \`vouch --version\` failed:" + err " $ver" + return 1 + fi + ok "$ver" + return 0 +} + +# --- phase 5: Claude Code nudge ------------------------------------------ + +claude_code_nudge() { + [ "$SKIP_CLAUDE_CHECK" -eq 1 ] && return 0 + + # Detect Claude Code in the most non-invasive way: ~/.claude/ exists OR + # the `claude` CLI is on PATH. Both are common. + if [ -d "$HOME/.claude" ] || has_cmd claude; then + printf '\n' + info "${C_BOLD}Claude Code detected.${C_RESET}" + info "Wire vouch into a project (one-time, per repo):" + printf '\n' + printf ' %scd /path/to/your/project%s\n' "$C_BOLD" "$C_RESET" + printf ' %svouch init%s\n' "$C_BOLD" "$C_RESET" + printf ' %svouch install-mcp claude-code%s\n' "$C_BOLD" "$C_RESET" + printf '\n' + info "Then restart Claude Code — vouch's kb.* tools and slash commands" + info "(/vouch-recall, /vouch-status, …) will be available." + fi +} + +# --- main ---------------------------------------------------------------- + +main() { + info "${C_BOLD}vouch installer${C_RESET} ${C_DIM}($REPO_URL)${C_RESET}" + + if ! py=$(pick_python); then + err "no Python >=$MIN_PY_MAJOR.$MIN_PY_MINOR found on PATH." + err "Install Python first:" + err " https://www.python.org/downloads/" + err " brew install python@3.12 # macOS" + err " sudo apt install python3.12 # Debian/Ubuntu 24.04+" + exit 1 + fi + ok "python: $(command -v "$py")" + + ensure_pipx "$py" || exit 1 + install_vouch + smoke_test || exit 1 + + printf '\n' + info "${C_BOLD}Next:${C_RESET}" + info " ${C_BOLD}vouch init${C_RESET} # create a .vouch/ KB in your project" + info " ${C_BOLD}vouch serve${C_RESET} # start the MCP server" + info " ${C_BOLD}vouch --help${C_RESET} # the rest" + claude_code_nudge + + printf '\n' + ok "done. happy reviewing." +} + +main From 964e445234b84f9fe127cc2b9be834fb5085eded Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:28:30 +0900 Subject: [PATCH 2/3] fix(install): address coderabbit review on PR #216 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit three findings, all real: 1. workflow supply chain — install-sh.yml now pins both third-party actions to commit SHAs (actions/checkout v4.2.2, setup-python v5.3.0) with human-readable tags in the comment, sets workflow-level `permissions: contents: read`, and disables credential persistence on every checkout. note in the file flags that ci.yml / release.yml / schema-check.yml use the older tag-pin convention; sweeping them is a worthwhile follow-up but kept out of this pr to avoid churn. 2. PATH normalization — the previous `ensure_pipx` returned early when pipx was already installed, so the PIPX_BIN_DIR normalization that guarantees `vouch` is findable never ran. extracted into `ensure_pipx_bin_on_path` and called unconditionally. on ubuntu where pipx ships in apt but ~/.local/bin isn't on PATH, the smoke step would have failed without this. 3. --version pin honoured on re-runs — when the package was already installed AND --version X.Y.Z was passed, the previous code called `pipx upgrade` (which ignores the pin) instead of pinning to the requested version. now: if pinned, force-reinstall to the exact version; if unpinned, upgrade in place. local validation: shellcheck install.sh → clean dash -n install.sh → posix-clean bash -n install.sh → bash-clean ./install.sh --help → still works ./install.sh --bogus → exits 2 with clean error --- .github/workflows/install-sh.yml | 24 ++++++++++++++--- install.sh | 46 +++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/.github/workflows/install-sh.yml b/.github/workflows/install-sh.yml index f956685c..8c94f19b 100644 --- a/.github/workflows/install-sh.yml +++ b/.github/workflows/install-sh.yml @@ -9,6 +9,12 @@ name: install-sh # The smoke run intentionally exercises the published PyPI artifact, not the # in-repo source, so we catch installation breakage that doesn't show up in # the regular pytest suite (e.g. a stale [web] extra reference). +# +# Supply-chain notes: this workflow pins third-party actions to their full +# commit SHA (the comment marks the human-readable tag). The rest of the +# repo's workflows still use tag pins — a sweep of ci.yml / release.yml / +# schema-check.yml to match this pattern is a worthwhile follow-up but +# kept out of this PR to avoid churning unrelated CI. on: push: @@ -22,11 +28,21 @@ on: - ".github/workflows/install-sh.yml" workflow_dispatch: +# Least-privilege at the workflow level; the smoke job needs nothing +# beyond reading the checked-out repo. Jobs that need more bump it up +# explicitly. +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Don't leave the token sitting in .git/config after checkout — + # a leaked credentials file would otherwise allow pushing back. + persist-credentials: false - name: shellcheck run: | @@ -42,9 +58,11 @@ jobs: runs-on: ubuntu-latest needs: lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with: python-version: "3.12" diff --git a/install.sh b/install.sh index 16184bf8..dd2bed70 100755 --- a/install.sh +++ b/install.sh @@ -138,10 +138,28 @@ pick_python() { # --- phase 2: ensure pipx ------------------------------------------------- +ensure_pipx_bin_on_path() { + # PIPX_BIN_DIR (typically ~/.local/bin) holds the `vouch` shim that pipx + # writes. On many Ubuntu/Debian setups it's NOT on PATH by default even + # when pipx itself is — the shim exists, smoke_test would still fail. + # Run this unconditionally so re-runs against a pre-installed pipx + # still produce a usable shell. + py="$1" + pipx_bin=$("$py" -m pipx environment --value PIPX_BIN_DIR 2>/dev/null || true) + if [ -z "$pipx_bin" ]; then + pipx_bin="$HOME/.local/bin" + fi + case ":$PATH:" in + *":$pipx_bin:"*) ;; + *) PATH="$pipx_bin:$PATH"; export PATH ;; + esac +} + ensure_pipx() { py="$1" if has_cmd pipx; then ok "pipx already installed ($(pipx --version 2>/dev/null | head -1))" + ensure_pipx_bin_on_path "$py" return 0 fi info "pipx not found — installing into user site (no sudo)" @@ -153,17 +171,10 @@ ensure_pipx() { fi "$py" -m pipx ensurepath >/dev/null 2>&1 || true - # `ensurepath` edits ~/.profile / ~/.zshrc to add pipx's bin dir, but the - # current process's PATH is already fixed. Add it explicitly so the rest - # of this script can find `vouch`. - pipx_bin=$("$py" -m pipx environment --value PIPX_BIN_DIR 2>/dev/null || true) - if [ -z "$pipx_bin" ]; then - pipx_bin="$HOME/.local/bin" - fi - case ":$PATH:" in - *":$pipx_bin:"*) ;; - *) PATH="$pipx_bin:$PATH"; export PATH ;; - esac + # `ensurepath` edits ~/.profile / ~/.zshrc to add pipx's bin dir, but + # the current process's PATH is already fixed. Add it explicitly so + # the rest of this script can find `vouch`. + ensure_pipx_bin_on_path "$py" if ! has_cmd pipx; then err "pipx installed but not on PATH — restart your shell, then re-run" @@ -181,9 +192,20 @@ install_vouch() { target="${PKG_NAME}==${PIN_VERSION}" fi + already_installed=0 if pipx list 2>/dev/null | grep -q "package $PKG_NAME"; then + already_installed=1 + fi + + if [ "$already_installed" -eq 1 ] && [ -n "$PIN_VERSION" ]; then + # --version was requested AND the package is already installed: + # `pipx upgrade` would ignore the pin and pull latest. Force a + # clean reinstall to the exact pin instead. + info "re-installing $target (honouring --version pin)" + pipx install --force "$target" >/dev/null + elif [ "$already_installed" -eq 1 ]; then info "upgrading existing $PKG_NAME" - if ! pipx upgrade --pip-args="" "$PKG_NAME" >/dev/null 2>&1; then + if ! pipx upgrade "$PKG_NAME" >/dev/null 2>&1; then warn "pipx upgrade failed — falling back to reinstall" pipx install --force "$target" >/dev/null fi From 7c6536fda19873b01ce237cdc2c8002f11192955 Mon Sep 17 00:00:00 2001 From: plind-junior <59729252+plind-junior@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:33:57 +0900 Subject: [PATCH 3/3] ci(install-sh): resolve pipx bin dir via pipx CLI, not python -m pipx The smoke job's verification step queried PIPX_BIN_DIR with `python -m pipx environment`, but GitHub runners ship pipx as a standalone binary that is on PATH yet not importable in the setup-python interpreter, so the call died with "No module named pipx" and failed the job under set -e (install.sh itself already tolerates this). Query via the pipx CLI instead and fall back to ~/.local/bin. --- .github/workflows/install-sh.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/install-sh.yml b/.github/workflows/install-sh.yml index 8c94f19b..066ea25e 100644 --- a/.github/workflows/install-sh.yml +++ b/.github/workflows/install-sh.yml @@ -74,8 +74,13 @@ jobs: set -e sh ./install.sh --no-claude # pipx's bin dir isn't necessarily on PATH for the verification - # step — re-export from pipx itself. - PIPX_BIN=$(python -m pipx environment --value PIPX_BIN_DIR) + # step — re-export from pipx itself. Query via the pipx CLI: the + # runner ships pipx as a standalone binary that's on PATH but NOT + # importable as `python -m pipx`, so that form fails with + # "No module named pipx". Fall back to the conventional + # ~/.local/bin (pipx's default PIPX_BIN_DIR) if the query fails. + PIPX_BIN=$(pipx environment --value PIPX_BIN_DIR 2>/dev/null || true) + [ -n "$PIPX_BIN" ] || PIPX_BIN="$HOME/.local/bin" export PATH="$PIPX_BIN:$PATH" vouch --version vouch capabilities | head -20