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:
2026-06-17 12:27:50 -05:00
parent c9645b374c
commit df9305c6ac
5 changed files with 379 additions and 76 deletions
+68 -6
View File
@@ -95,17 +95,78 @@ out-of-bounds events.
Invalid input (unknown flag, missing value, non-positive number) prints an
error to `stderr` and exits with code `2`.
## Configuration
`move` reads an optional JSON config file at:
```
${XDG_CONFIG_HOME:-$HOME/.config}/move/config.json
```
If the file is missing, defaults are used (no config file is required).
Pass `-C` / `--config <path>` to point at a different file; in that mode
the file must exist.
### Precedence
```
CLI flags > config file > built-in defaults
```
CLI flags always win. The config file fills in any flag the user didn't
pass on the command line. Built-in defaults fill in anything the file
doesn't set.
### Example
```jsonc
{
"moveInterval": 240,
"checkInterval": 10,
"stepDelay": 50,
"stepCount": 250,
"verbose": false
}
```
All keys are optional; supply only the ones you want to override. Keys
and units mirror the CLI flags exactly: `moveInterval` and
`checkInterval` are seconds, `stepDelay` is milliseconds, `stepCount` is
pixels, `verbose` is a boolean.
### Validation
The loader is strict:
- Root must be a JSON object.
- Unknown keys are rejected (catches typos like `"movInterval"`).
- Numeric values must be finite and strictly positive.
- `verbose` must be a boolean.
Any validation failure prints a message naming the file and the offending
key to `stderr` and exits `2`.
### Known limitation: `verbose` can be turned on but not off from the CLI
`--verbose` is a presence-only flag (there is no `--no-verbose`). If the
config file sets `"verbose": true`, the CLI cannot force quiet mode in
that invocation. Workarounds: edit the file, or point at a different
file with `--config`.
## How it works
The source lives under `src/`, split into an entry point plus three logic
The source lives under `src/`, split into an entry point plus four logic
modules:
- `src/move.ts` is a thin entry point: parses args, dispatches `--help` /
`--version`, resolves the runtime config, and calls
`runKeeper(config, verbose)`.
`--version`, loads the config file, resolves the layered runtime
config, and calls `runKeeper(config, verbose)`.
- `src/cli.ts` owns argument parsing, validation, and help/version output.
- `src/config.ts` exports the `Config` type, `DEFAULT_CONFIG`, and the
`resolveConfig` overlay function.
- `src/configFile.ts` owns optional JSON config-file loading + strict
schema validation.
- `src/config.ts` exports the `Config` type, `DEFAULT_CONFIG`,
`defaultConfigPath`, and the layered `resolveConfig` / `resolveVerbose`
overlay functions.
- `src/keeper.ts` owns the synthetic-activity sweep and the idle-watch loop.
Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
@@ -190,7 +251,8 @@ move --help
| `scripts/dev-setup.sh` | Contributor bootstrap (verify Bun + `bun 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, `DEFAULT_CONFIG`, and `resolveConfig` overlay. |
| `src/config.ts` | `Config` type, `DEFAULT_CONFIG`, `defaultConfigPath`, and the layered `resolveConfig` / `resolveVerbose` 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`. |
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |