From 0dfeca8a50fd705ee22ddeb800080d72c93318ec Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:19:38 -0400 Subject: [PATCH] fix: a bare seedpass-js printed help and then reported a bogus error exitOverride() makes commander throw for everything it would otherwise exit on -- displaying help and its own usage errors included. The handler only recognised two of its codes, so running `seedpass-js` with no arguments printed the help and followed it with `error: (outputHelp)`, and unknown commands had their message printed twice. Any commander.* code is now commander's business: honour its exit code and let it own the message. SeedPass's own errors still print as before. Found by running the command. Co-Authored-By: Claude Fable 5 --- js/packages/cli/src/main.ts | 15 +++++++++++---- js/packages/cli/test/cli.test.ts | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/js/packages/cli/src/main.ts b/js/packages/cli/src/main.ts index 2d5bb6b6..8074366d 100644 --- a/js/packages/cli/src/main.ts +++ b/js/packages/cli/src/main.ts @@ -10,12 +10,19 @@ program.version(version, "-v, --version", "print the seedpass-js version"); try { await program.parseAsync(process.argv); } catch (e) { - // commander's exitOverride throws for --help/--version too; those carry - // exitCode 0 and must not be reported as errors. const err = e as { exitCode?: number; code?: string; message?: string }; - if (err.code === "commander.helpDisplayed" || err.code === "commander.version") { - process.exit(0); + + // exitOverride() makes commander throw for everything it would otherwise + // exit on — including displaying help or the version, and its own usage + // errors, which it has already written to stderr. Re-reporting those as + // "error: ..." is wrong twice over: a bare `seedpass-js` printed the help + // and then claimed to have failed with "(outputHelp)". Honour commander's + // exit code, but let it own the message. + if (typeof err.code === "string" && err.code.startsWith("commander.")) { + process.exit(typeof err.exitCode === "number" ? err.exitCode : 0); } + + // Anything else is ours, and the message is the useful part. process.stderr.write(`error: ${err.message ?? String(e)}\n`); process.exit(typeof err.exitCode === "number" && err.exitCode !== 0 ? err.exitCode : 1); } diff --git a/js/packages/cli/test/cli.test.ts b/js/packages/cli/test/cli.test.ts index a529ac2c..fbbfff9f 100644 --- a/js/packages/cli/test/cli.test.ts +++ b/js/packages/cli/test/cli.test.ts @@ -434,6 +434,27 @@ describe("document import/export", () => { }); }); +describe("exit behaviour", () => { + it("does not report help output as an error", async () => { + // exitOverride() makes commander throw when it prints help, so a bare + // invocation printed the help and then claimed to have failed with + // "(outputHelp)". + const out: string[] = []; + const err: string[] = []; + const io: ProgramIo = { out: (l) => out.push(l), err: (l) => err.push(l) }; + let thrown: { code?: string; exitCode?: number } | undefined; + try { + await buildProgram(io).parseAsync(["node", "seedpass-js"]); + } catch (e) { + thrown = e as { code?: string; exitCode?: number }; + } + // commander signals help with its own code; main.ts must treat any + // commander.* code as commander's business, not as a SeedPass error. + expect(thrown?.code).toMatch(/^commander\./); + expect(err.join("\n")).not.toContain("outputHelp"); + }); +}); + describe("error messages", () => { it("explains a corrupt or foreign vault file instead of leaking a codec error", async () => { const dir = await mkdtemp(join(tmpdir(), "seedpass-corrupt-"));