From a7dc53459831c6abdaab765f8e23860828d521bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ChangZhuo=20Chen=20=28=E9=99=B3=E6=98=8C=E5=80=AC=29?= Date: Sun, 16 Aug 2026 14:38:29 +0800 Subject: [PATCH] fix(cli): exit cleanly after printing shell completions Force process termination via `postmortem.quit(0)` in `Completions.run()` after writing completion scripts to stdout. Loading all command modules during completion generation leaves open event loop handles (sockets, timers) that prevent natural process exit, causing tools like chezmoi to hang. --- packages/coding-agent/CHANGELOG.md | 4 ++++ .../coding-agent/src/commands/completions.ts | 3 ++- .../test/cli-completions-exit.test.ts | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/test/cli-completions-exit.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4901d84f7..0680ca84f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `omp completions ` hanging after writing shell completion scripts to stdout by invoking `postmortem.quit(0)` upon completion. Prevents lingering event loop handles (such as background timers or sockets loaded when inspecting command metadata) from pinning the process and blocking tools like `chezmoi`. + ## [17.3.5] - 2026-08-16 ### Added diff --git a/packages/coding-agent/src/commands/completions.ts b/packages/coding-agent/src/commands/completions.ts index e7182b7ae..8c1d8202e 100644 --- a/packages/coding-agent/src/commands/completions.ts +++ b/packages/coding-agent/src/commands/completions.ts @@ -5,7 +5,7 @@ * (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface. */ -import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils"; +import { APP_NAME, postmortem, VERSION } from "@oh-my-pi/pi-utils"; import { Args, type CliConfig, Command, type CommandCtor } from "@oh-my-pi/pi-utils/cli"; import { completionsHelp as commandHelp } from "../cli/command-help"; import { buildSpec, generateCompletion, type Shell } from "../cli/completion-gen"; @@ -55,6 +55,7 @@ export default class Completions extends Command { } await Bun.write(Bun.stdout, await generateLiveCompletion(shell)); + await postmortem.quit(0); } } diff --git a/packages/coding-agent/test/cli-completions-exit.test.ts b/packages/coding-agent/test/cli-completions-exit.test.ts new file mode 100644 index 000000000..5f631f60e --- /dev/null +++ b/packages/coding-agent/test/cli-completions-exit.test.ts @@ -0,0 +1,21 @@ +import { afterEach, describe, expect, it, spyOn } from "bun:test"; +import { postmortem } from "@oh-my-pi/pi-utils"; +import Completions from "../src/commands/completions"; + +describe("Completions command exit contract", () => { + afterEach(() => { + spyOn(postmortem, "quit").mockRestore(); + spyOn(Bun, "write").mockRestore(); + }); + + it("calls postmortem.quit(0) after writing completion script", async () => { + const quitSpy = spyOn(postmortem, "quit").mockResolvedValue(undefined); + const writeSpy = spyOn(Bun, "write").mockResolvedValue(0); + const config = { bin: "omp", version: "0.0.0", commands: new Map() }; + const cmd = new Completions(["zsh"], config); + await cmd.run(); + + expect(writeSpy).toHaveBeenCalled(); + expect(quitSpy).toHaveBeenCalledWith(0); + }); +});