diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b940ecbcb5..3b879bc548 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,6 +44,9 @@ jobs: - name: ๐Ÿ” Pre-commit hooks uses: pre-commit/action@v3.0.1 + - name: ๐Ÿงช install.sh unit tests + run: sh dev/install/install_test.sh + feature-check: # Guards the library/CLI cleave: the `cli` feature gates clap, skim, # crossterm, termimad, env_logger, humantime. If anything reachable from diff --git a/README.md b/README.md index 4041ac70e6..2dc845be35 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ†’ Privacy & security sudo pacman -S worktrunk && wt config shell install ``` +
+Script installer (experimental) + +Downloads a static binary โ€” no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: diff --git a/dev/install/install_test.sh b/dev/install/install_test.sh new file mode 100755 index 0000000000..7162792f82 --- /dev/null +++ b/dev/install/install_test.sh @@ -0,0 +1,165 @@ +#!/bin/sh +set -eu + +# Unit tests for docs/static/install.sh. +# +# These are fast behavioral tests that run install.sh with a mocked curl on +# PATH and verify exit codes + output. They cover the error/edge paths only. +# The happy path (curl | sh yielding a working `wt`) is covered by the +# container hand-test: dev/install/test-containers.sh + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh" + +PASS=0 +FAIL=0 + +pass() { + PASS=$((PASS + 1)) + echo " PASS: $1" +} + +fail() { + FAIL=$((FAIL + 1)) + echo " FAIL: $1" + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" + fi +} + +MOCK_DIR="$(mktemp -d)" +CARGO_DIR="$(mktemp -d)" +trap 'rm -rf "$MOCK_DIR" "$CARGO_DIR"' EXIT + +# Write an executable shell script to $MOCK_DIR/$1 with the given body. +mock_bin() { + name="$1" + body="$2" + path="$MOCK_DIR/$name" + printf '#!/bin/sh\n%s\n' "$body" > "$path" + chmod +x "$path" +} + +# Mock curl to find the `-o ` arg and write a minimal installer there +# that exits with the given code. Simulates a successful download of an +# installer whose *execution* then succeeds ($1=0) or fails ($1!=0). +mock_curl_writes_installer() { + mock_bin curl "while [ \$# -gt 0 ]; do + if [ \"\$1\" = \"-o\" ]; then + shift + printf '%s\n' '#!/bin/sh' 'exit $1' > \"\$1\" + exit 0 + fi + shift +done +exit 1" +} + +# Run install.sh with $MOCK_DIR first on PATH and a curated environment. +# Captures exit code in $rc and combined output in $output. Pass a value for +# $OS as the first argument (default empty). +run_install() { + os_val="${1:-}" + set +e + output="$(PATH="$MOCK_DIR:/usr/bin:/bin" \ + OS="$os_val" \ + HOME="$CARGO_DIR/home" \ + CARGO_HOME="$CARGO_DIR/cargo" \ + sh "$INSTALL_SH" 2>&1)" + rc=$? + set -e +} + +echo "=== install.sh test suite ===" +echo "" + +# --------------------------------------------------------------------------- +echo "--- Platform gate ---" + +# Windows detection: sets OS=Windows_NT, expects exit 1 with guidance. +run_install Windows_NT +if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected" \ + && echo "$output" | grep -q "install.ps1"; then + pass "exits with PowerShell guidance when OS=Windows_NT" +else + fail "exits with PowerShell guidance when OS=Windows_NT" "rc=$rc output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "--- Download / installer failures ---" + +# curl fails to download: expect non-zero exit. +mock_bin curl 'exit 22' +run_install +if [ "$rc" -ne 0 ]; then + pass "exits non-zero when curl fails" +else + fail "exits non-zero when curl fails" "rc=$rc output: $output" +fi + +# curl succeeds but writes a failing installer: expect non-zero exit. +mock_curl_writes_installer 5 +run_install +if [ "$rc" -ne 0 ]; then + pass "exits non-zero when upstream installer fails" +else + fail "exits non-zero when upstream installer fails" "rc=$rc output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "--- Post-install path resolution ---" + +# curl writes a no-op installer; CARGO_HOME is empty and wt is absent from +# PATH โ†’ script should warn and exit 0 (installer succeeded but wt missing). +mock_curl_writes_installer 0 +mkdir -p "$CARGO_DIR/cargo" +run_install +if [ "$rc" -eq 0 ] && echo "$output" | grep -q "'wt' not found in PATH"; then + pass "warns and exits 0 when wt is missing after install" +else + fail "warns and exits 0 when wt is missing after install" "rc=$rc output: $output" +fi + +# As above, but wt exists in CARGO_HOME/bin with an env file. The script +# should source env, find wt, and reach the post-wt-found stage. Whether the +# final `wt config shell install` actually runs depends on /dev/tty being +# openable โ€” that's a property of the test environment, so accept either the +# sentinel (TTY) or the non-interactive fallback message (no TTY). +sentinel="$CARGO_DIR/wt.args" +mkdir -p "$CARGO_DIR/cargo/bin" +cat > "$CARGO_DIR/cargo/bin/wt" < "$sentinel" +WT +chmod +x "$CARGO_DIR/cargo/bin/wt" +cat > "$CARGO_DIR/cargo/env" </dev/null) output: $output" +fi + +echo "" + +# --------------------------------------------------------------------------- +echo "=== Results ===" +echo " $PASS passed, $FAIL failed" +echo "" + +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/dev/install/test-containers.sh b/dev/install/test-containers.sh new file mode 100755 index 0000000000..bcb513f18e --- /dev/null +++ b/dev/install/test-containers.sh @@ -0,0 +1,101 @@ +#!/bin/sh +set -eu + +# Hand-test: run docs/static/install.sh inside clean Docker containers and +# verify it produces a working `wt`. Not wired into CI because it hits the +# real network (GitHub releases) and needs Docker. +# +# Usage: +# sh dev/install/test-containers.sh # test all images +# sh dev/install/test-containers.sh ubuntu # test one image +# +# The script under test is the one checked into this repo (not fetched from +# worktrunk.dev) โ€” we're testing the current source, not what's published. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh" + +if ! command -v docker >/dev/null 2>&1; then + echo "docker is required for the container test. Install Docker Desktop or" + echo "equivalent, then re-run." + exit 1 +fi + +# Each entry: image | setup. The upstream cargo-dist installer downloads a +# tar.xz release and extracts it, so `xz` must be on PATH alongside curl. +IMAGES=" +ubuntu:24.04|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils +debian:12|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils +fedora:41|dnf install -y -q curl xz +alpine:3.20|apk add --no-cache curl ca-certificates xz +archlinux:latest|pacman -Sy --noconfirm curl ca-certificates xz +" + +FILTER="${1:-}" +PASS=0 +FAIL=0 +FAILED="" + +run_one() { + image="$1" + setup="$2" + + echo "" + echo "=== $image ===" + + # Copy install.sh into the container, run setup + install, then verify + # `wt --version` prints a worktrunk version. Use `sh -c` as entrypoint so + # the script runs regardless of the image's default command. We capture + # into a temp file rather than piping โ€” a pipe would mask docker's exit + # code behind sed's. + log="$(mktemp)" + set +e + docker run --rm \ + -v "$INSTALL_SH:/tmp/install.sh:ro" \ + "$image" \ + sh -c "set -e; $setup >/dev/null; sh /tmp/install.sh; . \${CARGO_HOME:-\$HOME/.cargo}/env; wt --version" \ + >"$log" 2>&1 + rc=$? + set -e + sed 's/^/ /' "$log" + rm -f "$log" + + if [ "$rc" -eq 0 ]; then + echo " PASS: $image" + PASS=$((PASS + 1)) + else + echo " FAIL: $image (exit $rc)" + FAIL=$((FAIL + 1)) + FAILED="$FAILED $image" + fi +} + +echo "Testing install.sh in containers..." + +# Shell-splitting on newlines in POSIX sh: set IFS to newline, iterate. +old_ifs="$IFS" +IFS=' +' +for entry in $IMAGES; do + IFS='|' + # shellcheck disable=SC2086 + set -- $entry + IFS="$old_ifs" + image="$1" + setup="$2" + if [ -n "$FILTER" ] && ! echo "$image" | grep -q "$FILTER"; then + continue + fi + run_one "$image" "$setup" + IFS=' +' +done +IFS="$old_ifs" + +echo "" +echo "=== Results ===" +echo " $PASS passed, $FAIL failed" +if [ "$FAIL" -gt 0 ]; then + echo " Failed:$FAILED" + exit 1 +fi diff --git a/docs/content/worktrunk.md b/docs/content/worktrunk.md index 81f3dcd209..6c4a69bf69 100644 --- a/docs/content/worktrunk.md +++ b/docs/content/worktrunk.md @@ -131,6 +131,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ†’ Privacy & security {{ terminal(cmd="sudo pacman -S worktrunk && wt config shell install") }} +
+Script installer (experimental) + +Downloads a static binary โ€” no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: diff --git a/docs/static/install.ps1 b/docs/static/install.ps1 new file mode 100644 index 0000000000..19acae2237 --- /dev/null +++ b/docs/static/install.ps1 @@ -0,0 +1,42 @@ +# Worktrunk Installer (Windows) +# https://worktrunk.dev/install.ps1 + +$ErrorActionPreference = 'Stop' + +if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) { + Write-Host "Non-Windows environment detected. Please use the shell installer instead:" -ForegroundColor Yellow + Write-Host " curl -fsSL https://worktrunk.dev/install.sh | sh" + exit 1 +} + +Write-Host "Installing worktrunk..." +irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.ps1 | iex + +# Update PATH to pick up the newly installed binary. +# Respect CARGO_HOME if set, otherwise use the default location. +$cargoBin = if ($env:CARGO_HOME) { "$env:CARGO_HOME\bin" } else { "$HOME\.cargo\bin" } +if ($env:Path -notlike "*$cargoBin*") { + $env:Path += ";$cargoBin" +} + +# Check whether `wt` on PATH is actually worktrunk (Windows Terminal uses +# the same alias). Wrap in try/catch โ€” with ErrorActionPreference='Stop', +# a failing `wt --version` would throw instead of falling through. +$wtIsWorktrunk = $false +if (Get-Command wt -ErrorAction SilentlyContinue) { + try { + $wtIsWorktrunk = [bool](wt --version 2>&1 | Select-String 'worktrunk') + } catch { + $wtIsWorktrunk = $false + } +} + +if ($wtIsWorktrunk) { + wt config shell install +} elseif (Get-Command git-wt -ErrorAction SilentlyContinue) { + git-wt config shell install +} else { + Write-Host "" + Write-Host "Warning: worktrunk installed but neither 'wt' nor 'git-wt' found in PATH." -ForegroundColor Yellow + Write-Host "Restart your shell and run 'wt config shell install' (or 'git-wt config shell install') manually." +} diff --git a/docs/static/install.sh b/docs/static/install.sh new file mode 100644 index 0000000000..01b8d210c2 --- /dev/null +++ b/docs/static/install.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +# Worktrunk Installer (Unix) +# https://worktrunk.dev/install.sh + +if [ "${OS:-}" = "Windows_NT" ]; then + echo "Windows detected. Please use the PowerShell installer instead:" + echo " powershell -c \"irm https://worktrunk.dev/install.ps1 | iex\"" + exit 1 +fi + +echo "Installing worktrunk..." + +# Download to a temp file instead of piping curl to sh. This avoids two issues: +# 1. Pipe swallows curl failures (pipefail is not POSIX) +# 2. Piping consumes stdin, blocking interactive prompts in the installer +installer="$(mktemp)" +trap 'rm -f "$installer"' EXIT +curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh -o "$installer" +sh "$installer" + +# Source the cargo env to pick up PATH changes from the installer. +# This handles custom CARGO_HOME and avoids hardcoding ~/.cargo/bin. +# POSIX sh exits the whole script when `.` can't read the file, even with +# `|| true`, so guard with an explicit existence check. +cargo_env="${CARGO_HOME:-$HOME/.cargo}/env" +if [ -r "$cargo_env" ]; then + # shellcheck disable=SC1090 + . "$cargo_env" +fi + +if ! command -v wt >/dev/null 2>&1; then + echo "" + echo "Warning: worktrunk installed but 'wt' not found in PATH." + echo "Restart your shell and run 'wt config shell install' manually." + exit 0 +fi + +# Configure shell integration. We use < /dev/tty so the interactive prompt +# works even when this script was piped into sh (e.g. curl ... | sh). On +# non-interactive contexts /dev/tty exists but isn't openable; probe before +# redirecting. Use `true` (a regular builtin) rather than `:` โ€” POSIX exits +# the shell on a redirect failure against a special builtin. +if { true < /dev/tty; } 2>/dev/null; then + wt config shell install < /dev/tty +else + echo "" + echo "Non-interactive environment detected." + echo "Run 'wt config shell install' after restarting your shell." +fi diff --git a/skills/worktrunk/reference/worktrunk.md b/skills/worktrunk/reference/worktrunk.md index b0d4d0077d..1f40c16715 100644 --- a/skills/worktrunk/reference/worktrunk.md +++ b/skills/worktrunk/reference/worktrunk.md @@ -117,6 +117,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ†’ Privacy & security sudo pacman -S worktrunk && wt config shell install ``` +
+Script installer (experimental) + +Downloads a static binary โ€” no package manager required. + +**macOS & Linux:** + +```bash +curl -fsSL https://worktrunk.dev/install.sh | sh +``` + +**Windows:** + +```bash +powershell -c "irm https://worktrunk.dev/install.ps1 | iex" +``` + +
+ ## Quick start Create a worktree for a new feature: