Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 86 additions & 0 deletions .github/workflows/install-sh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: install-sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unpinned actions and missing workflow hardening in this file.
rg -n '^\s*uses:\s*[^@]+@v[0-9]+' .github/workflows/install-sh.yml
rg -n '^\s*permissions:' .github/workflows/install-sh.yml
rg -n 'actions/checkout@' .github/workflows/install-sh.yml -A3 -B1

Repository: vouchdev/vouch

Length of output: 275


Harden workflow supply-chain controls: pin actions to commit SHAs, restrict token scope, and disable credential persistence.

The workflow uses mutable action versions (v4, v5) and leaves default token permissions unset. Additionally, actions/checkout@v4 does not explicitly disable credential persistence, increasing exposure if the workflow is compromised.

Suggested hardening
 name: install-sh
+permissions:
+  contents: read
@@
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<pinned-commit-sha>
+        with:
+          persist-credentials: false
@@
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<pinned-commit-sha>
+        with:
+          persist-credentials: false
 
-      - uses: actions/setup-python@v5
+      - uses: actions/setup-python@<pinned-commit-sha>

Applies to: lines 1, 29–29, 45–47

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 1-64: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/install-sh.yml at line 1, Harden the GitHub Actions
workflow supply chain security by making three changes: First, pin all action
references from mutable versions (such as v4, v5) to specific commit SHAs
instead of version tags throughout the workflow. Second, add an explicit
permissions block at the workflow level to restrict the default GITHUB_TOKEN
scope to only the minimum required permissions. Third, locate the
actions/checkout action and add the persist-credentials: false parameter to
disable credential persistence and reduce exposure if the workflow is
compromised.

Source: Linters/SAST tools


# 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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
286 changes: 286 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -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 <X.Y.Z> 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 <<EOF
${C_BOLD}vouch installer${C_RESET}

curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh

Usage:
install.sh [--version X.Y.Z] [--no-claude] [--quiet] [--help]

What it does:
1. picks a python >=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
Comment on lines +160 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "install\.sh$"

Repository: vouchdev/vouch

Length of output: 69


🏁 Script executed:

wc -l install.sh

Repository: vouchdev/vouch

Length of output: 73


🏁 Script executed:

sed -n '130,220p' install.sh | cat -n

Repository: vouchdev/vouch

Length of output: 3573


🏁 Script executed:

sed -n '155,210p' install.sh | cat -n

Repository: vouchdev/vouch

Length of output: 2156


🏁 Script executed:

sed -n '1,50p' install.sh | cat -n

Repository: vouchdev/vouch

Length of output: 2020


🏁 Script executed:

sed -n '195,210p' install.sh | cat -n

Repository: vouchdev/vouch

Length of output: 582


🏁 Script executed:

# Check the main script flow to see how these functions are called
sed -n '230,264p' install.sh | cat -n

Repository: vouchdev/vouch

Length of output: 1367


Move PATH normalization outside the pre-existence check to ensure pipx's bin dir is always accessible.

When pipx already exists on the system, the function returns early before normalizing PIPX_BIN_DIR in the current process. This leaves vouch unreachable when smoke_test() runs, causing the installation to fail even though vouch was successfully installed.

Proposed fix
 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
+    if has_cmd pipx; then
+        ok "pipx already installed ($(pipx --version 2>/dev/null | head -1))"
+    else
+        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
     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)
+    # Always normalize current-process PATH to include pipx app binaries.
+    pipx_bin=$(pipx environment --value PIPX_BIN_DIR 2>/dev/null || "$py" -m pipx environment --value PIPX_BIN_DIR 2>/dev/null || true)
     if [ -z "$pipx_bin" ]; then
         pipx_bin="$HOME/.local/bin"
     fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 143 - 146, The early return statement in the has_cmd
pipx check prevents the PATH normalization for PIPX_BIN_DIR from executing when
pipx is already installed, making vouch unreachable during smoke_test. Move the
code that normalizes PIPX_BIN_DIR in the current process (typically involving
PATH updates) to execute before the early return in the has_cmd pipx conditional
block, ensuring the PATH is always updated to include pipx's bin directory
regardless of whether pipx pre-exists on the system.

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
Comment on lines +191 to +211

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and inspect install.sh
find . -name "install.sh" -type f

Repository: vouchdev/vouch

Length of output: 71


🏁 Script executed:

# Read the install.sh file with context around lines 180-189
head -c 20000 install.sh | tail -c 15000 | cat -n

Repository: vouchdev/vouch

Length of output: 9894


🏁 Script executed:

# Also search for PIN_VERSION and --version handling
rg "PIN_VERSION|--version" install.sh -B 2 -A 2

Repository: vouchdev/vouch

Length of output: 1590


--version pin is ignored when vouch-kb is already installed.

When re-running the installer with --version X.Y.Z on an already-installed package, the script upgrades to the latest version instead of respecting the pin. The target variable (containing vouch-kb==X.Y.Z) is only used if the upgrade fails, breaking the documented --version behavior.

Fix: Check for PIN_VERSION first. If set, always perform a pinned install; otherwise, upgrade existing or install fresh.

Proposed fix
 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
+    if [ -n "$PIN_VERSION" ]; then
+        info "installing pinned $target"
+        pipx install --force "$target" >/dev/null
+    elif 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
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
if [ -n "$PIN_VERSION" ]; then
target="${PKG_NAME}==${PIN_VERSION}"
fi
if [ -n "$PIN_VERSION" ]; then
info "installing pinned $target"
pipx install --force "$target" >/dev/null
elif 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
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@install.sh` around lines 180 - 189, The script currently constructs the
`target` variable with the pinned version but then ignores it when upgrading an
existing package, causing the `--version` flag to be disregarded. Restructure
the conditional logic to check `PIN_VERSION` first: if PIN_VERSION is set,
always perform a pinned install with the `target` variable using pipx install
--force, bypassing the upgrade path entirely. Otherwise, if the package already
exists (when PIN_VERSION is not set), proceed with the upgrade attempt; if
upgrade fails or the package doesn't exist, fall back to a fresh install without
a version pin.

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
Loading