diff --git a/.github/workflows/install-sh.yml b/.github/workflows/install-sh.yml new file mode 100644 index 0000000..066ea25 --- /dev/null +++ b/.github/workflows/install-sh.yml @@ -0,0 +1,86 @@ +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). +# +# 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: + branches: [main, release/*] + paths: + - "install.sh" + - ".github/workflows/install-sh.yml" + pull_request: + paths: + - "install.sh" + - ".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@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: | + 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + 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. 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 diff --git a/README.md b/README.md index 16f7f60..704ded1 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,25 @@ Skip it if: ## Install ```bash +<<<<<<< feat/install-script +# 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 +======= # from the cloned repo, in a venv +>>>>>>> test 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 0000000..dd2bed7 --- /dev/null +++ b/install.sh @@ -0,0 +1,286 @@ +#!/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_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)" + 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`. + ensure_pipx_bin_on_path "$py" + + 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 + + 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 "$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