Tighter type-checking: array indexing and dynamic property access now
return T | undefined rather than T. Catches bugs where code assumes a
key exists without checking.
No code changes were needed — the existing modules already either:
- cast through Record<string, unknown> (configFile.ts), where access
is already 'unknown';
- cast values from parseArgs to '... | undefined' (cli.ts), already
nullable; or
- use static, statically-known fields (Config, ConfigOverrides,
DEFAULT_CONFIG).
tsc --noEmit remains clean. The full test suite still passes.
The contributor bootstrap script was bash-only ('#!/usr/bin/env bash',
'set -euo pipefail', '[[ ... == ... ]]') while install.sh and
uninstall.sh are POSIX sh. Consistency lines them up:
- shebang -> '#!/usr/bin/env sh'
- 'set -euo pipefail' -> 'set -eu' (pipefail isn't POSIX; not needed
here either, no risky pipelines in this script)
- '[[ ... == ... ]]' -> '[ ... = ... ]'
Also added a 'bun run test' hint to the post-install workflow note now
that there's a test suite to run.
Two new test files exercise the layered config resolver and the JSON
config-file loader:
src/config.test.ts 25 cases:
- resolveConfig precedence (CLI > file > default) per field
- seconds-to-ms conversion at the resolver boundary
- verbose precedence across all four cells
- defaultConfigPath: XDG honored, HOME fallback, empty-as-unset,
throws CliError when both are missing
src/configFile.test.ts 11 cases:
- valid file -> overrides, with undefined for unspecified keys
- missing explicit path throws
- malformed JSON / non-object root throws with file path
- unknown key / wrong type / non-positive number all throw with
the offending key named
- default path missing -> null (silent default)
Runs via 'bun test' (or 'bun run test', now wired in package.json).
All 25 tests pass on the current codebase.
The two stderr-fatal paths used to be asymmetric: failUser() is a named
helper with 'move:' prefix and a 'Try --help' hint, while the runKeeper
catch was an inline 'console.error("Error:", err)' + process.exit(1).
failRuntime() now sits next to failUser(), so the entry-point reads as
'one of two well-named fatal paths.' It also prefers err.stack when
available, giving better diagnostics for nut.js / Accessibility-permission
failures than the previous formatter.
Same observable behavior on the success path; failure messages on the
runtime path are now more debuggable.
Replaces 'ReturnType<typeof makeLogger>' with a small named interface so
simulateActivity's signature reads as (config, log: Logger) instead of a
type-derivation chain. Also makes it cleaner to substitute a mock logger
if simulateActivity is ever unit-tested.
No behavior change.
The assignment used to run at import time, mutating the shared nut.js
singleton the second anything imported keeper.ts. Moved into the top
of runKeeper(), where the only caller actually needs it.
Same observable behavior — runKeeper is called exactly once per process
— but keeper.ts is now import-side-effect-free, which makes it
straightforward to import for tests or future tooling without touching
global mouse state.
CliError used to live in cli.ts and was imported by configFile.ts purely
to grab a one-line class — the dependency arrow said 'config-file loader
depends on the CLI parser' when really it just needed a shared error
type.
Moving CliError to its own errors.ts module flattens the graph:
errors.ts (no internal deps)
|
+-- cli.ts
+-- configFile.ts
+-- config.ts (will use it in the next commit)
cli.ts re-exports CliError for any consumer that prefers to keep
importing it from there; the canonical home is now errors.ts.
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.
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.
The static usage example in README.md was added in milestone 1 and
hand-maintained. When -C/--config landed in the JSON-config-file
commit, printHelp() was updated but this block wasn't. This commit
brings them back in sync and adds the precedence line.
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.
- install.sh, uninstall.sh, dev-setup.sh now live under scripts/.
- dev-setup.sh's 'cd $(dirname $0)' updated to '.../..' so it lands
at the repo root regardless of CWD.
- README install/uninstall curl URLs now reference scripts/<name>.sh.
- README contributor section and Files table updated to match.
Behavior is unchanged. The curl one-liner URL changes from
.../master/install.sh to .../master/scripts/install.sh; users following
the pre-v1.0.2 README will get a 404 from the old URL.
End-user installation collapses to a single command:
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh
Changes:
- install.sh rewritten as a POSIX sh, curl-pipeable end-user installer.
Honors XDG_DATA_HOME and XDG_BIN_HOME (de facto). Idempotent via a
version-marker file at $INSTALL_DIR/.installed-version; MOVE_FORCE=1
overrides. Hard-fails if Bun is missing (no auto-install). Includes
a safety guard refusing rm -rf on too-broad install dirs.
- uninstall.sh added; same curl-pipe pattern, shared XDG path
resolution, leaves Bun alone.
- dev-setup.sh added (= the previous install.sh content, retitled for
contributors and pointing at bun run start / bun link / install.sh).
- README updated: curl one-liner Install section, XDG behavior tables,
new Uninstall section, new 'For contributors' section, Files table
rows for all three scripts.
The plan is being implemented in the following commit. The working-notes
document has served its purpose and is removed to keep master focused
on shipping artifacts.
- src/move.ts entry point with CLI parsing, --help, --version
- src/cli.ts: parseCliArgs, printHelp, ParsedCliArgs, CliError, VERSION
- src/config.ts: Config type, DEFAULT_CONFIG, resolveConfig
- src/keeper.ts: synthetic-activity sweep + idle-watch loop
- package.json bin entry + shebang for 'bun link' global install
- install.sh: contributor bootstrap (will be repurposed; see
DISTRIBUTION-PLAN.md for the end-user installer design)
- DISTRIBUTION-PLAN.md captures the tabled end-user distribution work