From d92fc855553a2fba00ae7f32c2e4ddf230994fa2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 19:50:27 +0100 Subject: [PATCH] refactor(cli): dedupe gateway run mode parsing --- .../gateway-cli/run.option-collisions.test.ts | 29 ++++----- src/cli/gateway-cli/run.ts | 62 ++++++++++++++----- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index fd5afa1b7..4fa6d7046 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -118,6 +118,17 @@ describe("gateway run option collisions", () => { }); } + function expectAuthOverrideMode(mode: string) { + expect(startGatewayServer).toHaveBeenCalledWith( + 18789, + expect.objectContaining({ + auth: expect.objectContaining({ + mode, + }), + }), + ); + } + it("forwards parent-captured options to `gateway run` subcommand", async () => { await runGatewayCli([ "gateway", @@ -156,27 +167,13 @@ describe("gateway run option collisions", () => { it("accepts --auth none override", async () => { await runGatewayCli(["gateway", "run", "--auth", "none", "--allow-unconfigured"]); - expect(startGatewayServer).toHaveBeenCalledWith( - 18789, - expect.objectContaining({ - auth: expect.objectContaining({ - mode: "none", - }), - }), - ); + expectAuthOverrideMode("none"); }); it("accepts --auth trusted-proxy override", async () => { await runGatewayCli(["gateway", "run", "--auth", "trusted-proxy", "--allow-unconfigured"]); - expect(startGatewayServer).toHaveBeenCalledWith( - 18789, - expect.objectContaining({ - auth: expect.objectContaining({ - mode: "trusted-proxy", - }), - }), - ); + expectAuthOverrideMode("trusted-proxy"); }); it("prints all supported modes on invalid --auth value", async () => { diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 07f80227a..291328273 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -77,6 +77,42 @@ const GATEWAY_RUN_BOOLEAN_KEYS = [ "rawStream", ] as const; +const GATEWAY_AUTH_MODES: readonly GatewayAuthMode[] = [ + "none", + "token", + "password", + "trusted-proxy", +]; +const GATEWAY_TAILSCALE_MODES: readonly GatewayTailscaleMode[] = ["off", "serve", "funnel"]; + +function parseEnumOption( + raw: string | undefined, + allowed: readonly T[], +): T | null { + if (!raw) { + return null; + } + return (allowed as readonly string[]).includes(raw) ? (raw as T) : null; +} + +function formatModeChoices(modes: readonly T[]): string { + return modes.map((mode) => `"${mode}"`).join("|"); +} + +function formatModeErrorList(modes: readonly T[]): string { + const quoted = modes.map((mode) => `"${mode}"`); + if (quoted.length === 0) { + return ""; + } + if (quoted.length === 1) { + return quoted[0]; + } + if (quoted.length === 2) { + return `${quoted[0]} or ${quoted[1]}`; + } + return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`; +} + function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): GatewayRunOpts { const resolved: GatewayRunOpts = { ...opts }; @@ -185,25 +221,18 @@ async function runGatewayCommand(opts: GatewayRunOpts) { } } const authModeRaw = toOptionString(opts.auth); - const authMode: GatewayAuthMode | null = - authModeRaw === "none" || - authModeRaw === "token" || - authModeRaw === "password" || - authModeRaw === "trusted-proxy" - ? authModeRaw - : null; + const authMode = parseEnumOption(authModeRaw, GATEWAY_AUTH_MODES); if (authModeRaw && !authMode) { - defaultRuntime.error('Invalid --auth (use "none", "token", "password", or "trusted-proxy")'); + defaultRuntime.error(`Invalid --auth (use ${formatModeErrorList(GATEWAY_AUTH_MODES)})`); defaultRuntime.exit(1); return; } const tailscaleRaw = toOptionString(opts.tailscale); - const tailscaleMode: GatewayTailscaleMode | null = - tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel" - ? tailscaleRaw - : null; + const tailscaleMode = parseEnumOption(tailscaleRaw, GATEWAY_TAILSCALE_MODES); if (tailscaleRaw && !tailscaleMode) { - defaultRuntime.error('Invalid --tailscale (use "off", "serve", or "funnel")'); + defaultRuntime.error( + `Invalid --tailscale (use ${formatModeErrorList(GATEWAY_TAILSCALE_MODES)})`, + ); defaultRuntime.exit(1); return; } @@ -369,9 +398,12 @@ export function addGatewayRunCommand(cmd: Command): Command { "--token ", "Shared token required in connect.params.auth.token (default: OPENCLAW_GATEWAY_TOKEN env if set)", ) - .option("--auth ", 'Gateway auth mode ("none"|"token"|"password"|"trusted-proxy")') + .option("--auth ", `Gateway auth mode (${formatModeChoices(GATEWAY_AUTH_MODES)})`) .option("--password ", "Password for auth mode=password") - .option("--tailscale ", 'Tailscale exposure mode ("off"|"serve"|"funnel")') + .option( + "--tailscale ", + `Tailscale exposure mode (${formatModeChoices(GATEWAY_TAILSCALE_MODES)})`, + ) .option( "--tailscale-reset-on-exit", "Reset Tailscale serve/funnel configuration on shutdown",