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.
This commit is contained in:
2026-06-17 13:13:49 -05:00
parent 940113019d
commit 7714a6ad1a
6 changed files with 145 additions and 29 deletions
+21 -5
View File
@@ -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 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. `$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 ## Usage
```text ```text
@@ -109,7 +118,13 @@ error to `stderr` and exits with code `2`.
${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json ${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 <path>` to point at a different file; in that mode Pass `-C` / `--config <path>` to point at a different file; in that mode
the file must exist. the file must exist.
@@ -252,12 +267,13 @@ move --help
| File | Purpose | | File | Purpose |
| ------------------- | ----------------------------------------------------------------------------- | | ------------------- | ----------------------------------------------------------------------------- |
| `scripts/install.sh` | End-user installer; curl-pipeable from Gitea. | | `scripts/install.sh` | End-user installer; curl-pipeable from Gitea. |
| `scripts/uninstall.sh` | End-user uninstaller; 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/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/move.ts` | CLI entry point: parses args, dispatches help/version, starts the loop. |
| `src/cli.ts` | Argument parsing, validation, and help/version output. | | `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/configFile.ts` | Optional JSON config-file loader with strict schema validation. |
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. | | `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. | | `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
+7
View File
@@ -0,0 +1,7 @@
{
"moveInterval": 240,
"checkInterval": 10,
"stepDelay": 50,
"stepCount": 250,
"verbose": false
}
+48 -15
View File
@@ -14,15 +14,19 @@
# 5. Download the source tarball from Gitea, extract under the install dir. # 5. Download the source tarball from Gitea, extract under the install dir.
# 6. `bun install --production` (skips devDependencies). # 6. `bun install --production` (skips devDependencies).
# 7. Drop a small wrapper script as `move` on the user's bin dir. # 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): # Env vars (all optional):
# MOVE_VERSION Branch or tag to install. Default: master. # 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 reinstall even if the version marker matches.
# XDG_DATA_HOME Source install root (default $HOME/.local/share). # XDG_DATA_HOME Source install root (default $HOME/.local/share).
# Final source location is $XDG_DATA_HOME/move. # Final source location is $XDG_DATA_HOME/move.
# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin). # XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin).
# Final binary location is $XDG_BIN_HOME/move. # 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. # POSIX sh; no bashisms.
@@ -37,6 +41,8 @@ MOVE_FORCE="${MOVE_FORCE:-0}"
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move" INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}" BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/move"
CONFIG_FILE="$CONFIG_DIR/config.json"
die() { die() {
printf 'Error: %s\n' "$1" >&2 printf 'Error: %s\n' "$1" >&2
@@ -44,14 +50,14 @@ die() {
} }
# Refuse to operate on a directory that points somewhere catastrophic. # Refuse to operate on a directory that points somewhere catastrophic.
# `INSTALL_DIR` derives from XDG_DATA_HOME, a broadly-scoped env var the # The XDG_* env vars are broadly-scoped and the user could set them to
# user could conceivably set to anything; the install path always # anything; our paths always culminate in `.../move`, but a malformed
# culminates in `.../move`, but a malformed XDG_DATA_HOME could still # XDG var could still resolve to something like '/move' which we don't
# resolve to something like '/move' which we don't want to `rm -rf`. # want to `rm -rf` or otherwise mass-write into.
assert_safe_install_dir() { assert_safe_dir() {
case "$INSTALL_DIR" in case "$1" in
'' | '/' | "$HOME" | "$HOME/" | '/move') '' | '/' | "$HOME" | "$HOME/" | '/move')
die "refusing to operate on INSTALL_DIR='$INSTALL_DIR' (too broad)" die "refusing to operate on '$1' (too broad)"
;; ;;
esac esac
} }
@@ -96,7 +102,7 @@ printf '==> Using bun %s\n' "$BUN_VERSION"
# --- Idempotence check ------------------------------------------------------- # --- Idempotence check -------------------------------------------------------
assert_safe_install_dir assert_safe_dir "$INSTALL_DIR"
if [ "$MOVE_FORCE" != "1" ] && [ -f "$INSTALL_DIR/.installed-version" ]; then if [ "$MOVE_FORCE" != "1" ] && [ -f "$INSTALL_DIR/.installed-version" ]; then
CURRENT=$(cat "$INSTALL_DIR/.installed-version" 2>/dev/null || printf '') 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" 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 ------------------------------------------------------- # --- PATH sanity check -------------------------------------------------------
case ":$PATH:" in case ":$PATH:" in
+18 -2
View File
@@ -8,10 +8,15 @@
# #
# Removes the `move` wrapper from $XDG_BIN_HOME and the install tree from # 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. # $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): # Env vars (must match what install.sh used):
# XDG_DATA_HOME Source install root (default $HOME/.local/share). # XDG_DATA_HOME Source install root (default $HOME/.local/share).
# XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin). # XDG_BIN_HOME Wrapper install root (default $HOME/.local/bin).
# XDG_CONFIG_HOME User config root (default $HOME/.config).
# #
# POSIX sh; no bashisms. # POSIX sh; no bashisms.
@@ -19,6 +24,8 @@ set -eu
INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move" INSTALL_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/move"
BIN_DIR="${XDG_BIN_HOME:-$HOME/.local/bin}" 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" WRAPPER="$BIN_DIR/move"
die() { die() {
@@ -50,7 +57,16 @@ fi
if [ "$REMOVED_SOMETHING" = "0" ]; then if [ "$REMOVED_SOMETHING" = "0" ]; then
printf 'Nothing to remove. Checked %s and %s.\n' "$WRAPPER" "$INSTALL_DIR" 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 exit 0
fi 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' printf '\nUninstalled. (Bun was not touched.)\n'
+49 -6
View File
@@ -21,6 +21,13 @@
import { join } from "node:path"; 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 * The shape of a resolved runtime configuration. `readonly` to make
* accidental mutation a type error. * accidental mutation a type error.
@@ -44,16 +51,52 @@ export interface Config {
readonly verbose: boolean; 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<string, unknown>;
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 * 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 = { export const DEFAULT_CONFIG: Config = {
moveInterval: 4 * 60 * 1000, // 4 minutes in milliseconds moveInterval: seed.moveInterval * 1000,
checkInterval: 10 * 1000, // Check every 10 seconds checkInterval: seed.checkInterval * 1000,
stepDelay: 50, // ms between mouse steps stepDelay: seed.stepDelay,
stepCount: 250, // pixels to move per sweep stepCount: seed.stepCount,
verbose: false, // quiet by default; startup banner still prints verbose: seed.verbose,
}; };
/** /**
+2 -1
View File
@@ -8,7 +8,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true, "noEmit": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"verbatimModuleSyntax": true "verbatimModuleSyntax": true,
"resolveJsonModule": true
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"]
} }