Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createCli } from "bailian-cli-runtime";
import { createCli, runToExit } from "bailian-cli-runtime";
import { commandAliases, commands } from "./commands.ts";
import { commandPackPolicy } from "./command-pack-policy.ts";
import pkg from "../package.json" with { type: "json" };
Expand All @@ -12,7 +12,7 @@ const quickStartTasks = [
"介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。\n Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.",
] as const;

void createCli(
const cli = createCli(
{ ...commands, ...commandAliases },
{
binName: "bl",
Expand All @@ -23,4 +23,6 @@ void createCli(
quickStartTasks,
commandPacks: commandPackPolicy,
},
).run();
);

void runToExit(cli);
8 changes: 5 additions & 3 deletions packages/kscli/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createCli } from "bailian-cli-runtime";
import { createCli, runToExit } from "bailian-cli-runtime";
import { commands } from "./commands.ts";
import pkg from "../package.json" with { type: "json" };

void createCli(commands, {
const cli = createCli(commands, {
binName: "kscli",
version: pkg.version,
clientName: "knowledge-studio-cli",
npmPackage: "knowledge-studio-cli",
}).run();
});

void runToExit(cli);
20 changes: 20 additions & 0 deletions packages/runtime/src/create-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,26 @@ function handleInterrupt(): void {
void flushTelemetry(500).finally(() => process.exit(130));
}

/**
* Run the CLI and end the process once the invocation is complete.
*
* Every background task (update check, telemetry flush) is awaited inside `run()`, so nothing
* useful is left once it resolves. Without an explicit exit, keep-alive sockets held by the HTTP
* client can keep a finished process alive for tens of seconds (observed on the Bun release binary).
* Errors still exit through `handleError`.
*/
export async function runToExit(cli: Cli, argv?: string[]): Promise<void> {
await cli.run(argv);
const code = process.exitCode ?? 0;
const flush = (stream: NodeJS.WriteStream) =>
new Promise<void>((done) => {
stream.write("", () => done());
});
await flush(process.stdout);
await flush(process.stderr);
process.exit(code);
}

/** Keep EPIPE protection after run() returns, without retaining completed invocations. */
function handleStdoutPipeError(error: NodeJS.ErrnoException): void {
if (error.code === "EPIPE") process.exit(0);
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// each entrypoint (bl / rag / …) injects its own command set via createCli().

// Entrypoint factory + identity
export { createCli, registerInterruptHandler } from "./create-cli.ts";
export { createCli, registerInterruptHandler, runToExit } from "./create-cli.ts";
export { buildCommandSchema, serializeCommandSchema } from "./introspect.ts";
export type { CommandSchema, CommandSchemaNode } from "./introspect.ts";
export type { Cli, CliOptions } from "./create-cli.ts";
Expand Down
55 changes: 55 additions & 0 deletions packages/runtime/tests/run-to-exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { afterEach, expect, test, vi } from "vite-plus/test";
import { runToExit } from "../src/index.ts";

const originalExitCode = process.exitCode;

afterEach(() => {
process.exitCode = originalExitCode;
vi.restoreAllMocks();
});

function stubExit() {
return vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
}

test("runToExit exits with code 0 once run() resolves", async () => {
const exit = stubExit();
process.exitCode = undefined;
const events: string[] = [];
const run = vi.fn(async () => {
events.push("run");
});
exit.mockImplementation((() => {
events.push("exit");
}) as never);

await runToExit({ run }, ["x"]);

expect(run).toHaveBeenCalledWith(["x"]);
expect(events).toEqual(["run", "exit"]);
expect(exit).toHaveBeenCalledWith(0);
});

test("runToExit keeps the exit code the run set", async () => {
const exit = stubExit();
await runToExit({
async run() {
process.exitCode = 3;
},
});

expect(exit).toHaveBeenCalledWith(3);
});

test("runToExit does not exit while run() is still pending", async () => {
const exit = stubExit();
let finish!: () => void;
const pending = runToExit({ run: () => new Promise<void>((resolve) => (finish = resolve)) });

await new Promise((resolve) => setTimeout(resolve, 20));
expect(exit).not.toHaveBeenCalled();

finish();
await pending;
expect(exit).toHaveBeenCalledTimes(1);
});