diff --git a/README.md b/README.md index 628fb36..f8f0e52 100644 --- a/README.md +++ b/README.md @@ -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 -. +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 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 diff --git a/dev-setup.sh b/dev-setup.sh new file mode 100755 index 0000000..f2156c6 --- /dev/null +++ b/dev-setup.sh @@ -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 diff --git a/install.sh b/install.sh index 168bea0..428efdb 100755 --- a/install.sh +++ b/install.sh @@ -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 < 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" < "$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" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..a7b1597 --- /dev/null +++ b/uninstall.sh @@ -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'