* 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>
67 lines
2.9 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
}
|