diff --git a/src/cli.ts b/src/cli.ts index cb67e06..ecd3c50 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1672,7 +1672,8 @@ async function main(): Promise { case "completions": { // `pty completions ` — print a fish/bash/zsh completion script. const { cmdCompletions } = await import("./completions.ts"); - process.exit(cmdCompletions(args.slice(1))); + process.exitCode = cmdCompletions(args.slice(1)); + return; } case "version": diff --git a/tests/completions.test.ts b/tests/completions.test.ts index b3e66dc..d1e1a6e 100644 --- a/tests/completions.test.ts +++ b/tests/completions.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect } from "vitest"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { spawnSync } from "node:child_process"; import { execSync } from "node:child_process"; import { COMMANDS } from "../src/completions.ts"; @@ -186,6 +186,26 @@ printf '%s\n' "\${COMPREPLY[@]}"`; } }); + it("lets piped stdout drain before exiting", () => { + const delayedStdout = pathToFileURL( + path.join(__dirname, "fixtures", "delayed-stdout.mjs"), + ).href; + for (const shell of ["fish", "bash", "zsh"]) { + const r = spawnSync( + nodeBin, + ["--import", delayedStdout, cliPath, "completions", shell], + { encoding: "utf8" }, + ); + const checkedIn = fs.readFileSync( + path.join(__dirname, "..", "completions", `pty.${shell}`), + "utf8", + ); + + expect(r.status, r.stderr).toBe(0); + expect(r.stdout, `${shell} output was truncated`).toBe(checkedIn); + } + }); + it("prints usage and exits non-zero for an unknown shell", () => { const r = spawnSync(nodeBin, [cliPath, "completions", "tcsh"], { encoding: "utf8", diff --git a/tests/fixtures/delayed-stdout.mjs b/tests/fixtures/delayed-stdout.mjs new file mode 100644 index 0000000..7dbdfb6 --- /dev/null +++ b/tests/fixtures/delayed-stdout.mjs @@ -0,0 +1,10 @@ +// The natural race depends on the Node version, operating system, and pipe +// scheduling, so it does not reproduce on every supported setup. Delay the +// write so a forced exit loses the text on every platform, independent of +// pipe size or scheduling luck. +const write = process.stdout.write.bind(process.stdout); + +process.stdout.write = (chunk, encoding, callback) => { + setTimeout(() => write(chunk, encoding, callback), 25); + return false; +};