diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6c9913d..4ed4fc0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -32,40 +32,73 @@ jobs: run: | brew install zsh fish - - name: Setup shell environment - run: | - mkdir -p ~/.config/fish - mkdir -p ~/.zsh - touch ~/.bashrc - + # Each job installs from its shell the way a user whose login shell it is + # does: `bash ./install.bash` run from that shell, with $SHELL naming it + # (the runner's login shell is bash). The home directory is the runner's + # own, not seeded with config files: macOS has no ~/.zshrc, like a fresh + # account. macOS has brew, so it takes the Homebrew path; Linux takes the + # manual one. - name: Test install script in Zsh if: matrix.shell == 'zsh' - run: | - bash ./install.bash + run: SHELL=$(command -v zsh) bash ./install.bash shell: zsh {0} - name: Test install script in Fish if: matrix.shell == 'fish' - run: | - bash ./install.bash + run: SHELL=(command -v fish) bash ./install.bash shell: fish {0} + - name: Test install script in Bash + if: matrix.shell == 'bash' + run: SHELL=$(command -v bash) bash ./install.bash + shell: bash {0} - name: Verify installation + env: + LOGIN_SHELL: ${{ matrix.shell }} run: | - if [ "${{ matrix.shell }}" = "zsh" ]; then - zsh -c "source ~/.zshrc && echo 'Zsh configuration loaded'" - elif [ "${{ matrix.shell }}" = "fish" ]; then - fish -c "source ~/.config/fish/config.fish && echo 'Fish configuration loaded'" - elif [ "${{ matrix.shell }}" = "bash" ]; then - bash -c "source ~/.bashrc && echo 'Bash configuration loaded'" - fi - [ -f ~/.shelltime/hooks/bash-preexec.sh ] || (echo "bash-preexec.sh not found in ~/.shelltime/hooks!" && exit 1) - ls ~/.shelltime - if command -v brew >/dev/null 2>&1 && brew list shelltime >/dev/null 2>&1; then - which shelltime || (echo "shelltime not in PATH via Homebrew!" && exit 1) + set -euo pipefail + hooks="$HOME/.shelltime/hooks" + case "$LOGIN_SHELL" in + zsh) rc="$HOME/.zshrc" ;; + bash) rc="$HOME/.bashrc" ;; + fish) rc="$HOME/.config/fish/config.fish" ;; + esac + ls -la "$HOME/.shelltime" "$hooks" + + # Both paths download every shell's hook, and the bash-preexec.sh bash.bash sources + for f in zsh.zsh bash.bash bash-preexec.sh fish.fish; do + [ -s "$hooks/$f" ] || { echo "$hooks/$f is missing or empty"; exit 1; } + done + [ -d "$HOME/.shelltime/daemon" ] || { echo "$HOME/.shelltime/daemon is missing"; exit 1; } + + # The login shell's config exists and sources its hook + [ -f "$rc" ] || { echo "$rc was not created"; exit 1; } + grep -qxF "source $hooks/$LOGIN_SHELL.$LOGIN_SHELL" "$rc" || { echo "$rc does not source the $LOGIN_SHELL hook:"; cat "$rc"; exit 1; } + + if [ "$RUNNER_OS" = macOS ]; then + # Homebrew: the cask links both binaries into brew's bin dir, and + # nothing is left in ~/.shelltime/bin to shadow them + brew list --cask shelltime + bin="$(brew --prefix)/bin" + [ ! -e "$HOME/.shelltime/bin/shelltime" ] || { echo "$HOME/.shelltime/bin/shelltime shadows the cask"; exit 1; } else - ls ~/.shelltime/bin + # Manual: both binaries go in ~/.shelltime/bin, which the config puts on PATH + bin="$HOME/.shelltime/bin" + grep -qF '$HOME/.shelltime/bin' "$rc" || { echo "$rc does not put ~/.shelltime/bin on PATH:"; cat "$rc"; exit 1; } fi - ls ~/.shelltime/hooks + [ -x "$bin/shelltime" ] || { echo "$bin/shelltime is missing"; exit 1; } + [ -x "$bin/shelltime-daemon" ] || { echo "$bin/shelltime-daemon is missing"; exit 1; } + "$bin/shelltime" --version + + # A new shell (one reading the config, as a new terminal does) has the + # hook loaded and runs this shelltime. The hook may print on startup, + # so the path is the last line. + case "$LOGIN_SHELL" in + zsh) found=$(zsh -i -c '(( $+functions[preexec] && $+functions[precmd] )) && command -v shelltime' | tail -n 1) ;; + bash) found=$(bash -i -c '[[ " ${preexec_functions[*]} " == *" preexec_invoke_cmd "* && " ${precmd_functions[*]} " == *" precmd_invoke_cmd "* ]] && command -v shelltime' | tail -n 1) ;; + # fish reads config.fish in every shell, interactive or not + fish) found=$(fish -c 'functions -q fish_preexec fish_postexec; and command -v shelltime' | tail -n 1) ;; + esac || true + [ "$found" = "$bin/shelltime" ] || { echo "a new $LOGIN_SHELL shell did not load the hook and run $bin/shelltime (last line: '$found')"; exit 1; } test-cloud: runs-on: ${{ matrix.os }} diff --git a/CLAUDE.md b/CLAUDE.md index 1e42e9e..00796ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,10 +24,11 @@ Run the CI workflow locally or on GitHub Actions: bash ./install.bash ``` -The CI verifies: -1. Installation script runs successfully across shells -2. Shell configuration files are properly sourced -3. Hook files are placed in `~/.shelltime/hooks/` +Each `test-shells` job runs `bash ./install.bash` from its shell (zsh, fish or bash) with `$SHELL` naming it, as for a user whose login shell it is, in the runner's own home directory (macOS has no `~/.zshrc`, like a fresh account). macOS takes the Homebrew path, Linux the manual one. The CI verifies: +1. Every hook and `bash-preexec.sh` are in `~/.shelltime/hooks/`, and `~/.shelltime/daemon/` exists +2. The login shell's config file exists and sources its hook (`install.bash` creates it for the login shell, and `config.fish` whenever fish is installed) +3. Homebrew path: the cask is installed and links `shelltime` and `shelltime-daemon` into brew's bin, with no `~/.shelltime/bin/shelltime` shadowing it. Manual path: both binaries are in `~/.shelltime/bin`, which the config puts on `PATH` +4. A new interactive shell loads the hook's functions and runs that `shelltime` (bash needs `-i`: Ubuntu's default `~/.bashrc` returns early in a non-interactive shell) The `test-cloud` job checks that `cloud.bash` installs nothing without `CLAUDE_CODE_REMOTE=true`, then runs it twice with it on x86_64 and arm64 Linux and checks the binaries, the OTEL settings, that the SessionStart hook is registered exactly once, and that the hook prints nothing, signs in with `proxy-injected` (or `SHELLTIME_TOKEN` when set) and starts one daemon. @@ -44,7 +45,7 @@ Bash requires the external `bash-preexec.sh` library (downloaded during installa ## Installation Paths -- CLI binary: `~/.shelltime/bin/shelltime` +- CLI binary: `~/.shelltime/bin/shelltime` (manual install), or brew's bin dir when macOS installs the `shelltime/tap/shelltime` cask - Hook scripts: `~/.shelltime/hooks/` - Daemon directory: `~/.shelltime/daemon/` diff --git a/install.bash b/install.bash index a02e4fd..616087a 100644 --- a/install.bash +++ b/install.bash @@ -61,6 +61,27 @@ if [[ "$OS" == "Darwin" ]] && command_exists brew; then fi fi +# The shell config files the PATH entry and the hooks go into, created for both +# install paths when they don't exist yet. The login shell ($SHELL, also under +# `curl | bash`) always gets one: a fresh macOS account runs zsh with no +# ~/.zshrc, and without it the shell would load no hook and the reload step +# printed at the end would fail. config.fish is created whenever fish is +# installed. +ensure_config_file() { + local file="$1" + if [ ! -f "$file" ]; then + mkdir -p "$(dirname "$file")" && touch "$file" + fi +} + +case "$(basename "${SHELL:-}")" in + zsh) ensure_config_file "$HOME/.zshrc" ;; + bash) ensure_config_file "$HOME/.bashrc" ;; +esac +if command_exists fish; then + ensure_config_file "$HOME/.config/fish/config.fish" +fi + if [ "$BREW_INSTALLED" = false ]; then CLI_FILE_NAME="https://github.com/shelltime/cli/releases/latest/download/cli_" @@ -189,13 +210,7 @@ if [[ "$OS" == "Darwin" ]] || [[ "$OS" == "Linux" ]]; then fi # For Fish - if command_exists fish; then - if [ ! -d "$HOME/.config/fish" ]; then - mkdir -p "$HOME/.config/fish" - fi - if [ ! -f "$HOME/.config/fish/config.fish" ]; then - touch "$HOME/.config/fish/config.fish" - fi + if [ -f "$HOME/.config/fish/config.fish" ]; then if ! grep -q '$HOME/.shelltime/bin' "$HOME/.config/fish/config.fish"; then echo '# Added by shelltime' >> "$HOME/.config/fish/config.fish" echo 'fish_add_path $HOME/.shelltime/bin' >> "$HOME/.config/fish/config.fish"