diff --git a/src/cli.ts b/src/cli.ts index bf95382..fb992d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -27,14 +27,7 @@ import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import { DEFAULT_CONFIG, defaultConfigPath } from "./config.ts"; - -/** - * Thrown when CLI input is invalid (unknown option, missing value, bad - * number, malformed config file). Distinct from runtime errors so the - * entry point can exit with code 2 (user error) instead of code 1 - * (runtime failure). - */ -export class CliError extends Error {} +import { CliError } from "./errors.ts"; /** * Result of `parseCliArgs`. Numeric fields are `undefined` when the user diff --git a/src/config.ts b/src/config.ts index f6a0f70..616dab7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,8 @@ import { join } from "node:path"; +import { CliError } from "./errors.ts"; + // Single source of truth for default values. The same file ships in the // install tree and is copied to $XDG_CONFIG_HOME/move/config.json on a // fresh install (only if no config exists there yet). Values use the CLI @@ -141,7 +143,17 @@ export function defaultConfigPath(): string { if (xdg && xdg.length > 0) { return join(xdg, "move", "config.json"); } - const home = process.env.HOME ?? ""; + const home = process.env.HOME; + if (!home || home.length === 0) { + // Neither var is set; we don't have a sensible fallback. Throwing + // CliError lets the entry-point's normal handler surface this as + // 'move: cannot resolve default config path: ...' + exit 2 instead + // of silently producing '/.config/move/config.json' and bewildering + // the user with a downstream 'file not found' message. + throw new CliError( + "cannot resolve default config path: neither $XDG_CONFIG_HOME nor $HOME is set", + ); + } return join(home, ".config", "move", "config.json"); } diff --git a/src/configFile.ts b/src/configFile.ts index cf06be4..3a47c66 100644 --- a/src/configFile.ts +++ b/src/configFile.ts @@ -27,8 +27,8 @@ import { existsSync, readFileSync, statSync } from "node:fs"; -import { CliError } from "./cli.ts"; import { defaultConfigPath, type ConfigOverrides } from "./config.ts"; +import { CliError } from "./errors.ts"; const ALLOWED_KEYS: ReadonlySet = new Set([ "moveInterval", diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..28ab339 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,16 @@ +/** + * errors.ts + * --------- + * Cross-cutting error types used by parsers, loaders, and config resolution. + * Kept in its own module so feature modules can import error types without + * pulling in unrelated implementation code (e.g., the JSON loader doesn't + * need to depend on the CLI parser just to throw a typed error). + */ + +/** + * Thrown when user-supplied input is invalid: unknown CLI option, missing + * value, non-positive number, malformed config file, unresolvable default + * path, etc. Distinct from runtime errors so the top-level entry can exit + * with code 2 (user error) instead of code 1 (runtime failure). + */ +export class CliError extends Error {} diff --git a/src/move.ts b/src/move.ts index e62b1ff..ef9c2ab 100755 --- a/src/move.ts +++ b/src/move.ts @@ -49,7 +49,13 @@ try { } if (cliArgs.help) { - printHelp(); + // printHelp() resolves defaultConfigPath(), which can throw CliError + // when neither $XDG_CONFIG_HOME nor $HOME is set. + try { + printHelp(); + } catch (err: unknown) { + failUser(err); + } process.exit(0); } if (cliArgs.version) {