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
+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,
};
/**