Add JSON config file support (XDG-respecting)
End users can now set defaults in a config file at:
${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
CLI flags continue to win when both are set:
CLI flags > config file > built-in defaults
Schema mirrors the CLI flag names and units. Loader is strict: unknown
keys, wrong types, and non-positive numerics are rejected with a clear
message naming the file and key, and the process exits 2.
Changes:
- New src/configFile.ts: existence-aware loader + strict schema
validation. Throws CliError; the entry point converts those to exit 2.
- src/config.ts: ConfigOverrides gains 'verbose'; resolveConfig takes
both file and CLI override layers; new defaultConfigPath() honors
XDG_CONFIG_HOME; new resolveVerbose() layers verbose with the
presence-only-CLI semantics documented.
- src/cli.ts: -C/--config <path> flag. printHelp prints the default
config path and the precedence rule.
- src/move.ts: loads the config file (default XDG path or --config),
passes both override layers into resolveConfig, resolves verbose
separately, exits 2 on any validation failure.
- README: new Configuration section with path, precedence, example,
validation rules, and the verbose CLI-can't-turn-off limitation.
Files table gains src/configFile.ts.
This commit is contained in:
+29
-21
@@ -9,6 +9,7 @@
|
||||
*
|
||||
* -h, --help Prints `printHelp()` to stdout; entry exits 0.
|
||||
* -v, --version Prints `move <VERSION>` to stdout; entry exits 0.
|
||||
* -C, --config <path> Override the default config-file path.
|
||||
* -m, --move-interval Idle time (seconds) before a sweep fires.
|
||||
* -c, --check-interval Cursor poll cadence (seconds).
|
||||
* -d, --step-delay Pause between synthetic steps (ms).
|
||||
@@ -16,7 +17,7 @@
|
||||
* -V, --verbose Enable per-sweep / interrupt / bounds logging.
|
||||
* (`-V` capital because `-v` is `--version`.)
|
||||
*
|
||||
* Numeric overrides are layered onto `DEFAULT_CONFIG` by
|
||||
* Numeric overrides are layered (CLI > file > DEFAULT_CONFIG) by
|
||||
* `resolveConfig` in `config.ts`; this module only parses and validates.
|
||||
*/
|
||||
|
||||
@@ -25,23 +26,25 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { DEFAULT_CONFIG } from "./config.ts";
|
||||
import { DEFAULT_CONFIG, defaultConfigPath } from "./config.ts";
|
||||
|
||||
/**
|
||||
* Thrown when CLI input is invalid (unknown option, missing value, bad number).
|
||||
* Distinct from runtime errors so the top-level entry can exit with code 2
|
||||
* (user error) instead of code 1 (runtime failure).
|
||||
* Thrown when CLI input is invalid (unknown option, missing value, bad
|
||||
* number, malformed config file). Distinct from runtime errors so the
|
||||
* entry point can exit with code 2 (user error) instead of code 1
|
||||
* (runtime failure).
|
||||
*/
|
||||
export class CliError extends Error {}
|
||||
|
||||
/**
|
||||
* Result of `parseCliArgs`. Numeric fields are `undefined` when the user did
|
||||
* not supply the flag; this lets `resolveConfig` cleanly distinguish "use the
|
||||
* default" from "explicit override".
|
||||
* Result of `parseCliArgs`. Numeric fields are `undefined` when the user
|
||||
* did not supply the flag; this lets `resolveConfig` cleanly distinguish
|
||||
* "use the layer below" from "explicit override".
|
||||
*/
|
||||
export interface ParsedCliArgs {
|
||||
help: boolean;
|
||||
version: boolean;
|
||||
config: string | undefined;
|
||||
moveInterval: number | undefined; // seconds
|
||||
checkInterval: number | undefined; // seconds
|
||||
stepDelay: number | undefined; // milliseconds
|
||||
@@ -50,10 +53,9 @@ export interface ParsedCliArgs {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CLI-supplied numeric value. Returns `undefined` if the user did
|
||||
* not supply the flag at all; throws `CliError` on anything that isn't a
|
||||
* positive finite number. Zero is rejected: every numeric tunable here is a
|
||||
* duration or count where zero is meaningless or actively broken.
|
||||
* Validate a CLI-supplied numeric value. Returns `undefined` if the user
|
||||
* did not supply the flag at all; throws `CliError` on anything that isn't
|
||||
* a positive finite number.
|
||||
*/
|
||||
function parsePositiveNumber(name: string, raw: string | undefined): number | undefined {
|
||||
if (raw === undefined) return undefined;
|
||||
@@ -66,11 +68,8 @@ function parsePositiveNumber(name: string, raw: string | undefined): number | un
|
||||
|
||||
/**
|
||||
* Parse `process.argv` into a typed `ParsedCliArgs`. Uses Node's built-in
|
||||
* `parseArgs` in strict mode so unknown flags and missing values surface as
|
||||
* `CliError`s that the entry point can turn into exit code 2.
|
||||
*
|
||||
* Numeric flags are stored as `string` by `parseArgs` and then validated by
|
||||
* `parsePositiveNumber`.
|
||||
* `parseArgs` in strict mode so unknown flags and missing values surface
|
||||
* as `CliError`s that the entry point can turn into exit code 2.
|
||||
*/
|
||||
export function parseCliArgs(): ParsedCliArgs {
|
||||
let values: Record<string, string | boolean | undefined>;
|
||||
@@ -80,6 +79,7 @@ export function parseCliArgs(): ParsedCliArgs {
|
||||
options: {
|
||||
help: { type: "boolean", short: "h" },
|
||||
version: { type: "boolean", short: "v" },
|
||||
config: { type: "string", short: "C" },
|
||||
"move-interval": { type: "string", short: "m" },
|
||||
"check-interval": { type: "string", short: "c" },
|
||||
"step-delay": { type: "string", short: "d" },
|
||||
@@ -100,6 +100,7 @@ export function parseCliArgs(): ParsedCliArgs {
|
||||
return {
|
||||
help: Boolean(values.help),
|
||||
version: Boolean(values.version),
|
||||
config: values.config as string | undefined,
|
||||
moveInterval: parsePositiveNumber("move-interval", values["move-interval"] as string | undefined),
|
||||
checkInterval: parsePositiveNumber("check-interval", values["check-interval"] as string | undefined),
|
||||
stepDelay: parsePositiveNumber("step-delay", values["step-delay"] as string | undefined),
|
||||
@@ -111,8 +112,8 @@ export function parseCliArgs(): ParsedCliArgs {
|
||||
/**
|
||||
* Read the package version from `package.json` at runtime so help/version
|
||||
* output stays in sync with the manifest without a build step. Resolved
|
||||
* relative to this module's own location so the lookup works regardless of
|
||||
* the caller's CWD.
|
||||
* relative to this module's own location so the lookup works regardless
|
||||
* of the caller's CWD.
|
||||
*/
|
||||
export const VERSION: string = (() => {
|
||||
// This module lives in `src/`, so `package.json` is one directory up.
|
||||
@@ -123,12 +124,14 @@ export const VERSION: string = (() => {
|
||||
|
||||
/**
|
||||
* Write the usage block to stdout. Default values are pulled from
|
||||
* `DEFAULT_CONFIG` (converted to the units the CLI exposes) so the help
|
||||
* text never drifts from the actual defaults.
|
||||
* `DEFAULT_CONFIG` (converted to the units the CLI exposes); the default
|
||||
* config-file path is computed by `defaultConfigPath`. Both make the help
|
||||
* text self-updating when their sources change.
|
||||
*/
|
||||
export function printHelp(): void {
|
||||
const moveDefaultSec: number = DEFAULT_CONFIG.moveInterval / 1000;
|
||||
const checkDefaultSec: number = DEFAULT_CONFIG.checkInterval / 1000;
|
||||
const cfgPath: string = defaultConfigPath();
|
||||
|
||||
process.stdout.write(`Usage: move [options]
|
||||
|
||||
@@ -138,6 +141,8 @@ nudging the mouse cursor after a configurable idle period.
|
||||
Options:
|
||||
-h, --help Show this help and exit.
|
||||
-v, --version Print version and exit.
|
||||
-C, --config <path> Load defaults from a JSON config file.
|
||||
Default path: ${cfgPath}
|
||||
-m, --move-interval <seconds> Idle time before a sweep fires. Default: ${moveDefaultSec}.
|
||||
-c, --check-interval <seconds> Cursor poll cadence. Default: ${checkDefaultSec}.
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: ${DEFAULT_CONFIG.stepDelay}.
|
||||
@@ -145,9 +150,12 @@ Options:
|
||||
-V, --verbose Log every sweep, interrupt, and bounds event
|
||||
(default prints only the startup banner).
|
||||
|
||||
Precedence (highest wins): CLI flags > config file > built-in defaults.
|
||||
|
||||
Examples:
|
||||
move
|
||||
move --move-interval 180 --check-interval 5
|
||||
move -m 300 -V
|
||||
move --config ~/myprofile.json
|
||||
`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user