From 7714a6ad1a2cc7801da11d20e470fecad1755a51 Mon Sep 17 00:00:00 2001 From: nokeo08 Date: Wed, 17 Jun 2026 13:13:49 -0500 Subject: [PATCH] Seed default config on install; share defaults JSON with runtime The defaults now live in a single file, scripts/config.default.json: - src/config.ts imports it via 'with { type: "json" }' and derives DEFAULT_CONFIG (with seconds->ms conversion at the boundary), so the CLI help text always matches what the seeded user config contains. - scripts/install.sh copies the same file to $XDG_CONFIG_HOME/move/config.json only if no file is already there. Existing configs (yours or from a previous install) are never overwritten. - scripts/uninstall.sh does not touch the user config file at all, following the strict Unix convention. It does print a one-line notice pointing at the path so the user can rm -rf the dir themselves if they want a fully-clean removal. - tsconfig.json gains resolveJsonModule:true so tsc accepts the JSON import. - A small runtime assertion in src/config.ts (assertSeedShape) fails loudly if the seed file is missing keys or has wrong types. - README Configuration section documents the seed behavior; Uninstall section documents the leave-config-behind policy with the rm-it- yourself command; Files table gains scripts/config.default.json. --- README.md | 26 ++++++++++++--- scripts/config.default.json | 7 +++++ scripts/install.sh | 63 ++++++++++++++++++++++++++++--------- scripts/uninstall.sh | 20 ++++++++++-- src/config.ts | 55 ++++++++++++++++++++++++++++---- tsconfig.json | 3 +- 6 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 scripts/config.default.json diff --git a/README.md b/README.md index 540e58c..0dfd494 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,15 @@ curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/scripts/unins 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. +Your config file at `$XDG_CONFIG_HOME/move/config.json` is **intentionally +left behind**, whether you customized it or never touched the seeded +defaults. The uninstaller prints a one-line notice pointing at the path +so you can remove it manually if you want: + +```sh +rm -rf "${XDG_CONFIG_HOME:-$HOME/.config}/move" +``` + ## Usage ```text @@ -109,7 +118,13 @@ error to `stderr` and exits with code `2`. ${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json ``` -If the file is missing, defaults are used (no config file is required). +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`). + Pass `-C` / `--config ` to point at a different file; in that mode the file must exist. @@ -252,12 +267,13 @@ move --help | File | Purpose | | ------------------- | ----------------------------------------------------------------------------- | -| `scripts/install.sh` | End-user installer; curl-pipeable from Gitea. | -| `scripts/uninstall.sh` | End-user uninstaller; curl-pipeable from Gitea. | -| `scripts/dev-setup.sh` | Contributor bootstrap (verify Bun + `bun install`). | +| `scripts/install.sh` | End-user installer; curl-pipeable from Gitea. | +| `scripts/uninstall.sh` | End-user uninstaller; curl-pipeable from Gitea. | +| `scripts/dev-setup.sh` | Contributor bootstrap (verify Bun + `bun install`). | +| `scripts/config.default.json`| Single source of truth for default values: imported by `src/config.ts` and copied to `$XDG_CONFIG_HOME/move/config.json` on a fresh 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 (carries every tunable, including `verbose`), `DEFAULT_CONFIG`, `defaultConfigPath`, and the layered `resolveConfig` overlay. | +| `src/config.ts` | `Config` type (carries every tunable, including `verbose`), `DEFAULT_CONFIG` (derived from `scripts/config.default.json`), `defaultConfigPath`, and the layered `resolveConfig` overlay. | | `src/configFile.ts` | Optional JSON config-file loader with strict schema validation. | | `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. | | `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. | diff --git a/scripts/config.default.json b/scripts/config.default.json new file mode 100644 index 0000000..673dcce --- /dev/null +++ b/scripts/config.default.json @@ -0,0 +1,7 @@ +{ + "moveInterval": 240, + "checkInterval": 10, + "stepDelay": 50, + "stepCount": 250, + "verbose": false +} diff --git a/scripts/install.sh b/scripts/install.sh index 428efdb..a4eea54 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -14,15 +14,19 @@ # 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. +# 8. Seed the user's config file with defaults, only if one doesn't already +# exist at $XDG_CONFIG_HOME/move/config.json. +# 9. 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. +# 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. +# XDG_CONFIG_HOME User config root (default $HOME/.config). +# Default config file path is $XDG_CONFIG_HOME/move/config.json. # # POSIX sh; no bashisms. @@ -37,6 +41,8 @@ MOVE_FORCE="${MOVE_FORCE:-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" die() { printf 'Error: %s\n' "$1" >&2 @@ -44,14 +50,14 @@ die() { } # 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 +# The XDG_* env vars are broadly-scoped and the user could set them to +# anything; our paths always culminate in `.../move`, but a malformed +# XDG var could still resolve to something like '/move' which we don't +# want to `rm -rf` or otherwise mass-write into. +assert_safe_dir() { + case "$1" in '' | '/' | "$HOME" | "$HOME/" | '/move') - die "refusing to operate on INSTALL_DIR='$INSTALL_DIR' (too broad)" + die "refusing to operate on '$1' (too broad)" ;; esac } @@ -96,7 +102,7 @@ printf '==> Using bun %s\n' "$BUN_VERSION" # --- Idempotence check ------------------------------------------------------- -assert_safe_install_dir +assert_safe_dir "$INSTALL_DIR" if [ "$MOVE_FORCE" != "1" ] && [ -f "$INSTALL_DIR/.installed-version" ]; then CURRENT=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '') @@ -152,6 +158,33 @@ chmod +x "$WRAPPER" printf '%s\n' "$MOVE_VERSION" > "$INSTALL_DIR/.installed-version" +# --- Seed user config file (only if absent) ---------------------------------- +# +# 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. + +assert_safe_dir "$CONFIG_DIR" +SEED_SRC="$INSTALL_DIR/scripts/config.default.json" + +if [ ! -f "$SEED_SRC" ]; then + die "default config seed missing from install tree: $SEED_SRC" +fi + +mkdir -p "$CONFIG_DIR" +if [ ! -e "$CONFIG_FILE" ]; then + cp "$SEED_SRC" "$CONFIG_FILE" + printf '==> Wrote default config to %s\n' "$CONFIG_FILE" +else + printf '==> Config already exists at %s; leaving it alone\n' "$CONFIG_FILE" +fi + # --- PATH sanity check ------------------------------------------------------- case ":$PATH:" in diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index a7b1597..c250c9b 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -8,10 +8,15 @@ # # 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. +# Also leaves the user config file at $XDG_CONFIG_HOME/move/config.json +# intact (Unix convention; user customizations are not ours to delete). +# A notice is printed pointing at the file so you can remove it yourself +# if desired. # # 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). +# XDG_DATA_HOME Source install root (default $HOME/.local/share). +# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin). +# XDG_CONFIG_HOME User config root (default $HOME/.config). # # POSIX sh; no bashisms. @@ -19,6 +24,8 @@ set -eu 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() { @@ -50,7 +57,16 @@ fi if [ "$REMOVED_SOMETHING" = "0" ]; then printf 'Nothing to remove. Checked %s and %s.\n' "$WRAPPER" "$INSTALL_DIR" + if [ -e "$CONFIG_FILE" ]; then + printf 'Note: config file at %s was left in place.\n' "$CONFIG_FILE" + printf ' Remove it manually with: rm -rf %s\n' "$CONFIG_DIR" + fi exit 0 fi +if [ -e "$CONFIG_FILE" ]; then + printf '\nNote: your config file at %s was left in place.\n' "$CONFIG_FILE" + printf ' Remove it manually with: rm -rf %s\n' "$CONFIG_DIR" +fi + printf '\nUninstalled. (Bun was not touched.)\n' diff --git a/src/config.ts b/src/config.ts index 800f9de..f6a0f70 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,13 @@ import { join } from "node:path"; +// Single source of truth for default values. The same file ships in the +// install tree and is copied to $XDG_CONFIG_HOME/move/config.json on a +// fresh install (only if no config exists there yet). Values use the CLI +// units (seconds for time fields, ms for stepDelay, pixels for stepCount); +// the seconds->ms conversion happens below where DEFAULT_CONFIG is built. +import seedRaw from "../scripts/config.default.json" with { type: "json" }; + /** * The shape of a resolved runtime configuration. `readonly` to make * accidental mutation a type error. @@ -44,16 +51,52 @@ export interface Config { readonly verbose: boolean; } +/** + * Shape of `scripts/config.default.json` after parsing. The cast below + * trusts the file's structure; `assertSeedShape` performs a small runtime + * sanity check at import time so a corrupted seed file fails loudly + * instead of silently producing `NaN` or `undefined` defaults. + */ +interface SeedShape { + moveInterval: number; // seconds + checkInterval: number; // seconds + stepDelay: number; // milliseconds + stepCount: number; // pixels + verbose: boolean; +} + +function assertSeedShape(raw: unknown): asserts raw is SeedShape { + if (typeof raw !== "object" || raw === null) { + throw new Error("scripts/config.default.json: root must be an object"); + } + const r = raw as Record; + for (const key of ["moveInterval", "checkInterval", "stepDelay", "stepCount"] as const) { + const v = r[key]; + if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) { + throw new Error(`scripts/config.default.json: '${key}' must be a positive finite number (got ${JSON.stringify(v)})`); + } + } + if (typeof r.verbose !== "boolean") { + throw new Error(`scripts/config.default.json: 'verbose' must be a boolean (got ${JSON.stringify(r.verbose)})`); + } +} + +assertSeedShape(seedRaw); +const seed: SeedShape = seedRaw; + /** * Built-in defaults used when neither the CLI nor the config file supplies - * a value for a given field. + * a value for a given field. Derived from `scripts/config.default.json` + * (the single source of truth); time-valued fields are converted from + * seconds to milliseconds here so the rest of the codebase works in + * internal units. */ export const DEFAULT_CONFIG: Config = { - moveInterval: 4 * 60 * 1000, // 4 minutes in milliseconds - checkInterval: 10 * 1000, // Check every 10 seconds - stepDelay: 50, // ms between mouse steps - stepCount: 250, // pixels to move per sweep - verbose: false, // quiet by default; startup banner still prints + moveInterval: seed.moveInterval * 1000, + checkInterval: seed.checkInterval * 1000, + stepDelay: seed.stepDelay, + stepCount: seed.stepCount, + verbose: seed.verbose, }; /** diff --git a/tsconfig.json b/tsconfig.json index 0b5cf29..91d3a2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "skipLibCheck": true, "noEmit": true, "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + "resolveJsonModule": true }, "include": ["src/**/*.ts"] }