Add curl-pipe installer, uninstaller, and contributor bootstrap

End-user installation collapses to a single command:

  curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh

Changes:

- install.sh rewritten as a POSIX sh, curl-pipeable end-user installer.
  Honors XDG_DATA_HOME and XDG_BIN_HOME (de facto). Idempotent via a
  version-marker file at $INSTALL_DIR/.installed-version; MOVE_FORCE=1
  overrides. Hard-fails if Bun is missing (no auto-install). Includes
  a safety guard refusing rm -rf on too-broad install dirs.
- uninstall.sh added; same curl-pipe pattern, shared XDG path
  resolution, leaves Bun alone.
- dev-setup.sh added (= the previous install.sh content, retitled for
  contributors and pointing at bun run start / bun link / install.sh).
- README updated: curl one-liner Install section, XDG behavior tables,
  new Uninstall section, new 'For contributors' section, Files table
  rows for all three scripts.
This commit is contained in:
2026-06-17 11:55:03 -05:00
parent 081f94088d
commit d4fa67c6bd
4 changed files with 347 additions and 68 deletions
+80 -40
View File
@@ -11,7 +11,7 @@ cursor leaves the position the script just commanded.
## Requirements
- [Bun](https://bun.sh) >= 1.0.0
- macOS, Linux, or Windows (relies on
- macOS or Linux (relies on
[`@nut-tree-fork/nut-js`](https://github.com/nut-tree-fork/nut.js) for
cross-platform mouse + screen control)
- On macOS: Accessibility permission for the terminal running Bun
@@ -20,38 +20,53 @@ cursor leaves the position the script just commanded.
## Install
```sh
./install.sh
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh
```
The installer verifies `bun` is on `PATH` and runs `bun install`. It does
not auto-install Bun; if Bun is missing it prints a hard error pointing at
<https://bun.sh>.
This fetches the latest `master` from Gitea, runs `bun install --production`
under the install dir, and drops a `move` wrapper on your bin dir.
The installer respects the XDG Base Directory Specification:
- Source lives at `$XDG_DATA_HOME/move` (default `~/.local/share/move`).
- Wrapper goes to `$XDG_BIN_HOME/move` (default `~/.local/bin/move`).
`XDG_BIN_HOME` is the widely-recognized de facto convention; XDG itself
doesn't standardize a user bin dir.
Env vars (all optional):
| Var | Default | Purpose |
| --- | ------- | ------- |
| `MOVE_VERSION` | `master` | Branch or tag to install. Pin with e.g. `v1.0.0`. |
| `MOVE_FORCE` | unset | Set to `1` to reinstall when the same version is already present. |
| `XDG_DATA_HOME` | `$HOME/.local/share` | Where the source tree is installed (under `move/`). |
| `XDG_BIN_HOME` | `$HOME/.local/bin` | Where the `move` wrapper is placed. |
Bun must already be installed; the installer fails with a clear pointer
to <https://bun.sh> if it isn't.
If your bin dir isn't on `PATH`, the installer prints the line to add to
your shell rc. It will not modify rc files for you.
## Run
```sh
bun run start
```
Or directly:
```sh
bun run src/move.ts
```
Or install it as a global `move` command via Bun's bin-linking:
```sh
bun link
move
move --help
```
`bun link` registers the `bin.move` entry from `package.json` (`./src/move.ts`,
which carries `#!/usr/bin/env bun`) as an executable on your `PATH`.
Stop with `Ctrl+C`. If `SIGINT` arrives mid-sweep, the cursor stays at
whichever step was last commanded — by design.
## Uninstall
```sh
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/uninstall.sh | sh
```
Removes the wrapper at `$XDG_BIN_HOME/move` and the install tree at
`$XDG_DATA_HOME/move`. Bun stays — it's your runtime, not ours.
## Usage
```text
@@ -68,12 +83,9 @@ Options:
(default prints only the startup banner).
```
Run `bun run src/move.ts --help` for the full text.
All flags are wired. Numeric overrides are layered onto the defaults via
`resolveConfig` in `src/config.ts`; time-valued inputs (`-m`, `-c`) are
expressed in seconds at the CLI boundary and converted to milliseconds
internally.
Numeric overrides are layered onto the defaults via `resolveConfig` in
`src/config.ts`; time-valued inputs (`-m`, `-c`) are expressed in seconds
at the CLI boundary and converted to milliseconds internally.
Logging is **quiet by default**: only the startup banner ("Teams Status
Keeper started…") and any error from an unhandled rejection print on a
@@ -140,21 +152,49 @@ sweep. The script controls cadence itself via `config.stepDelay`, so the
implicit delay is disabled at module load (a side effect of importing
`keeper.ts`).
## For contributors
Clone the repo and bootstrap a dev environment:
```sh
git clone https://gitea.cahlen.com/nokeo08/Move.git
cd Move
./dev-setup.sh
```
`dev-setup.sh` verifies Bun is installed and runs `bun install` (with
devDependencies, unlike the end-user `install.sh`).
Run from the source tree:
```sh
bun run start # via the package.json script
bun run src/move.ts # direct
bun run src/move.ts --help
```
Or install a global `move` pointed at your checkout:
```sh
bun link
move --help
```
## Files
| File | Purpose |
| ------------------- | ----------------------------------------------------------------------- |
| `src/move.ts` | CLI entry point: parses args, dispatches help/version, starts the loop. |
| `src/cli.ts` | Argument parsing, validation, and help/version output. |
| `src/config.ts` | `Config` type, `DEFAULT_CONFIG`, and `resolveConfig` overlay. |
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
| `install.sh` | Bootstrap: verify Bun, `bun install`, print macOS permission hint. |
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
| `bun.lock` | Bun's lockfile. Commit this. |
| `LICENSE` | GPLv3 license text. |
| `CHANGES.md` | History of the post-port review fixes. |
| `MouseMover.java` | Original Java source, kept for reference. |
| File | Purpose |
| ------------------- | ----------------------------------------------------------------------------- |
| `install.sh` | End-user installer; curl-pipeable from Gitea. |
| `uninstall.sh` | End-user uninstaller; curl-pipeable from Gitea. |
| `dev-setup.sh` | Contributor bootstrap (verify Bun + `bun install`). |
| `src/move.ts` | CLI entry point: parses args, dispatches help/version, starts the loop. |
| `src/cli.ts` | Argument parsing, validation, and help/version output. |
| `src/config.ts` | `Config` type, `DEFAULT_CONFIG`, and `resolveConfig` overlay. |
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
| `bun.lock` | Bun's lockfile. Commit this. |
| `LICENSE` | GPLv3 license text. |
## License
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
#
# dev-setup.sh - contributor bootstrap for the `move` repo.
#
# Use this when you have cloned the repo and want a working development
# environment. End users should use `install.sh` (curl-pipeable) instead.
#
# Verifies Bun is installed, then runs `bun install` to fetch dependencies
# (notably `@nut-tree-fork/nut-js`, which ships prebuilt native binaries
# for darwin-arm64/x64, linux, and windows).
#
# This script is idempotent: re-running it just re-resolves the dependency
# tree against the existing `bun.lock`.
# Fail fast on any error, unset variable, or failed pipe stage.
set -euo pipefail
# Always operate relative to the script's own directory so the install works
# regardless of the caller's CWD.
cd "$(dirname "$0")"
echo "==> Checking for Bun..."
if ! command -v bun >/dev/null 2>&1; then
# Hard error rather than auto-install: keep the script's behavior
# predictable and let the user pick their own install method.
echo "Error: bun is not installed." >&2
echo " Install it from https://bun.sh (e.g. 'brew install oven-sh/bun/bun') and re-run." >&2
exit 1
fi
echo " bun $(bun --version)"
echo "==> Installing dependencies..."
bun install
echo
echo "Done. Dev workflow:"
echo " - 'bun run start' to run from the source tree."
echo " - 'bun link' to install a global 'move' command pointed at this checkout."
echo "End-user installer is install.sh (curl-pipeable from Gitea)."
# macOS gates synthetic mouse events behind the Accessibility permission.
# Without this hint, the first run silently fails to move the cursor and
# the user has no obvious next step.
if [[ "$(uname)" == "Darwin" ]]; then
echo "Note: macOS will prompt for Accessibility permission on first mouse move."
echo " Grant it under System Settings > Privacy & Security > Accessibility."
fi
+164 -28
View File
@@ -1,41 +1,177 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
#
# install.sh - bootstrap the Teams Status Keeper project.
# install.sh - End-user installer for the `move` CLI.
#
# Verifies Bun is installed, then runs `bun install` to fetch dependencies
# (notably `@nut-tree-fork/nut-js`, which ships prebuilt native binaries
# for darwin-arm64/x64, linux, and windows).
# Curl-pipe ready:
#
# This script is idempotent: re-running it just re-resolves the dependency
# tree against the existing `bun.lock`.
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh
#
# What it does:
# 1. Detect platform; bail on anything @nut-tree-fork/nut-js doesn't ship.
# 2. Require Bun; fail with a clear hint if missing (no auto-install).
# 3. Resolve XDG-compliant install paths.
# 4. Idempotence check via a version marker file.
# 5. Download the source tarball from Gitea, extract under the install dir.
# 6. `bun install --production` (skips devDependencies).
# 7. Drop a small wrapper script as `move` on the user's bin dir.
# 8. Verify PATH, surface macOS Accessibility hint, print final status.
#
# Env vars (all optional):
# MOVE_VERSION Branch or tag to install. Default: master.
# MOVE_FORCE Set to 1 to reinstall even if the version marker matches.
# XDG_DATA_HOME Source install root (default $HOME/.local/share).
# Final source location is $XDG_DATA_HOME/move.
# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin).
# Final binary location is $XDG_BIN_HOME/move.
#
# POSIX sh; no bashisms.
# Fail fast on any error, unset variable, or failed pipe stage.
set -euo pipefail
set -eu
# Always operate relative to the script's own directory so the install works
# regardless of the caller's CWD.
cd "$(dirname "$0")"
REPO_OWNER="nokeo08"
REPO_NAME="Move"
GITEA_HOST="gitea.cahlen.com"
MOVE_VERSION="${MOVE_VERSION:-master}"
MOVE_FORCE="${MOVE_FORCE:-0}"
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
# Refuse to operate on a directory that points somewhere catastrophic.
# `INSTALL_DIR` derives from XDG_DATA_HOME, a broadly-scoped env var the
# user could conceivably set to anything; the install path always
# culminates in `.../move`, but a malformed XDG_DATA_HOME could still
# resolve to something like '/move' which we don't want to `rm -rf`.
assert_safe_install_dir() {
case "$INSTALL_DIR" in
'' | '/' | "$HOME" | "$HOME/" | '/move')
die "refusing to operate on INSTALL_DIR='$INSTALL_DIR' (too broad)"
;;
esac
}
# --- Prerequisite tools ------------------------------------------------------
for tool in curl tar mktemp; do
if ! command -v "$tool" >/dev/null 2>&1; then
die "$tool is required (not found in PATH)"
fi
done
# --- Platform detection ------------------------------------------------------
OS=$(uname -s)
ARCH=$(uname -m)
case "$OS" in
Darwin|Linux) ;;
*) die "unsupported OS '$OS' (move supports macOS and Linux)" ;;
esac
case "$ARCH" in
arm64|aarch64|x86_64|amd64) ;;
*) die "unsupported architecture '$ARCH'" ;;
esac
# --- Bun check (hard fail; no auto-install) ----------------------------------
echo "==> Checking for Bun..."
if ! command -v bun >/dev/null 2>&1; then
# Hard error rather than auto-install: keep the script's behavior
# predictable and let the user pick their own install method.
echo "Error: bun is not installed." >&2
echo " Install it from https://bun.sh (e.g. 'brew install oven-sh/bun/bun') and re-run." >&2
cat >&2 <<EOF
Error: bun is not installed.
Install it from https://bun.sh
(e.g. 'curl -fsSL https://bun.sh/install | bash')
then re-run this installer.
EOF
exit 1
fi
echo " bun $(bun --version)"
echo "==> Installing dependencies..."
bun install
BUN_VERSION=$(bun --version)
printf '==> Using bun %s\n' "$BUN_VERSION"
echo
echo "Done. Run with: bun run start"
# --- Idempotence check -------------------------------------------------------
# macOS gates synthetic mouse events behind the Accessibility permission.
# Without this hint, the first run silently fails to move the cursor and
# the user has no obvious next step.
if [[ "$(uname)" == "Darwin" ]]; then
echo "Note: macOS will prompt for Accessibility permission on first mouse move."
echo " Grant it under System Settings > Privacy & Security > Accessibility."
assert_safe_install_dir
if [ "$MOVE_FORCE" != "1" ] && [ -f "$INSTALL_DIR/.installed-version" ]; then
CURRENT=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '')
if [ "$CURRENT" = "$MOVE_VERSION" ]; then
printf 'move %s is already installed at %s/move.\n' "$MOVE_VERSION" "$BIN_DIR"
printf 'Set MOVE_FORCE=1 to reinstall, or set MOVE_VERSION to a different ref.\n'
exit 0
fi
fi
# --- Clean install dir -------------------------------------------------------
mkdir -p "$BIN_DIR"
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# --- Download source ---------------------------------------------------------
TARBALL_URL="https://$GITEA_HOST/$REPO_OWNER/$REPO_NAME/archive/$MOVE_VERSION.tar.gz"
TARBALL_TMP=$(mktemp) || die "could not create temp file"
cleanup() {
rm -f "$TARBALL_TMP"
}
trap cleanup EXIT INT TERM
printf '==> Downloading %s\n' "$TARBALL_URL"
if ! curl -fsSL "$TARBALL_URL" -o "$TARBALL_TMP"; then
die "could not download $TARBALL_URL (check MOVE_VERSION='$MOVE_VERSION' and network)"
fi
printf '==> Extracting source to %s\n' "$INSTALL_DIR"
if ! tar -xzf "$TARBALL_TMP" -C "$INSTALL_DIR" --strip-components=1; then
die "could not extract tarball from $TARBALL_URL"
fi
# --- Install runtime deps ----------------------------------------------------
printf '==> Installing runtime dependencies (bun install --production)\n'
(cd "$INSTALL_DIR" && bun install --production)
# --- Drop the wrapper --------------------------------------------------------
WRAPPER="$BIN_DIR/move"
printf '==> Writing wrapper to %s\n' "$WRAPPER"
cat > "$WRAPPER" <<EOF
#!/usr/bin/env sh
exec bun "$INSTALL_DIR/src/move.ts" "\$@"
EOF
chmod +x "$WRAPPER"
# --- Write version marker ----------------------------------------------------
printf '%s\n' "$MOVE_VERSION" > "$INSTALL_DIR/.installed-version"
# --- PATH sanity check -------------------------------------------------------
case ":$PATH:" in
*":$BIN_DIR:"*) BIN_ON_PATH=1 ;;
*) BIN_ON_PATH=0 ;;
esac
if [ "$BIN_ON_PATH" = "0" ]; then
printf '\nNote: %s is not on your PATH. Add this to your shell rc:\n' "$BIN_DIR"
printf ' export PATH="%s:$PATH"\n' "$BIN_DIR"
fi
# --- macOS Accessibility hint ------------------------------------------------
if [ "$OS" = "Darwin" ]; then
printf '\nNote: macOS will prompt for Accessibility permission on first mouse move.\n'
printf ' Grant it under System Settings > Privacy & Security > Accessibility.\n'
fi
# --- Final message -----------------------------------------------------------
printf '\nInstalled move %s at %s.\n' "$MOVE_VERSION" "$WRAPPER"
printf "Run 'move --help' to get started.\n"
Executable
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env sh
#
# uninstall.sh - End-user uninstaller for the `move` CLI.
#
# Curl-pipe ready:
#
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/uninstall.sh | sh
#
# Removes the `move` wrapper from $XDG_BIN_HOME and the install tree from
# $XDG_DATA_HOME/move. Does NOT remove Bun — that's your runtime, not ours.
#
# Env vars (must match what install.sh used):
# XDG_DATA_HOME Source install root (default $HOME/.local/share).
# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin).
#
# POSIX sh; no bashisms.
set -eu
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
WRAPPER="$BIN_DIR/move"
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
# Same safety guard as install.sh: refuse to wipe a directory that points
# somewhere catastrophic.
case "$INSTALL_DIR" in
'' | '/' | "$HOME" | "$HOME/" | '/move')
die "refusing to operate on INSTALL_DIR='$INSTALL_DIR' (too broad)"
;;
esac
REMOVED_SOMETHING=0
if [ -e "$WRAPPER" ] || [ -L "$WRAPPER" ]; then
rm -f "$WRAPPER"
printf 'Removed %s\n' "$WRAPPER"
REMOVED_SOMETHING=1
fi
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
printf 'Removed %s\n' "$INSTALL_DIR"
REMOVED_SOMETHING=1
fi
if [ "$REMOVED_SOMETHING" = "0" ]; then
printf 'Nothing to remove. Checked %s and %s.\n' "$WRAPPER" "$INSTALL_DIR"
exit 0
fi
printf '\nUninstalled. (Bun was not touched.)\n'