Skip to content

Commit 7fff4ea

Browse files
committed
Make the installer zero-friction
- No arguments needed: setup.sh detects the shell from $SHELL, falling back to installed shells with existing rc files - git is now optional: falls back to a curl/wget tarball download, staged so a failed fetch can't leave a half-install; re-running with git available upgrades the install to a checkout - Missing rc files are created instead of skipped with "re-run" - doctor/update handle git-less installs gracefully - New hermetic test suite for setup.sh, run in CI on ubuntu and macOS system bash 3.2
1 parent 833d3b7 commit 7fff4ea

9 files changed

Lines changed: 447 additions & 51 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
bash -n _constants.sh
2727
bash -n tests/test_activator.sh
2828
bash -n tests/test_subcommands.sh
29+
bash -n tests/test_setup.sh
2930
bash -n bench/bench.sh
3031
3132
- name: Syntax check (zsh)
@@ -45,6 +46,7 @@ jobs:
4546
shellcheck --shell=bash --severity=warning _constants.sh
4647
shellcheck --shell=bash --severity=warning tests/test_activator.sh
4748
shellcheck --shell=bash --severity=warning tests/test_subcommands.sh
49+
shellcheck --shell=bash --severity=warning tests/test_setup.sh
4850
4951
- name: Activator tests (bash)
5052
run: bash tests/test_activator.sh
@@ -58,6 +60,11 @@ jobs:
5860
- name: Subcommand tests (zsh)
5961
run: zsh tests/test_subcommands.sh
6062

63+
# setup.sh is bash-only (piped installs run it under bash), so this
64+
# suite has no zsh variant.
65+
- name: Setup tests (bash)
66+
run: bash tests/test_setup.sh
67+
6168
test-macos:
6269
runs-on: macos-latest
6370
steps:
@@ -76,3 +83,8 @@ jobs:
7683

7784
- name: Subcommand tests (zsh)
7885
run: zsh tests/test_subcommands.sh
86+
87+
# The system bash 3.2 is exactly what a piped install runs under on
88+
# macOS, so this is the highest-value environment for the installer.
89+
- name: Setup tests (system bash 3.2)
90+
run: /bin/bash tests/test_setup.sh

CONTRIBUTING.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,27 @@ Shell scripts in this repo — including the tests and the benchmark — are lin
2727
shellcheck --severity=warning setup.sh bench/bench.sh
2828
shellcheck --shell=bash --severity=warning activator.sh autoactivator_config.sh _constants.sh tests/*.sh
2929
bash -n setup.sh activator.sh autoactivator_config.sh _constants.sh tests/*.sh bench/bench.sh
30-
zsh -n activator.sh autoactivator_config.sh _constants.sh tests/*.sh bench/bench.sh
30+
zsh -n activator.sh autoactivator_config.sh _constants.sh tests/test_activator.sh tests/test_subcommands.sh bench/bench.sh
3131
```
3232

33+
(`setup.sh` and `tests/test_setup.sh` are bash-only — piped installs always run under bash — so they are excluded from the `zsh -n` check.)
34+
3335
`activator.sh` is sourced by both `bash` and `zsh`, so any zsh-specific syntax must be guarded by `[[ -n "$ZSH_VERSION" ]]` and any bash-specific syntax by `[[ -n "$BASH_VERSION" ]]`.
3436

3537
## Tests
3638

37-
The repo has two black-box test suites, neither of which needs Python or an external test framework:
39+
The repo has three black-box test suites, none of which needs Python or an external test framework:
3840

3941
- [`tests/test_activator.sh`](tests/test_activator.sh) builds a fake project tree in a temp directory, sources `activator.sh`, and asserts behavior by invoking `_check_for_venv` directly.
4042
- [`tests/test_subcommands.sh`](tests/test_subcommands.sh) sources the full config under a throwaway `$HOME` and exercises the `autoactivator` subcommands.
43+
- [`tests/test_setup.sh`](tests/test_setup.sh) runs the installer end-to-end against a local fixture repo and tarball under throwaway `$HOME`s — no network. It is bash-only, like the installer itself.
4144

42-
Run both under both shells before opening a PR:
45+
Run them before opening a PR (the first two under both shells):
4346

4447
```bash
4548
bash tests/test_activator.sh && zsh tests/test_activator.sh
4649
bash tests/test_subcommands.sh && zsh tests/test_subcommands.sh
50+
bash tests/test_setup.sh
4751
```
4852

4953
CI runs all of these on every push and pull request, on Linux and on macOS (where the system bash 3.2 exercises the cacheless code path).

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ If you activate a venv by hand, AutoActivator leaves it alone — no automatic s
3232
## Requirements
3333

3434
- **bash** or **zsh**
35-
- **git**used for installation and updates
35+
- **git**recommended for installation and updates; without it, the installer falls back to a tarball download (needs **curl** or **wget**, plus **tar**)
3636
- Linux or macOS (both covered by CI, including macOS's stock bash 3.2)
3737

3838
No Python required.
3939

4040
## Installation
4141

4242
```bash
43-
curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash -s <shell>
43+
curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash
4444
```
4545

46-
Replace `<shell>` with `bash`, `zsh`, or both:
46+
No arguments needed — the installer detects your shell from `$SHELL` and wires up the matching rc file (creating it if it doesn't exist). Want a specific shell (or both)? Pass it explicitly:
4747

4848
```bash
4949
# zsh only
@@ -62,7 +62,7 @@ curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash -s zsh bash
6262
```bash
6363
git clone https://github.com/aymenkrifa/autoactivator.git ~/.autoactivator
6464
chmod +x ~/.autoactivator/setup.sh
65-
~/.autoactivator/setup.sh <shell>
65+
~/.autoactivator/setup.sh # optionally pass a shell: ~/.autoactivator/setup.sh zsh
6666
```
6767

6868
</details>
@@ -82,6 +82,8 @@ autoactivator update
8282

8383
This pulls the latest changes and re-sources the hook in your current shell. The update will refuse to run if you have local modifications in `~/.autoactivator`.
8484

85+
Installed without git? `autoactivator update` will point you back to the install one-liner — re-running it refreshes the install in place (and upgrades it to a git checkout once git is available).
86+
8587
## Tool support
8688

8789
AutoActivator finds venvs that live **inside the project tree**. Here's how that maps to the common Python tools:

RELEASING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Releasing
22

33
AutoActivator ships as source — installs happen via `setup.sh` (`git clone` /
4-
`git pull`), so a release is a **git tag + a GitHub Release**, with no build
5-
artifacts. The `autoactivator version` command derives its output from
4+
`git pull`, with a tarball fallback when git is absent), so a release is a
5+
**git tag + a GitHub Release**, with no build artifacts. The `autoactivator version` command derives its output from
66
`git describe --tags`, so nothing in the code hardcodes the version.
77

88
## Versioning

autoactivator_config.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ _autoactivator_cmd_doctor() {
224224
else
225225
_autoactivator_check warn "local modifications in $_AUTOACTIVATOR_DIR\`update\` will refuse to run"
226226
fi
227+
elif [ -f "$_AUTOACTIVATOR_DIR/autoactivator_config.sh" ]; then
228+
_autoactivator_check ok "install present at $_AUTOACTIVATOR_DIR (tarball install — update by re-running the installer)"
227229
else
228230
_autoactivator_check fail "repo NOT found at $_AUTOACTIVATOR_DIR"
229231
fi
@@ -321,7 +323,9 @@ _autoactivator_cmd_uninstall() {
321323

322324
_autoactivator_cmd_update() {
323325
if [ ! -d "$_AUTOACTIVATOR_DIR/.git" ]; then
324-
_autoactivator_msg 2 "not a git checkout at $_AUTOACTIVATOR_DIR — cannot update."
326+
_autoactivator_msg 2 "not a git checkout at $_AUTOACTIVATOR_DIR (installed without git)."
327+
_autoactivator_msg 2 "update by re-running the installer:"
328+
_autoactivator_msg 2 " curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash"
325329
return 1
326330
fi
327331

docs/index.html

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,13 @@ <h1><span class="a">Auto</span><span class="b">Activator</span></h1>
188188
</div>
189189

190190
<div class="tabs" role="tablist" aria-label="Choose your shell">
191-
<button data-shell="zsh" class="on" aria-pressed="true">zsh</button>
191+
<button data-shell="" class="on" aria-pressed="true">auto</button>
192+
<button data-shell="zsh" aria-pressed="false">zsh</button>
192193
<button data-shell="bash" aria-pressed="false">bash</button>
193194
<button data-shell="zsh bash" aria-pressed="false">both</button>
194195
</div>
195196
<div class="install">
196-
<code id="cmdline">curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash -s <b id="shellarg">zsh</b></code>
197+
<code id="cmdline">curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash<b id="shellarg"></b></code>
197198
<button id="copy" aria-label="Copy install command">copy</button>
198199
</div>
199200
</section>
@@ -224,7 +225,11 @@ <h3 class="sub">And it's fast — a cd hook has no business being slow</h3>
224225

225226
<section id="install">
226227
<h2>Install</h2>
227-
<p class="lede">You need <b>bash</b> or <b>zsh</b>, <b>git</b>, and Linux or macOS — both covered by CI, including macOS's stock bash 3.2. No Python required.</p>
228+
<p class="lede">You need <b>bash</b> or <b>zsh</b> and Linux or macOS — both covered by CI, including macOS's stock bash 3.2. <b>git</b> is recommended; without it the installer falls back to a tarball via <b>curl</b>/<b>wget</b> + <b>tar</b>. No Python required.</p>
229+
230+
<pre data-label="auto-detect (recommended)">curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash</pre>
231+
232+
<p class="lede fine">No arguments needed — the installer detects your shell from <code>$SHELL</code> and creates the rc file if it's missing. Want a specific shell (or both)?</p>
228233

229234
<pre class="grouped" data-label="zsh">curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash -s <span class="y">zsh</span></pre>
230235

@@ -236,7 +241,7 @@ <h2>Install</h2>
236241

237242
<h3 class="sub">Update &amp; uninstall</h3>
238243
<pre class="grouped" data-label="update">autoactivator <span class="s">update</span></pre>
239-
<p class="lede fine">Pulls the latest version and re-sources the hook in your current shell.</p>
244+
<p class="lede fine">Pulls the latest version and re-sources the hook in your current shell. Installed without git? Re-run the install one-liner instead — it refreshes the install in place (and upgrades it to a git checkout once git is available).</p>
240245

241246
<pre data-label="uninstall">autoactivator <span class="s">uninstall</span></pre>
242247
<p class="lede fine">Removes the hook from your rc files, backing them up first — add <code>--purge</code> to also delete <code>~/.autoactivator</code>.</p>
@@ -338,7 +343,7 @@ <h2>Security note</h2>
338343
t.addEventListener('click', function(){
339344
tabs.forEach(function(o){ o.classList.remove('on'); o.setAttribute('aria-pressed','false'); });
340345
t.classList.add('on'); t.setAttribute('aria-pressed','true');
341-
shellarg.textContent = t.dataset.shell;
346+
shellarg.textContent = t.dataset.shell ? ' -s ' + t.dataset.shell : '';
342347
});
343348
});
344349

docs/setup.sh

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ set -e
44
APP_NAME="AutoActivator"
55
TARGET_DIR="$HOME/.autoactivator"
66
REPO_URL="${AUTOACTIVATOR_REPO_URL:-https://github.com/aymenkrifa/autoactivator}"
7+
TARBALL_URL="${AUTOACTIVATOR_TARBALL_URL:-${REPO_URL%.git}/archive/refs/heads/main.tar.gz}"
8+
ONELINER="curl -sSL https://autoactivator.aymenkrifa.com/setup.sh | bash"
79

810
die() { echo "Error: $*" >&2; exit 1; }
11+
have() { command -v "$1" >/dev/null 2>&1; }
912

1013
rc_for_shell() {
1114
case "$1" in
@@ -14,51 +17,102 @@ rc_for_shell() {
1417
esac
1518
}
1619

17-
[ $# -gt 0 ] || die "Usage: $(basename "$0") <shell> [<shell>...] (shells: bash, zsh)"
18-
19-
for s in "$@"; do
20-
case "$s" in
21-
bash|zsh) ;;
22-
*) die "Unsupported shell '$s'. Must be 'bash' or 'zsh'." ;;
23-
esac
24-
done
20+
# Fetch and unpack a source tarball into $TARGET_DIR when git is missing.
21+
# Staged in a temp dir beside the target so a failed download or extract
22+
# can never leave a half-install at $TARGET_DIR.
23+
tarball_install() {
24+
if ! { have curl || have wget; } || ! have tar; then
25+
die "git is not installed, and the tarball fallback needs curl or wget, plus tar. Install git (recommended), or curl/wget and tar, then re-run."
26+
fi
27+
echo "git not found — downloading $APP_NAME as a tarball instead ..."
28+
TMP_STAGE=$(mktemp -d "$HOME/.autoactivator.new.XXXXXX")
29+
trap 'rm -rf "$TMP_STAGE"' EXIT
30+
if have curl; then
31+
curl -fsSL "$TARBALL_URL" -o "$TMP_STAGE/src.tar.gz"
32+
else
33+
wget -qO "$TMP_STAGE/src.tar.gz" "$TARBALL_URL"
34+
fi
35+
mkdir "$TMP_STAGE/src"
36+
tar -xzf "$TMP_STAGE/src.tar.gz" -C "$TMP_STAGE/src" --strip-components=1
37+
rm -rf "$TARGET_DIR"
38+
mv "$TMP_STAGE/src" "$TARGET_DIR"
39+
}
2540

2641
case "$(uname -s)" in
2742
Linux|Darwin) ;;
2843
*) die "Unsupported OS: $(uname -s). Only Linux and macOS are supported." ;;
2944
esac
3045

31-
command -v git >/dev/null 2>&1 || die "git is not installed. https://git-scm.com/"
46+
shells=()
47+
if [ $# -gt 0 ]; then
48+
for s in "$@"; do
49+
case "$s" in
50+
bash|zsh) shells+=("$s") ;;
51+
*) die "Unsupported shell '$s'. Usage: $(basename "$0") [<shell>...] (shells: bash, zsh; no arguments = auto-detect)" ;;
52+
esac
53+
done
54+
else
55+
# No arguments: detect the user's shell. $0 is useless here (piped
56+
# installs always run under bash), but login shells leave $SHELL behind.
57+
s="${SHELL##*/}"
58+
if { [ "$s" = bash ] || [ "$s" = zsh ]; } && have "$s"; then
59+
echo "No shell specified — detected $s from \$SHELL."
60+
shells=("$s")
61+
else
62+
# $SHELL unset or unsupported: fall back to every supported shell
63+
# that is installed and already has an rc file.
64+
for s in bash zsh; do
65+
if have "$s" && [ -f "$(rc_for_shell "$s")" ]; then
66+
shells+=("$s")
67+
fi
68+
done
69+
[ ${#shells[@]} -eq 0 ] || echo "No shell specified — detected: ${shells[*]}."
70+
fi
71+
[ ${#shells[@]} -gt 0 ] || die "Could not auto-detect your shell (SHELL=${SHELL:-unset}). Re-run with an explicit shell, e.g.: $ONELINER -s zsh"
72+
fi
3273

3374
if [ -d "$TARGET_DIR/.git" ]; then
75+
have git || die "$TARGET_DIR is a git checkout but git is no longer installed. Install git, or remove the directory (rm -rf $TARGET_DIR) and re-run."
3476
echo "$APP_NAME is already cloned at $TARGET_DIR. Pulling latest..."
3577
git -C "$TARGET_DIR" pull --ff-only origin main
3678
elif [ -e "$TARGET_DIR" ]; then
37-
die "$TARGET_DIR exists but is not a git checkout. Move or remove it and re-run."
38-
else
79+
[ -f "$TARGET_DIR/autoactivator_config.sh" ] || die "$TARGET_DIR exists but is not an $APP_NAME install. Move or remove it and re-run."
80+
if have git; then
81+
# A previous tarball install: replace it with a git checkout so
82+
# 'autoactivator update' works from now on.
83+
echo "Upgrading $TARGET_DIR to a git checkout ..."
84+
rm -rf "$TARGET_DIR"
85+
git clone "$REPO_URL" "$TARGET_DIR"
86+
else
87+
echo "Refreshing the existing install at $TARGET_DIR ..."
88+
tarball_install
89+
fi
90+
elif have git; then
3991
echo "Cloning $REPO_URL to $TARGET_DIR ..."
4092
git clone "$REPO_URL" "$TARGET_DIR"
93+
else
94+
tarball_install
4195
fi
4296

4397
CONFIG_PATH="$TARGET_DIR/autoactivator_config.sh"
44-
[ -f "$CONFIG_PATH" ] || die "Expected $CONFIG_PATH after clone but it's missing."
98+
[ -f "$CONFIG_PATH" ] || die "Expected $CONFIG_PATH after install but it's missing."
4599

46100
CONSTANTS_PATH="$TARGET_DIR/_constants.sh"
47-
[ -f "$CONSTANTS_PATH" ] || die "Expected $CONSTANTS_PATH after clone but it's missing."
101+
[ -f "$CONSTANTS_PATH" ] || die "Expected $CONSTANTS_PATH after install but it's missing."
48102
# shellcheck source=_constants.sh disable=SC1091
49103
. "$CONSTANTS_PATH"
50104

51105
installed=()
52-
for shell in "$@"; do
53-
if ! command -v "$shell" >/dev/null 2>&1; then
106+
for shell in "${shells[@]}"; do
107+
if ! have "$shell"; then
54108
echo "Skipping $shell: not installed on this system."
55109
continue
56110
fi
57111

58112
rc=$(rc_for_shell "$shell")
59113
if [ ! -f "$rc" ]; then
60-
echo "Skipping $shell: $rc does not exist. Run 'touch $rc' and re-run."
61-
continue
114+
touch "$rc"
115+
echo "Created $rc (didn't exist)."
62116
fi
63117

64118
backup="$rc.pre-autoactivator"
@@ -85,4 +139,10 @@ done
85139

86140
echo
87141
echo "$APP_NAME installed for: ${installed[*]}"
88-
echo "Restart your terminal for the changes to take effect."
142+
echo
143+
echo "Activate it now — run:"
144+
for shell in "${installed[@]}"; do
145+
rc=$(rc_for_shell "$shell")
146+
printf ' %-5s source ~%s\n' "$shell:" "${rc#"$HOME"}"
147+
done
148+
echo "(or just open a new terminal)"

0 commit comments

Comments
 (0)