diff --git a/src/move.ts b/src/move.ts index 39ef3fd..d9800a0 100755 --- a/src/move.ts +++ b/src/move.ts @@ -12,13 +12,17 @@ * * Order of operations: * 1. Parse CLI args. Bad input -> stderr + usage hint, exit 2. - * 2. `--help` / `--version` short-circuit before any I/O or mouse work. + * 2. `--help` / `--version` short-circuit before any I/O, config load, or + * mouse work. `keeper.ts` is also lazy-imported (see below) so these + * flags don't pay the cost of loading the nut.js native binary. * 3. Load + validate the config file (default XDG path, or `--config * ` if supplied). Validation failures share the exit-2 path. * 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. + * 5. Lazy-import `keeper.ts` (dynamic import keeps nut.js out of the + * `--help` / `--version` startup path) and run it. Any unhandled + * rejection — from the import itself or from the loop — exits 1. * * Runtime: Bun (uses `@nut-tree-fork/nut-js` via `keeper.ts`). The shebang * above lets this file run as a real CLI once linked via `bun link`. @@ -29,7 +33,13 @@ import type { ParsedCliArgs } from "./cli.ts"; import { resolveConfig } from "./config.ts"; import type { ConfigOverrides } from "./config.ts"; import { loadConfigFile } from "./configFile.ts"; -import { runKeeper } from "./keeper.ts"; + +// `keeper.ts` is intentionally NOT statically imported here. It transitively +// pulls in `@nut-tree-fork/nut-js`, which in turn dlopens a sizeable native +// `.node` binary. On a cold first run that load dominates startup (~1 s on +// macOS). For `--help` and `--version` we never actually need nut.js, so we +// defer the import to the only branch that actually runs the keeper loop. +// See the dynamic `await import("./keeper.ts")` near the bottom of the file. /** * Print a user-error message and exit 2. Used for anything that comes from @@ -98,4 +108,14 @@ const cliOverrides: ConfigOverrides = { const config = resolveConfig(fileOverrides, cliOverrides); -runKeeper(config).catch(failRuntime); +// Dynamic import so nut.js and the rest of the keeper machinery aren't +// loaded for invocations that exit early (--help, --version, validation +// failures). The `try` covers import-time failures too (e.g., a missing +// nut.js native binary), routing them through the same runtime-failure +// path as anything raised by the loop itself. +try { + const { runKeeper } = await import("./keeper.ts"); + runKeeper(config).catch(failRuntime); +} catch (err: unknown) { + failRuntime(err); +}