The stepCount and stepSize knobs were two controls for one quantity users actually care about (reach), and the number of steps is an implementation detail nobody meaningfully tunes. Each pattern has a natural size and resolution — a jitter is inherently small, an arc a broad curve — so those now live as constants in each strategy rather than as global config. - strategies.ts: each pattern defines its own step count and size; MoveContext drops `config` down to pure geometry (start/width/height/rng), and the module no longer imports Config at all (dissolving the type-only-import cycle workaround). line stays byte-for-byte: 250 one-pixel steps. - executor.ts: executePath takes `config` for pacing (stepDelay); the path itself needs nothing from it. - config.ts / cli.ts / move.ts / config.default.json: drop stepCount and stepSize from the type, seed, validation, resolver, CLI flags (-n, -s), and help. stepDelay stays as the one pacing lever. - configFile.ts: tolerate the removed keys instead of rejecting them — every pre-1.3.0 install seeded stepCount, so a hard "unknown key" failure on upgrade is avoided. They're ignored with a one-line stderr notice; genuine unknown keys still error. The -n/--step-count CLI flag (shipped since 1.0.0) is now an unknown option; config files degrade gracefully, command lines don't. Stays in the unpushed 1.3.0 release. 64 tests pass.
187 lines
8.1 KiB
TypeScript
187 lines
8.1 KiB
TypeScript
/**
|
|
* cli.ts
|
|
* ------
|
|
* Command-line argument parsing, validation, and help/version output for
|
|
* the `move` CLI. Pure module: no side effects on import beyond reading
|
|
* `package.json` once to populate `VERSION`.
|
|
*
|
|
* Flag table:
|
|
*
|
|
* -h, --help Prints `printHelp()` to stdout; entry exits 0.
|
|
* -v, --version Prints `move <VERSION>` to stdout; entry exits 0.
|
|
* -e, --edit Open the active config file in `$EDITOR`.
|
|
* Refuses if the file doesn't exist; refuses if
|
|
* `$EDITOR` is unset.
|
|
* -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).
|
|
* -p, --pattern Movement strategy name (see strategies.ts).
|
|
* -V, --verbose Enable per-sweep / interrupt / bounds logging.
|
|
* (`-V` capital because `-v` is `--version`.)
|
|
*
|
|
* Numeric overrides are layered (CLI > file > DEFAULT_CONFIG) by
|
|
* `resolveConfig` in `config.ts`; this module only parses and validates.
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
|
|
import { DEFAULT_CONFIG, defaultConfigPath } from "./config.ts";
|
|
import { CliError } from "./errors.ts";
|
|
import { PATTERN_NAMES, resolvePatternName } from "./strategies.ts";
|
|
|
|
/**
|
|
* 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;
|
|
edit: boolean;
|
|
config: string | undefined;
|
|
moveInterval: number | undefined; // seconds
|
|
checkInterval: number | undefined; // seconds
|
|
stepDelay: number | undefined; // milliseconds
|
|
/** Movement strategy name, validated against the registry. */
|
|
pattern: string | undefined;
|
|
/**
|
|
* `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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
const n: number = Number(raw);
|
|
if (!Number.isFinite(n) || n <= 0) {
|
|
throw new CliError(`invalid value for --${name}: '${raw}' (expected a positive number)`);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Validate a CLI-supplied movement-pattern name. Returns `undefined` when
|
|
* the flag was not supplied; throws `CliError` naming the valid patterns
|
|
* when the value isn't a registered strategy.
|
|
*/
|
|
function parsePatternName(raw: string | undefined): string | undefined {
|
|
if (raw === undefined) return undefined;
|
|
const canonical: string | null = resolvePatternName(raw);
|
|
if (canonical === null) {
|
|
throw new CliError(`invalid value for --pattern: '${raw}' (valid: ${PATTERN_NAMES.join(", ")})`);
|
|
}
|
|
return canonical;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export function parseCliArgs(): ParsedCliArgs {
|
|
let values: Record<string, string | boolean | undefined>;
|
|
try {
|
|
const result = parseArgs({
|
|
args: process.argv.slice(2),
|
|
options: {
|
|
help: { type: "boolean", short: "h" },
|
|
version: { type: "boolean", short: "v" },
|
|
edit: { type: "boolean", short: "e" },
|
|
config: { type: "string", short: "C" },
|
|
"move-interval": { type: "string", short: "m" },
|
|
"check-interval": { type: "string", short: "c" },
|
|
"step-delay": { type: "string", short: "d" },
|
|
pattern: { type: "string", short: "p" },
|
|
verbose: { type: "boolean", short: "V" },
|
|
},
|
|
strict: true,
|
|
allowPositionals: false,
|
|
});
|
|
values = result.values as Record<string, string | boolean | undefined>;
|
|
} catch (err: unknown) {
|
|
// node:util throws TypeError for unknown options / missing values;
|
|
// surface its message verbatim so the user sees exactly what was wrong.
|
|
const msg: string = err instanceof Error ? err.message : String(err);
|
|
throw new CliError(msg);
|
|
}
|
|
|
|
return {
|
|
help: Boolean(values.help),
|
|
version: Boolean(values.version),
|
|
edit: Boolean(values.edit),
|
|
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),
|
|
pattern: parsePatternName(values.pattern as string | undefined),
|
|
verbose: values.verbose === true ? true : undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export const VERSION: string = (() => {
|
|
// This module lives in `src/`, so `package.json` is one directory up.
|
|
const here: string = dirname(fileURLToPath(import.meta.url));
|
|
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")) as { version: string };
|
|
return pkg.version;
|
|
})();
|
|
|
|
/**
|
|
* Write the usage block to stdout. Default values are pulled from
|
|
* `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]
|
|
|
|
Keeps presence-tracking apps (e.g. Microsoft Teams) from going Away by
|
|
nudging the mouse cursor after a configurable idle period.
|
|
|
|
Options:
|
|
-h, --help Show this help and exit.
|
|
-v, --version Print version and exit.
|
|
-e, --edit Open the config file in $EDITOR 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}.
|
|
-p, --pattern <name> Movement strategy. Default: ${DEFAULT_CONFIG.pattern}.
|
|
One of: ${PATTERN_NAMES.join(", ")}.
|
|
Each pattern defines its own size and speed.
|
|
-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 --pattern arc
|
|
move --config ~/myprofile.json
|
|
`);
|
|
}
|