From 44ec7580e2f0d61cd7f830ddb6b7dac67faa0f7e Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 30 Jun 2026 14:15:19 -0700 Subject: [PATCH] fix(cli): stop `pairing list` crashing with empty channel enum (#98142) When no chat DM pairing channels are configured, `openclaw pairing list` (no channel argument) threw `Channel required ... (expected one of: )` with an empty enum that reads like a bug. Users who hit this are usually trying to approve a TUI/device or scope-upgrade request, which lives under `openclaw devices`, not `openclaw pairing` (which only handles chat DM pairing). - Guard the channel hint in help text and errors so an empty channel list no longer renders a bare `()` / `(expected one of: )`. - When no pairing channels exist, redirect to `openclaw devices list` / `openclaw devices approve` instead of failing opaquely. AI-assisted (Claude Code). --- src/cli/pairing-cli.test.ts | 24 ++++++++++++++++++++++++ src/cli/pairing-cli.ts | 19 +++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/cli/pairing-cli.test.ts b/src/cli/pairing-cli.test.ts index 520f4f583eb..a21ad376a9d 100644 --- a/src/cli/pairing-cli.test.ts +++ b/src/cli/pairing-cli.test.ts @@ -224,6 +224,30 @@ describe("pairing cli", () => { expect(listChannelPairingRequests).toHaveBeenCalledWith("slack"); }); + it("redirects to openclaw devices when no pairing channels are configured", async () => { + listPairingChannels.mockReturnValueOnce([]); + + const error = await runPairing(["pairing", "list"]).then( + () => null, + (err: unknown) => err, + ); + + expect(error).toBeInstanceOf(Error); + const message = (error as Error).message; + expect(message).toContain("openclaw devices"); + // Must not leak the empty enum that originally read like a bug. + expect(message).not.toContain("expected one of: )"); + expect(message).not.toContain("()"); + expect(listChannelPairingRequests).not.toHaveBeenCalled(); + }); + + it("lists supported channels when one is required but omitted", async () => { + // Multiple channels configured (default mock) + no channel argument. + await expect(runPairing(["pairing", "list"])).rejects.toThrow( + "expected one of: telegram, discord, imessage", + ); + }); + it("accepts channel as positional for approve (npm-run compatible)", async () => { mockApprovedPairing(); diff --git a/src/cli/pairing-cli.ts b/src/cli/pairing-cli.ts index 2ac6043af6d..5f2340069ae 100644 --- a/src/cli/pairing-cli.ts +++ b/src/cli/pairing-cli.ts @@ -88,6 +88,8 @@ async function maybeBootstrapCommandOwnerFromPairing(params: { export function registerPairingCli(program: Command) { const channels = listPairingChannels(); + // Avoid rendering a bare "()" enum when no channels are configured. + const channelHint = channels.length > 0 ? channels.join(", ") : "none configured"; const pairing = program .command("pairing") .description("Secure DM pairing (approve inbound requests)") @@ -100,16 +102,21 @@ export function registerPairingCli(program: Command) { pairing .command("list") .description("List pending pairing requests") - .option("--channel ", `Channel (${channels.join(", ")})`) + .option("--channel ", `Channel (${channelHint})`) .option("--account ", "Account id (for multi-account channels)") - .argument("[channel]", `Channel (${channels.join(", ")})`) + .argument("[channel]", `Channel (${channelHint})`) .option("--json", "Print JSON", false) .action(async (channelArg, opts) => { const channelRaw = opts.channel ?? channelArg ?? (channels.length === 1 ? channels[0] : ""); if (!channelRaw) { - throw new Error( - `Channel required. Use --channel or pass it as the first argument (expected one of: ${channels.join(", ")})`, - ); + if (channels.length === 0) { + // `pairing` is chat DM only; TUI/device approvals live under `openclaw devices`. + throw new Error( + `No chat DM pairing channels are configured. To approve a TUI or device request, ` + + `use ${formatCliCommand("openclaw devices approve")} instead.`, + ); + } + throw new Error(`Channel required (expected one of: ${channelHint}).`); } const channel = parseChannel(channelRaw, channels); const accountId = normalizeStringifiedOptionalString(opts.account) ?? ""; @@ -151,7 +158,7 @@ export function registerPairingCli(program: Command) { pairing .command("approve") .description("Approve a pairing code and allow that sender") - .option("--channel ", `Channel (${channels.join(", ")})`) + .option("--channel ", `Channel (${channelHint})`) .option("--account ", "Account id (for multi-account channels)") .argument("", "Pairing code (or channel when using 2 args)") .argument("[code]", "Pairing code (when channel is passed as the 1st arg)")