Files
Moltbot/src/cli/acp-cli.ts
Taylor Asplund 874ff7089c fix: ensure CLI exits after command completion (#12906)
* fix: ensure CLI exits after command completion

The CLI process would hang indefinitely after commands like
`openclaw gateway restart` completed successfully.  Two root causes:

1. `runCli()` returned without calling `process.exit()` after
   `program.parseAsync()` resolved, and Commander.js does not
   force-exit the process.

2. `daemon-cli/register.ts` eagerly called `createDefaultDeps()`
   which imported all messaging-provider modules, creating persistent
   event-loop handles that prevented natural Node exit.

Changes:
- Add `flushAndExit()` helper that drains stdout/stderr before calling
  `process.exit()`, preventing truncated piped output in CI/scripts.
- Call `flushAndExit()` after both `tryRouteCli()` and
  `program.parseAsync()` resolve.
- Remove unnecessary `void createDefaultDeps()` from daemon-cli
  registration — daemon lifecycle commands never use messaging deps.
- Make `serveAcpGateway()` return a promise that resolves on
  intentional shutdown (SIGINT/SIGTERM), so `openclaw acp` blocks
  `parseAsync` for the bridge lifetime and exits cleanly on signal.
- Handle the returned promise in the standalone main-module entry
  point to avoid unhandled rejections.

Fixes #12904

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: refactor CLI lifecycle and lazy outbound deps (#12906) (thanks @DrCrinkle)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-14 00:34:33 +01:00

67 lines
2.9 KiB
TypeScript

import type { Command } from "commander";
import { runAcpClientInteractive } from "../acp/client.js";
import { serveAcpGateway } from "../acp/server.js";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
export function registerAcpCli(program: Command) {
const acp = program.command("acp").description("Run an ACP bridge backed by the Gateway");
acp
.option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
.option("--token <token>", "Gateway token (if required)")
.option("--password <password>", "Gateway password (if required)")
.option("--session <key>", "Default session key (e.g. agent:main:main)")
.option("--session-label <label>", "Default session label to resolve")
.option("--require-existing", "Fail if the session key/label does not exist", false)
.option("--reset-session", "Reset the session key before first use", false)
.option("--no-prefix-cwd", "Do not prefix prompts with the working directory", false)
.option("--verbose, -v", "Verbose logging to stderr", false)
.addHelpText(
"after",
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/acp", "docs.openclaw.ai/cli/acp")}\n`,
)
.action(async (opts) => {
try {
await serveAcpGateway({
gatewayUrl: opts.url as string | undefined,
gatewayToken: opts.token as string | undefined,
gatewayPassword: opts.password as string | undefined,
defaultSessionKey: opts.session as string | undefined,
defaultSessionLabel: opts.sessionLabel as string | undefined,
requireExistingSession: Boolean(opts.requireExisting),
resetSession: Boolean(opts.resetSession),
prefixCwd: !opts.noPrefixCwd,
verbose: Boolean(opts.verbose),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
acp
.command("client")
.description("Run an interactive ACP client against the local ACP bridge")
.option("--cwd <dir>", "Working directory for the ACP session")
.option("--server <command>", "ACP server command (default: openclaw)")
.option("--server-args <args...>", "Extra arguments for the ACP server")
.option("--server-verbose", "Enable verbose logging on the ACP server", false)
.option("--verbose, -v", "Verbose client logging", false)
.action(async (opts) => {
try {
await runAcpClientInteractive({
cwd: opts.cwd as string | undefined,
serverCommand: opts.server as string | undefined,
serverArgs: opts.serverArgs as string[] | undefined,
serverVerbose: Boolean(opts.serverVerbose),
verbose: Boolean(opts.verbose),
});
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
}
});
}