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
+18 -2
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
`$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 <path>` to point at a different file; in that mode
the file must exist.
@@ -255,9 +270,10 @@ move --help
| `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`. |
+7
View File
@@ -0,0 +1,7 @@
{
"moveInterval": 240,
"checkInterval": 10,
"stepDelay": 50,
"stepCount": 250,
"verbose": false
}
+42 -9
View File
@@ -14,7 +14,9 @@
# 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.
@@ -23,6 +25,8 @@
# 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
+16
View File
@@ -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_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'
+49 -6
View File
@@ -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<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
* 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,
};
/**
+2 -1
View File
@@ -8,7 +8,8 @@
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts"]
}