Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9669269bc | ||
|
|
68e64eeaef | ||
|
|
d0528b4a92 | ||
|
|
c002a6d902 | ||
|
|
41903ebaf1 |
@@ -5,6 +5,48 @@ All notable changes to `move` are documented here.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.3.2] - 2026-08-17
|
||||
|
||||
### Added
|
||||
- `install.sh` is now interactive. When it finds an existing install and a
|
||||
controlling terminal is available, it reports what's there and asks before
|
||||
replacing it, instead of leaving `MOVE_FORCE` as the only control. If a
|
||||
config file already exists it asks separately whether to reseed it from the
|
||||
shipped defaults. Both questions are asked before anything is downloaded or
|
||||
deleted, so declining changes nothing.
|
||||
- `MOVE_RESEED_CONFIG=1` overwrites the user config with the shipped defaults
|
||||
without prompting, for unattended use. The previous file is kept as
|
||||
`config.json.bak`; the same backup is written when reseeding is confirmed
|
||||
at the prompt.
|
||||
|
||||
### Changed
|
||||
- `MOVE_FORCE=1` now means "skip every prompt and reinstall unconditionally".
|
||||
It deliberately does not touch the user config, so automation that
|
||||
reinstalls the CLI can't take customizations down with it.
|
||||
- Existing-install detection looks for the install tree and the wrapper, not
|
||||
just the `.installed-version` marker, so a half-finished or hand-moved
|
||||
install is caught rather than silently overwritten.
|
||||
|
||||
### Fixed
|
||||
- Installing a *different* version over an existing one used to wipe and
|
||||
replace it with no warning; only an exact version match was ever reported.
|
||||
That case now prompts. With no terminal (CI, cron, container builds) the
|
||||
previous non-interactive behavior is preserved exactly: an identical
|
||||
version is a no-op, a different version is replaced.
|
||||
|
||||
## [1.3.1] - 2026-08-14
|
||||
|
||||
### Added
|
||||
- `docs/execution-happy-path.md`: a sequence diagram (plus invariants) tracing
|
||||
a clean idle-triggered sweep end to end, linked from the README.
|
||||
|
||||
### Fixed
|
||||
- Stale comments corrected to match the current code: the `install.sh` /
|
||||
`uninstall.sh` header curl URLs pointed at a nonexistent repo-root path
|
||||
(they live under `scripts/`), so the documented command 404'd; `move.ts`'s
|
||||
module list omitted `editor.ts` and the `--edit` step; and `config.ts` still
|
||||
described a removed pixel unit.
|
||||
|
||||
## [1.3.0] - 2026-08-14
|
||||
|
||||
### Added
|
||||
@@ -113,6 +155,8 @@ Initial release.
|
||||
- Source split into `src/{move,cli,config,keeper}.ts`.
|
||||
- `bin` entry + shebang so `bun link` registers `move` globally.
|
||||
|
||||
[1.3.2]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.3.1...v1.3.2
|
||||
[1.3.1]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.3.0...v1.3.1
|
||||
[1.3.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.2.0...v1.3.0
|
||||
[1.2.0]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.1.1...v1.2.0
|
||||
[1.1.1]: https://gitea.cahlen.com/nokeo08/Move/compare/v1.1.0...v1.1.1
|
||||
|
||||
@@ -33,14 +33,38 @@ The installer respects the XDG Base Directory Specification:
|
||||
`XDG_BIN_HOME` is the widely-recognized de facto convention; XDG itself
|
||||
doesn't standardize a user bin dir.
|
||||
|
||||
### Reinstalling over an existing install
|
||||
|
||||
The installer never replaces an existing install silently. When it finds
|
||||
one and it can reach a terminal, it tells you what's there and asks:
|
||||
|
||||
```
|
||||
==> Found an existing move install (v1.2.0) at /home/you/.local/share/move
|
||||
Replace it with master? [Y/n]
|
||||
```
|
||||
|
||||
If a config file already exists, it asks separately whether to overwrite
|
||||
it with the shipped defaults (default: no). Both questions come *before*
|
||||
anything is downloaded or deleted, so declining costs you nothing.
|
||||
|
||||
This works under `curl ... | sh` too: the prompts read from `/dev/tty`
|
||||
rather than stdin, which the piped script itself occupies.
|
||||
|
||||
With no terminal available — CI, cron, a container build — there's nobody
|
||||
to ask, so the installer falls back to its long-standing behavior: an
|
||||
identical version is a no-op, a different version is replaced, and your
|
||||
config is left alone. Use the env vars below to drive it explicitly.
|
||||
|
||||
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. |
|
||||
| `MOVE_FORCE` | unset | Set to `1` to skip every prompt and reinstall unconditionally. Never touches your config. |
|
||||
| `MOVE_RESEED_CONFIG` | unset | Set to `1` to overwrite your config with the shipped defaults without asking. The old file is kept as `config.json.bak`. |
|
||||
| `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. |
|
||||
| `XDG_CONFIG_HOME` | `$HOME/.config` | Where the config file lives (under `move/`). |
|
||||
|
||||
Bun must already be installed; the installer fails with a clear pointer
|
||||
to <https://bun.sh> if it isn't.
|
||||
@@ -123,11 +147,14 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
|
||||
```
|
||||
|
||||
The installer seeds this file with the default values on a fresh install,
|
||||
**only if no file already exists at that path**. Existing configs — yours
|
||||
or from a previous install — are never overwritten. If you remove the
|
||||
file later, `move` still works: missing defaults fall back to the values
|
||||
baked into the binary (which match what was seeded, since both come from
|
||||
`scripts/config.default.json`).
|
||||
**only if no file already exists at that path**. An existing config —
|
||||
yours or from a previous install — is never overwritten silently: the
|
||||
installer asks first, and replaces it only if you say yes (or if you set
|
||||
`MOVE_RESEED_CONFIG=1`), keeping the old file as `config.json.bak` either
|
||||
way. `MOVE_FORCE=1` reinstalls the software but leaves your config alone.
|
||||
If you remove the file later, `move` still works: missing defaults fall
|
||||
back to the values baked into the binary (which match what was seeded,
|
||||
since both come from `scripts/config.default.json`).
|
||||
|
||||
Pass `-C` / `--config <path>` to point at a different file; in that mode
|
||||
the file must exist.
|
||||
@@ -206,6 +233,9 @@ file with `--config`.
|
||||
|
||||
## How it works
|
||||
|
||||
For a step-by-step trace of a clean sweep, see the
|
||||
[execution happy-path sequence diagram](docs/execution-happy-path.md).
|
||||
|
||||
The source lives under `src/`, split into an entry point plus logic
|
||||
modules:
|
||||
|
||||
@@ -358,6 +388,7 @@ move --help
|
||||
| `src/device.ts` | `Device` I/O seam over nut.js (`Point`, `createNutDevice`); the only nut.js importer. |
|
||||
| `src/strategies.ts` | Pure movement-pattern generators, the strategy registry, and name validation. |
|
||||
| `src/executor.ts` | `executePath` driver: bounds policy, pacing, interrupt detection, restore. |
|
||||
| `docs/execution-happy-path.md` | Sequence diagram + invariants for a clean sweep. |
|
||||
| `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. |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Execution: the happy path
|
||||
|
||||
This traces one full idle-triggered sweep that completes cleanly — the
|
||||
"happy path" where the machine is idle long enough to fire, the configured
|
||||
pattern runs to exhaustion, and no real user activity interrupts it.
|
||||
|
||||
For the module breakdown and the three seams (`device` / `strategies` /
|
||||
`executor`), see the "How it works" section of the [README](../README.md).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Entry as move.ts
|
||||
participant Keeper as runKeeper
|
||||
participant Sim as simulateActivity
|
||||
participant Strat as Strategy<br/>(e.g. line)
|
||||
participant Exec as executePath
|
||||
participant Dev as Device<br/>(nut.js)
|
||||
|
||||
Note over Entry: startup (args → config)
|
||||
Entry->>Entry: parseCliArgs()
|
||||
Entry->>Entry: loadConfigFile()
|
||||
Entry->>Entry: resolveConfig(file, cli)
|
||||
Entry->>Keeper: runKeeper(config)
|
||||
|
||||
Keeper->>Dev: createNutDevice()
|
||||
Note right of Dev: sets mouse.config.autoDelayMs = 0
|
||||
Keeper->>Keeper: log.info(banner)
|
||||
Keeper->>Dev: getPosition()
|
||||
Dev-->>Keeper: lastPos
|
||||
Note over Keeper: lastActivity = now
|
||||
|
||||
loop every checkInterval (until idle long enough)
|
||||
Keeper->>Dev: sleep(checkInterval)
|
||||
Keeper->>Dev: getPosition()
|
||||
Dev-->>Keeper: pos
|
||||
Note over Keeper: pos == lastPos (no user movement)<br/>now - lastActivity ≥ moveInterval → fire
|
||||
end
|
||||
|
||||
Keeper->>Sim: simulateActivity(config, log, dev)
|
||||
Sim->>Dev: getPosition()
|
||||
Dev-->>Sim: start
|
||||
Sim->>Dev: width()
|
||||
Dev-->>Sim: width
|
||||
Sim->>Dev: height()
|
||||
Dev-->>Sim: height
|
||||
Note over Sim: strategy = STRATEGIES[config.pattern]<br/>ctx = { start, width, height, rng }
|
||||
Sim->>Exec: executePath(strategy, ctx, dev, log, config)
|
||||
|
||||
Exec->>Strat: path(ctx)
|
||||
Strat-->>Exec: iterable of Points
|
||||
|
||||
loop for each target point (clean run)
|
||||
Exec->>Exec: resolveTarget(bounds, target) → point
|
||||
Exec->>Dev: setPosition(point)
|
||||
Exec->>Dev: sleep(stepDelay)
|
||||
Exec->>Dev: getPosition()
|
||||
Dev-->>Exec: current
|
||||
Note over Exec: |current - point| ≤ 2px → not the user, continue
|
||||
end
|
||||
|
||||
Note over Exec: path exhausted, no interruption
|
||||
Exec->>Dev: setPosition(round(start))
|
||||
Note right of Exec: restore cursor to origin
|
||||
Exec-->>Sim: "completed"
|
||||
Sim-->>Keeper: (done)
|
||||
|
||||
Keeper->>Dev: getPosition()
|
||||
Dev-->>Keeper: lastPos (equals start, re-synced)
|
||||
Note over Keeper: lastActivity = now<br/>loop continues
|
||||
```
|
||||
|
||||
## Invariants this path relies on
|
||||
|
||||
- **`createNutDevice()` is the only nut.js touchpoint.** It disables nut.js's
|
||||
100ms auto-delay so `executePath` owns cadence via `stepDelay`.
|
||||
- **The strategy is pure.** `path(ctx)` yields ideal points from geometry
|
||||
alone (`start` / `width` / `height` / `rng`); it never touches the device,
|
||||
which is what makes every pattern unit-testable without a screen.
|
||||
- **Every step re-reads the cursor** and compares it against the *commanded*
|
||||
point (not the strategy's ideal, possibly fractional target) within a 2px
|
||||
tolerance. On the happy path each check passes, so the loop runs to
|
||||
exhaustion. A mismatch beyond tolerance is real user activity and returns
|
||||
`"interrupted"` without restoring — the branch this diagram omits.
|
||||
- **Clean completion restores the cursor to `round(start)`.** That is why the
|
||||
follow-up `getPosition()` in `runKeeper` re-syncs `lastPos` to the origin as
|
||||
a no-op, and the next idle check sees no net movement (so the synthetic
|
||||
sweep is never mistaken for the user returning).
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "move",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.2",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"type": "module",
|
||||
|
||||
+195
-34
@@ -4,31 +4,47 @@
|
||||
#
|
||||
# Curl-pipe ready:
|
||||
#
|
||||
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh
|
||||
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/scripts/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.
|
||||
# 4. Detect an existing install and, on a terminal, ask before replacing it.
|
||||
# 5. Download and build in a temp staging dir; swap it over the install
|
||||
# dir only once it's complete, so a failed run can't destroy a working
|
||||
# install.
|
||||
# 6. `bun install --production` (skips devDependencies).
|
||||
# 7. Drop a small wrapper script as `move` on the user's bin dir.
|
||||
# 8. Seed the user's config file with defaults, only if one doesn't already
|
||||
# exist at $XDG_CONFIG_HOME/move/config.json.
|
||||
# 8. Seed the user's config file with defaults if one doesn't already exist
|
||||
# at $XDG_CONFIG_HOME/move/config.json; if one does, offer to reseed it.
|
||||
# 9. Verify PATH, surface macOS Accessibility hint, print final status.
|
||||
#
|
||||
# Interactivity:
|
||||
# When a controlling terminal is available, an existing install is never
|
||||
# replaced without asking, and an existing config is never overwritten
|
||||
# without asking. Both questions are put up front, before anything is
|
||||
# downloaded or deleted, so declining costs nothing. With no terminal
|
||||
# (CI, cron, container build) the script falls back to its historical
|
||||
# non-interactive contract: an identical version is a no-op, a different
|
||||
# version is replaced, and the config is left alone.
|
||||
#
|
||||
# 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.
|
||||
# MOVE_FORCE Set to 1 to skip every prompt and reinstall
|
||||
# unconditionally. Does not touch the user config.
|
||||
# MOVE_RESEED_CONFIG Set to 1 to overwrite the user config with the
|
||||
# shipped defaults without asking. The previous file
|
||||
# is saved alongside it as config.json.bak.
|
||||
# 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.
|
||||
# XDG_CONFIG_HOME User config root (default $HOME/.config).
|
||||
# Default config file path is $XDG_CONFIG_HOME/move/config.json.
|
||||
# Default config file is $XDG_CONFIG_HOME/move/config.json.
|
||||
#
|
||||
# POSIX sh; no bashisms.
|
||||
# POSIX sh; no bashisms. Note the absence of `local`: helper functions use
|
||||
# `_`-prefixed globals, which POSIX sh leaves us with.
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -38,11 +54,13 @@ GITEA_HOST="gitea.cahlen.com"
|
||||
|
||||
MOVE_VERSION="${MOVE_VERSION:-master}"
|
||||
MOVE_FORCE="${MOVE_FORCE:-0}"
|
||||
MOVE_RESEED_CONFIG="${MOVE_RESEED_CONFIG:-0}"
|
||||
|
||||
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
|
||||
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/move"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
WRAPPER="$BIN_DIR/move"
|
||||
|
||||
die() {
|
||||
printf 'Error: %s\n' "$1" >&2
|
||||
@@ -62,6 +80,56 @@ assert_safe_dir() {
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Interactive prompt support ----------------------------------------------
|
||||
#
|
||||
# The documented entry point is `curl -fsSL ... | sh`, which means stdin is
|
||||
# the *script source itself*. Reading a prompt answer from stdin would
|
||||
# consume the rest of the program and truncate execution mid-run, so every
|
||||
# prompt reads from /dev/tty directly.
|
||||
#
|
||||
# Detecting whether that's possible needs a real open(2) attempt. A `[ -r
|
||||
# /dev/tty ]` test is not enough: the device node exists and is mode 0666
|
||||
# even in contexts with no controlling terminal (cron, CI, container
|
||||
# builds), where opening it fails with ENXIO. The probe runs in a subshell
|
||||
# because a redirection failure on `exec` -- a special built-in -- exits a
|
||||
# non-interactive shell outright under POSIX.
|
||||
|
||||
if (: >/dev/tty) 2>/dev/null; then
|
||||
INTERACTIVE=1
|
||||
else
|
||||
INTERACTIVE=0
|
||||
fi
|
||||
|
||||
# confirm PROMPT DEFAULT -> 0 for yes, 1 for no.
|
||||
#
|
||||
# DEFAULT is 'y' or 'n' and is taken on a bare Enter or on EOF (^D), so the
|
||||
# loop can't spin forever against a closed terminal. Prompts are written to
|
||||
# /dev/tty rather than stdout so they stay visible when the caller redirects
|
||||
# our output.
|
||||
confirm() {
|
||||
_prompt="$1"
|
||||
_default="$2"
|
||||
case "$_default" in
|
||||
y) _hint='[Y/n]' ;;
|
||||
*) _hint='[y/N]' ;;
|
||||
esac
|
||||
while :; do
|
||||
printf '%s %s ' "$_prompt" "$_hint" > /dev/tty
|
||||
if ! IFS= read -r _reply < /dev/tty; then
|
||||
printf '\n' > /dev/tty
|
||||
_reply=''
|
||||
fi
|
||||
if [ -z "$_reply" ]; then
|
||||
_reply="$_default"
|
||||
fi
|
||||
case "$_reply" in
|
||||
[yY] | [yY][eE][sS]) return 0 ;;
|
||||
[nN] | [nN][oO]) return 1 ;;
|
||||
*) printf "Please answer 'y' or 'n'.\n" > /dev/tty ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# --- Prerequisite tools ------------------------------------------------------
|
||||
|
||||
for tool in curl tar mktemp; do
|
||||
@@ -100,53 +168,143 @@ fi
|
||||
BUN_VERSION=$(bun --version)
|
||||
printf '==> Using bun %s\n' "$BUN_VERSION"
|
||||
|
||||
# --- Idempotence check -------------------------------------------------------
|
||||
# --- Existing install check --------------------------------------------------
|
||||
#
|
||||
# Both questions this script can ask are asked here, before anything is
|
||||
# downloaded, deleted, or written. Declining therefore costs the user
|
||||
# nothing, and no prompt appears minutes into a `bun install`.
|
||||
|
||||
assert_safe_dir "$INSTALL_DIR"
|
||||
assert_safe_dir "$CONFIG_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"
|
||||
INSTALLED_VERSION=''
|
||||
if [ -f "$INSTALL_DIR/.installed-version" ]; then
|
||||
INSTALLED_VERSION=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '')
|
||||
fi
|
||||
|
||||
# Look wider than the version marker: a half-finished or hand-edited install
|
||||
# can leave a tree or a wrapper behind without one, and steamrolling that
|
||||
# silently is precisely what this check exists to prevent.
|
||||
FOUND_EXISTING=0
|
||||
if [ -d "$INSTALL_DIR" ] || [ -e "$WRAPPER" ] || [ -L "$WRAPPER" ]; then
|
||||
FOUND_EXISTING=1
|
||||
fi
|
||||
|
||||
# Decided here, applied at the end -- the seed file it copies from only
|
||||
# exists once the tarball has been extracted.
|
||||
RESEED_CONFIG="$MOVE_RESEED_CONFIG"
|
||||
|
||||
if [ "$FOUND_EXISTING" = "1" ] && [ "$MOVE_FORCE" != "1" ]; then
|
||||
if [ -n "$INSTALLED_VERSION" ]; then
|
||||
printf '==> Found an existing move install (%s) at %s\n' \
|
||||
"$INSTALLED_VERSION" "$INSTALL_DIR"
|
||||
else
|
||||
printf '==> Found an existing move install at %s (version unknown)\n' \
|
||||
"$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
if [ "$INTERACTIVE" = "1" ]; then
|
||||
# The defaults below are chosen so that a bare Enter reproduces
|
||||
# exactly what this script did before it learned to ask: skip when
|
||||
# the version is identical, replace when it differs.
|
||||
if [ "$INSTALLED_VERSION" = "$MOVE_VERSION" ]; then
|
||||
REPLACE_PROMPT="Reinstall move $MOVE_VERSION over it?"
|
||||
REPLACE_DEFAULT=n
|
||||
else
|
||||
REPLACE_PROMPT="Replace it with $MOVE_VERSION?"
|
||||
REPLACE_DEFAULT=y
|
||||
fi
|
||||
if ! confirm "$REPLACE_PROMPT" "$REPLACE_DEFAULT"; then
|
||||
printf 'Leaving the existing install alone. Nothing was changed.\n'
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
# Nowhere to ask, so fall back to the historical contract.
|
||||
if [ "$INSTALLED_VERSION" = "$MOVE_VERSION" ]; then
|
||||
printf 'move %s is already installed at %s.\n' "$MOVE_VERSION" "$WRAPPER"
|
||||
printf 'Set MOVE_FORCE=1 to reinstall, or set MOVE_VERSION to a different ref.\n'
|
||||
exit 0
|
||||
fi
|
||||
printf '==> No terminal available; replacing %s with %s\n' \
|
||||
"${INSTALLED_VERSION:-unknown}" "$MOVE_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Clean install dir -------------------------------------------------------
|
||||
if [ -e "$CONFIG_FILE" ] && [ "$RESEED_CONFIG" != "1" ] &&
|
||||
[ "$INTERACTIVE" = "1" ] && [ "$MOVE_FORCE" != "1" ]; then
|
||||
printf '==> A config file already exists at %s\n' "$CONFIG_FILE"
|
||||
if confirm 'Overwrite it with the shipped defaults?' n; then
|
||||
RESEED_CONFIG=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Stage, download, build --------------------------------------------------
|
||||
#
|
||||
# Everything is assembled in a temp staging dir first; the existing install
|
||||
# is removed only once the staged tree is fully built and ready to swap in.
|
||||
# A failed download, extract, or `bun install` therefore leaves a working
|
||||
# install untouched -- unlike the old flow, which wiped INSTALL_DIR before
|
||||
# the download even started and left nothing behind on any failure.
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
rm -rf "$INSTALL_DIR"
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# --- Download source ---------------------------------------------------------
|
||||
DATA_ROOT=$(dirname "$INSTALL_DIR")
|
||||
mkdir -p "$DATA_ROOT"
|
||||
|
||||
TARBALL_URL="https://$GITEA_HOST/$REPO_OWNER/$REPO_NAME/archive/$MOVE_VERSION.tar.gz"
|
||||
TARBALL_TMP=$(mktemp) || die "could not create temp file"
|
||||
# Stage on the same filesystem as INSTALL_DIR so the final swap is a rename,
|
||||
# not a cross-device copy.
|
||||
STAGE_DIR=$(mktemp -d "$DATA_ROOT/.move-stage.XXXXXX") ||
|
||||
{ rm -f "$TARBALL_TMP"; die "could not create staging dir under $DATA_ROOT"; }
|
||||
|
||||
# On any exit, clean up the tarball and any leftover staging dir. After a
|
||||
# successful swap STAGE_DIR has been renamed away, so the rm -rf is a no-op.
|
||||
cleanup() {
|
||||
rm -f "$TARBALL_TMP"
|
||||
rm -rf "$STAGE_DIR"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
TARBALL_URL="https://$GITEA_HOST/$REPO_OWNER/$REPO_NAME/archive/$MOVE_VERSION.tar.gz"
|
||||
|
||||
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
|
||||
printf '==> Extracting source\n'
|
||||
if ! tar -xzf "$TARBALL_TMP" -C "$STAGE_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)
|
||||
(cd "$STAGE_DIR" && bun install --production)
|
||||
|
||||
# Sanity-check the staged tree before we disturb the existing install: a
|
||||
# truncated or wrong tarball that's missing the config seed should fail here,
|
||||
# while the old install is still intact and swappable-out.
|
||||
if [ ! -f "$STAGE_DIR/scripts/config.default.json" ]; then
|
||||
die "downloaded tree is missing scripts/config.default.json (bad MOVE_VERSION='$MOVE_VERSION'?)"
|
||||
fi
|
||||
|
||||
# Record the version inside the staged tree so the install is self-consistent
|
||||
# the instant it lands.
|
||||
printf '%s\n' "$MOVE_VERSION" > "$STAGE_DIR/.installed-version"
|
||||
|
||||
# --- Swap staged tree into place ---------------------------------------------
|
||||
#
|
||||
# The only destructive step, kept as late as possible: the window where
|
||||
# INSTALL_DIR is absent is just this rm + rename, not the whole build.
|
||||
|
||||
assert_safe_dir "$INSTALL_DIR"
|
||||
printf '==> Installing to %s\n' "$INSTALL_DIR"
|
||||
rm -rf "$INSTALL_DIR"
|
||||
if ! mv "$STAGE_DIR" "$INSTALL_DIR"; then
|
||||
die "could not move staged install into place at $INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# --- Drop the wrapper --------------------------------------------------------
|
||||
|
||||
WRAPPER="$BIN_DIR/move"
|
||||
printf '==> Writing wrapper to %s\n' "$WRAPPER"
|
||||
cat > "$WRAPPER" <<EOF
|
||||
#!/usr/bin/env sh
|
||||
@@ -154,23 +312,21 @@ exec bun "$INSTALL_DIR/src/move.ts" "\$@"
|
||||
EOF
|
||||
chmod +x "$WRAPPER"
|
||||
|
||||
# --- Write version marker ----------------------------------------------------
|
||||
|
||||
printf '%s\n' "$MOVE_VERSION" > "$INSTALL_DIR/.installed-version"
|
||||
|
||||
# --- Seed user config file (only if absent) ----------------------------------
|
||||
# --- Seed user config file ---------------------------------------------------
|
||||
#
|
||||
# The defaults file shipped with the source tree (scripts/config.default.json)
|
||||
# is also the single source of truth for the runtime defaults loaded by
|
||||
# src/config.ts, so seeding a fresh user file from the same place keeps the
|
||||
# CLI behavior and the user-visible config in sync.
|
||||
#
|
||||
# Strict policy: never overwrite an existing user config. The uninstaller
|
||||
# follows the matching policy of never removing it; together that
|
||||
# preserves user customizations unconditionally across (re)installs and
|
||||
# uninstalls.
|
||||
# Policy: an existing user config is never overwritten *silently*. It is
|
||||
# replaced only on an explicit answer to the prompt above or an explicit
|
||||
# MOVE_RESEED_CONFIG=1, and even then the previous file is kept as a .bak
|
||||
# rather than destroyed. Everything else -- MOVE_FORCE=1, a non-interactive
|
||||
# run -- leaves it untouched, so automation that reinstalls the software
|
||||
# can't take a user's customizations down with it. The uninstaller follows
|
||||
# the matching policy of never removing the config at all.
|
||||
|
||||
assert_safe_dir "$CONFIG_DIR"
|
||||
SEED_SRC="$INSTALL_DIR/scripts/config.default.json"
|
||||
|
||||
if [ ! -f "$SEED_SRC" ]; then
|
||||
@@ -181,6 +337,11 @@ mkdir -p "$CONFIG_DIR"
|
||||
if [ ! -e "$CONFIG_FILE" ]; then
|
||||
cp "$SEED_SRC" "$CONFIG_FILE"
|
||||
printf '==> Wrote default config to %s\n' "$CONFIG_FILE"
|
||||
elif [ "$RESEED_CONFIG" = "1" ]; then
|
||||
cp "$CONFIG_FILE" "$CONFIG_FILE.bak"
|
||||
cp "$SEED_SRC" "$CONFIG_FILE"
|
||||
printf '==> Reseeded %s (previous file saved as %s)\n' \
|
||||
"$CONFIG_FILE" "$CONFIG_FILE.bak"
|
||||
else
|
||||
printf '==> Config already exists at %s; leaving it alone\n' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
# Curl-pipe ready:
|
||||
#
|
||||
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/uninstall.sh | sh
|
||||
# curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/scripts/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.
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* `resolveConfig` rather than mutating the defaults, so the defaults stay
|
||||
* genuinely constant and the resolved config stays structurally typed.
|
||||
*
|
||||
* All numeric `Config` fields are in their internal units (ms, pixels).
|
||||
* All numeric `Config` fields are in their internal units (milliseconds).
|
||||
* The CLI and config file expose the time-valued fields in seconds for
|
||||
* ergonomics; `resolveConfig` performs the seconds->ms conversion at the
|
||||
* boundary so downstream code never has to think about it.
|
||||
|
||||
+9
-5
@@ -4,23 +4,27 @@
|
||||
* -------
|
||||
* Entry point for the `move` CLI.
|
||||
*
|
||||
* Thin shim that ties the four logic modules together:
|
||||
* Thin shim that ties the logic modules together:
|
||||
* - `cli.ts` parses and validates `process.argv`.
|
||||
* - `configFile.ts` loads and validates the JSON config file.
|
||||
* - `config.ts` holds defaults and the layered `resolveConfig` overlay.
|
||||
* - `keeper.ts` owns the synthetic-activity sweep and idle-watch loop.
|
||||
* - `editor.ts` backs `--edit` (open the config file in `$EDITOR`).
|
||||
* - `keeper.ts` owns the idle-watch loop and drives the movement
|
||||
* machinery (device / strategy / executor).
|
||||
*
|
||||
* Order of operations:
|
||||
* 1. Parse CLI args. Bad input -> stderr + usage hint, exit 2.
|
||||
* 2. `--help` / `--version` short-circuit before any I/O, config load, or
|
||||
* mouse work. `keeper.ts` is also lazy-imported (see below) so these
|
||||
* flags don't pay the cost of loading the nut.js native binary.
|
||||
* 3. Load + validate the config file (default XDG path, or `--config
|
||||
* 3. `--edit` opens the resolved config file in `$EDITOR` and is a
|
||||
* terminal action (propagates the editor's exit code).
|
||||
* 4. Load + validate the config file (default XDG path, or `--config
|
||||
* <path>` if supplied). Validation failures share the exit-2 path.
|
||||
* 4. Resolve the full `Config` (CLI > file > DEFAULT_CONFIG) — verbose
|
||||
* 5. Resolve the full `Config` (CLI > file > DEFAULT_CONFIG) — verbose
|
||||
* lives inside `Config` and is layered with the same precedence as
|
||||
* the numeric fields.
|
||||
* 5. Lazy-import `keeper.ts` (dynamic import keeps nut.js out of the
|
||||
* 6. Lazy-import `keeper.ts` (dynamic import keeps nut.js out of the
|
||||
* `--help` / `--version` startup path) and run it. Any unhandled
|
||||
* rejection — from the import itself or from the loop — exits 1.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user