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 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-15 14:19:38 -04:00
1 parent 3a50f57b66
commit 0dfeca8a50
2 files changed
+32 -4

No files matched your search

+11 -4
View File
@@ -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);
}
+21
View File
@@ -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-"));