From 940113019d13389bb61cdfb0dd8c14ce6c77011c Mon Sep 17 00:00:00 2001 From: nokeo08 Date: Wed, 17 Jun 2026 12:45:46 -0500 Subject: [PATCH] Move verbose into Config; drop resolveVerbose Verbose was awkwardly a separate runKeeper argument with a separate resolveVerbose helper, even though it's just another tunable on the same precedence ladder as the numeric fields. This commit collapses it. Changes: - Config gains 'readonly verbose: boolean'. DEFAULT_CONFIG sets it to false. - resolveConfig now returns the full Config including verbose. Verbose layers with the same 'first defined value wins' precedence as the numeric fields. - resolveVerbose is gone. - ParsedCliArgs.verbose becomes boolean | undefined: undefined when -V was not passed, true when it was. parseCliArgs maps accordingly. This lets the layered resolver treat verbose uniformly. - runKeeper takes a single Config arg. The internal logger reads from config.verbose at the top of runKeeper. - move.ts no longer plumbs verbose separately; the single resolved Config drives everything. - README How-it-works and Files-table entries updated to match. Behavior verified for all five verbose-precedence cases (no file/no flag, no file/-V, file:true/no flag, file:false/-V, file:false/no flag) and config-error scenarios remain intact. --- README.md | 10 ++++---- src/cli.ts | 10 ++++++-- src/config.ts | 65 +++++++++++++++++++++++---------------------------- src/keeper.ts | 10 ++++---- src/move.ts | 9 +++---- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0ced309..540e58c 100644 --- a/README.md +++ b/README.md @@ -166,13 +166,13 @@ modules: - `src/move.ts` is a thin entry point: parses args, dispatches `--help` / `--version`, loads the config file, resolves the layered runtime - config, and calls `runKeeper(config, verbose)`. + config, and calls `runKeeper(config)`. - `src/cli.ts` owns argument parsing, validation, and help/version output. - `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/config.ts` exports the `Config` type (which carries every tunable + including `verbose`), `DEFAULT_CONFIG`, `defaultConfigPath`, and the + layered `resolveConfig` overlay function. - `src/keeper.ts` owns the synthetic-activity sweep and the idle-watch loop. Defaults live in `src/config.ts` as `DEFAULT_CONFIG`: @@ -257,7 +257,7 @@ 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`, `defaultConfigPath`, and the layered `resolveConfig` / `resolveVerbose` overlay. | +| `src/config.ts` | `Config` type (carries every tunable, including `verbose`), `DEFAULT_CONFIG`, `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`. | diff --git a/src/cli.ts b/src/cli.ts index f45c002..bf95382 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -49,7 +49,13 @@ export interface ParsedCliArgs { checkInterval: number | undefined; // seconds stepDelay: number | undefined; // milliseconds stepCount: number | undefined; // pixels - verbose: boolean; + /** + * `true` when `-V`/`--verbose` was passed; `undefined` when it was not. + * `undefined` (not `false`) lets the layered resolver distinguish "user + * did not specify" from a hypothetical "user explicitly turned off", + * even though the CLI has no off-switch today. + */ + verbose: boolean | undefined; } /** @@ -105,7 +111,7 @@ export function parseCliArgs(): ParsedCliArgs { checkInterval: parsePositiveNumber("check-interval", values["check-interval"] as string | undefined), stepDelay: parsePositiveNumber("step-delay", values["step-delay"] as string | undefined), stepCount: parsePositiveNumber("step-count", values["step-count"] as string | undefined), - verbose: Boolean(values.verbose), + verbose: values.verbose === true ? true : undefined, }; } diff --git a/src/config.ts b/src/config.ts index 218c0df..800f9de 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,16 +4,16 @@ * Runtime configuration types, defaults, the layered resolver, and the * default config-file path. * - * The keeper is driven by a single `Config` object that carries the four - * tunables it cares about. Defaults live in `DEFAULT_CONFIG`; CLI and - * config-file overrides are layered on top by `resolveConfig` rather than - * mutating the defaults, so the defaults stay genuinely constant and the - * resolved config stays structurally typed. + * The keeper is driven by a single `Config` object that carries every + * tunable it cares about, including the `verbose` flag. Defaults live in + * `DEFAULT_CONFIG`; CLI and config-file overrides are layered on top by + * `resolveConfig` rather than mutating the defaults, so the defaults stay + * genuinely constant and the resolved config stays structurally typed. * - * All `Config` fields are in their internal units (ms, pixels). The CLI and - * config file expose the time-valued fields in seconds for ergonomics; - * `resolveConfig` performs the seconds->ms conversion at the boundary so - * downstream code never has to think about it. + * All numeric `Config` fields are in their internal units (ms, pixels). + * The CLI and config file expose the time-valued fields in seconds for + * ergonomics; `resolveConfig` performs the seconds->ms conversion at the + * boundary so downstream code never has to think about it. * * Layering precedence (highest wins): * CLI overrides > file overrides > DEFAULT_CONFIG @@ -33,12 +33,15 @@ import { join } from "node:path"; * a sweep. Also the window in which the user can * "interrupt" by moving the cursor. Milliseconds. * - `stepCount` — number of pixel-steps in a single sweep. Pixels. + * - `verbose` — whether per-sweep / interrupt / bounds events are + * logged. The startup banner is always printed. */ export interface Config { readonly moveInterval: number; readonly checkInterval: number; readonly stepDelay: number; readonly stepCount: number; + readonly verbose: boolean; } /** @@ -50,6 +53,7 @@ export const DEFAULT_CONFIG: Config = { 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 }; /** @@ -63,9 +67,14 @@ export const DEFAULT_CONFIG: Config = { * stepDelay — milliseconds * stepCount — pixels * - * `verbose` is included so both layers can share a single shape, even - * though `verbose` isn't part of `Config` itself (it's plumbed through to - * `runKeeper` as a separate parameter). + * `verbose` is `boolean | undefined` like the numeric fields, so all five + * fields share the same "first defined value wins" precedence logic. + * + * For the CLI specifically, `verbose` is `undefined` when `-V/--verbose` + * was not passed and `true` when it was. There is no CLI off-switch + * today, so CLI `false` doesn't occur — a file-set `verbose: true` cannot + * be overridden back to false from the command line (see the Configuration + * section of the README). */ export interface ConfigOverrides { readonly moveInterval: number | undefined; @@ -96,8 +105,8 @@ export function defaultConfigPath(): string { /** * Overlay file overrides (lowest priority) and CLI overrides (highest) * on top of `DEFAULT_CONFIG` and return a resolved `Config`. Time-valued - * inputs are in seconds; this is where they're converted to milliseconds - * for internal use. + * numeric inputs are in seconds; this is where they're converted to + * milliseconds for internal use. * * For each field, the first layer that supplies a defined value wins: * CLI -> file -> DEFAULT_CONFIG @@ -113,11 +122,11 @@ export function resolveConfig(file: ConfigOverrides | null, cli: ConfigOverrides return fallbackMs; }; - const pickRaw = ( - cliVal: number | undefined, - fileVal: number | undefined, - fallback: number, - ): number => { + const pickRaw = ( + cliVal: T | undefined, + fileVal: T | undefined, + fallback: T, + ): T => { if (cliVal !== undefined) return cliVal; if (fileVal !== undefined) return fileVal; return fallback; @@ -128,22 +137,6 @@ export function resolveConfig(file: ConfigOverrides | null, cli: ConfigOverrides checkInterval: pickSeconds(cli.checkInterval, file?.checkInterval, DEFAULT_CONFIG.checkInterval), stepDelay: pickRaw(cli.stepDelay, file?.stepDelay, DEFAULT_CONFIG.stepDelay), stepCount: pickRaw(cli.stepCount, file?.stepCount, DEFAULT_CONFIG.stepCount), + verbose: pickRaw(cli.verbose, file?.verbose, DEFAULT_CONFIG.verbose), }; } - -/** - * Resolve the effective `verbose` flag from the same layered overrides. - * - * `--verbose` on the CLI is a presence-only switch (no `--no-verbose`), - * so this is strictly an "or-up" merge: CLI true wins outright, otherwise - * the file's value (or false if absent) is used. - * - * Known limitation: a user with `"verbose": true` in their config file - * cannot force quiet mode from the CLI. They must edit the file (or use - * `--config` to point at a different one). - */ -export function resolveVerbose(file: ConfigOverrides | null, cli: ConfigOverrides): boolean { - if (cli.verbose === true) return true; - if (file?.verbose === true) return true; - return false; -} diff --git a/src/keeper.ts b/src/keeper.ts index 659a628..f97252c 100644 --- a/src/keeper.ts +++ b/src/keeper.ts @@ -11,9 +11,9 @@ * Logging policy: * - The startup banner in `runKeeper` is unconditional so the user always * sees confirmation that the process is alive. - * - Every per-sweep / interrupt / bounds log is gated by `verbose` so the - * default is quiet. Errors stay on `console.error` (unconditional, raised - * by the entry point on unhandled rejection). + * - Every per-sweep / interrupt / bounds log is gated by `config.verbose` + * so the default is quiet. Errors stay on `console.error` (unconditional, + * raised by the entry point on unhandled rejection). */ import { mouse, Point, screen } from "@nut-tree-fork/nut-js"; @@ -144,8 +144,8 @@ async function simulateActivity(config: Config, log: ReturnType { - const log = makeLogger(verbose); +export async function runKeeper(config: Config): Promise { + const log = makeLogger(config.verbose); log.info("Teams Status Keeper started. Press Ctrl+C to stop."); let lastPos: Point = await mouse.getPosition(); diff --git a/src/move.ts b/src/move.ts index bde3f4e..e62b1ff 100755 --- a/src/move.ts +++ b/src/move.ts @@ -15,7 +15,9 @@ * 2. `--help` / `--version` short-circuit before any I/O or mouse work. * 3. Load + validate the config file (default XDG path, or `--config * ` if supplied). Validation failures share the exit-2 path. - * 4. Resolve config (CLI > file > DEFAULT_CONFIG) and the verbose flag. + * 4. Resolve the full `Config` (CLI > file > DEFAULT_CONFIG) — verbose + * lives inside `Config` and is layered with the same precedence as + * the numeric fields. * 5. Run keeper. Any unhandled rejection exits 1. * * Runtime: Bun (uses `@nut-tree-fork/nut-js` via `keeper.ts`). The shebang @@ -24,7 +26,7 @@ import { parseCliArgs, printHelp, VERSION } from "./cli.ts"; import type { ParsedCliArgs } from "./cli.ts"; -import { resolveConfig, resolveVerbose } from "./config.ts"; +import { resolveConfig } from "./config.ts"; import type { ConfigOverrides } from "./config.ts"; import { loadConfigFile } from "./configFile.ts"; import { runKeeper } from "./keeper.ts"; @@ -71,9 +73,8 @@ const cliOverrides: ConfigOverrides = { }; const config = resolveConfig(fileOverrides, cliOverrides); -const verbose: boolean = resolveVerbose(fileOverrides, cliOverrides); -runKeeper(config, verbose).catch((err: unknown): void => { +runKeeper(config).catch((err: unknown): void => { console.error("Error:", err); process.exit(1); });