fix(cli): honor gateway --port on the status leaf (#130847)

This commit is contained in:
Peter Steinberger
2026-08-27 02:24:40 -07:00
committed by GitHub
parent 58fbbe2e04
commit d3aa4be83a
11 changed files with 263 additions and 138 deletions
+9 -8
View File
@@ -23,18 +23,19 @@ openclaw daemon uninstall
## Subcommands and options
| Subcommand | Options |
| ----------- | ------------------------------------------------------------------------------------------------ |
| `status` | `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json` |
| `install` | `--port`, `--runtime <node\|bun>`, `--token`, `--wrapper <path>`, `--force`, `--json` |
| `uninstall` | `--json` |
| `start` | `--json` |
| `stop` | `--force`, `--json`, `--disable` (launchd only: suppress KeepAlive/RunAtLoad until next start) |
| `restart` | `--force`, `--safe`, `--skip-deferral`, `--wait <duration>`, `--json` |
| Subcommand | Options |
| ----------- | ---------------------------------------------------------------------------------------------------------- |
| `status` | `--url`, `--port`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json` |
| `install` | `--port`, `--runtime <node\|bun>`, `--token`, `--wrapper <path>`, `--force`, `--json` |
| `uninstall` | `--json` |
| `start` | `--json` |
| `stop` | `--force`, `--json`, `--disable` (launchd only: suppress KeepAlive/RunAtLoad until next start) |
| `restart` | `--force`, `--safe`, `--skip-deferral`, `--wait <duration>`, `--json` |
`--json` is accepted before or after every subcommand (for example, `daemon --json status` and `daemon status --json`).
- `status`: shows service install state (launchd/systemd/schtasks) and probes Gateway health.
- `status --port <port>`: selects a local Gateway using the invoking CLI config for auth and TLS. Cannot combine with `--url`; native service details remain diagnostic-only.
- `install`: installs the service; `--force` reinstalls/overwrites an existing install.
- Node is the primary, default, and recommended service runtime. Bun 1.4+ with WAL-reset-safe `node:sqlite` is available as an explicit opt-in with `install --runtime bun`.
- `restart --safe`: asks the running Gateway to preflight active work and schedule one coalesced restart after work drains, bounded to 5 minutes. When that budget expires, the restart is forced anyway. Plain `restart` uses the service manager directly; `--force` is the immediate override.
+6 -2
View File
@@ -344,10 +344,14 @@ Shows the Gateway service (launchd/systemd/schtasks) plus an optional connectivi
openclaw gateway status
openclaw gateway status --json
openclaw gateway status --require-rpc
openclaw gateway status --port 19001
```
<ParamField path="--url <url>" type="string">
Add an explicit probe target. Configured remote + localhost are still probed.
Probe this explicit WebSocket URL instead of the service-derived target. Cannot combine with `--port`.
</ParamField>
<ParamField path="--port <port>" type="number">
Select a local Gateway port using the invoking CLI config for auth and TLS. Accepts `gateway --port 19001 status` and `gateway status --port 19001`; an explicit status port wins. Native service details remain visible as diagnostics but do not select the probe target.
</ParamField>
<ParamField path="--token <token>" type="string">
Token auth for the probe.
@@ -592,7 +596,7 @@ openclaw gateway restart
<AccordionGroup>
<Accordion title="Command options">
- `gateway status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
- `gateway status`: `--url`, `--port`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
- `gateway install`: `--port`, `--runtime <node|bun>` (default: `node`), `--token`, `--wrapper <path>`, `--force`, `--json`
- `gateway restart`: `--safe`, `--skip-deferral`, `--force`, `--wait <duration>`, `--json`
- `gateway uninstall|start`: `--json`
+54 -47
View File
@@ -274,58 +274,63 @@ describe("probeGatewayStatus", () => {
expect(result.version).toBe("2026.5.6");
});
it("uses a real status RPC when requireRpc is enabled", async () => {
callGatewayMock.mockReset();
probeGatewayMock.mockReset();
callGatewayMock.mockImplementationOnce(async (opts) => {
opts.onHelloOk?.({
server: { version: "2026.8.1", buildId: "build-1", connId: "conn-1" },
auth: { role: "operator", scopes: ["operator.admin"] },
it.each([undefined, 19191])(
"uses a status RPC with local port override %s when requireRpc is enabled",
async (localPortOverride) => {
callGatewayMock.mockReset();
probeGatewayMock.mockReset();
callGatewayMock.mockImplementationOnce(async (opts) => {
opts.onHelloOk?.({
server: { version: "2026.8.1", buildId: "build-1", connId: "conn-1" },
auth: { role: "operator", scopes: ["operator.admin"] },
});
return { runtimeVersion: "2026.8.1", status: "ok" };
});
return { runtimeVersion: "2026.8.1", status: "ok" };
});
const result = await probeGatewayStatus({
url: "ws://127.0.0.1:19191",
token: "temp-token",
tlsFingerprint: "abc123",
timeoutMs: 5_000,
json: true,
requireRpc: true,
configPath: "/tmp/openclaw-daemon/openclaw.json",
});
const result = await probeGatewayStatus({
url: "ws://127.0.0.1:19191",
token: "temp-token",
tlsFingerprint: "abc123",
timeoutMs: 5_000,
json: true,
requireRpc: true,
localPortOverride,
configPath: "/tmp/openclaw-daemon/openclaw.json",
});
expect(result).toEqual({
ok: true,
kind: "read",
capability: "admin_capable",
auth: {
role: "operator",
scopes: ["operator.admin"],
expect(result).toEqual({
ok: true,
kind: "read",
capability: "admin_capable",
},
server: {
auth: {
role: "operator",
scopes: ["operator.admin"],
capability: "admin_capable",
},
server: {
version: "2026.8.1",
buildId: "build-1",
connId: "conn-1",
},
version: "2026.8.1",
buildId: "build-1",
connId: "conn-1",
},
version: "2026.8.1",
});
expect(probeGatewayMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledOnce();
expect(callGatewayMock).toHaveBeenCalledWith({
url: "ws://127.0.0.1:19191",
token: "temp-token",
password: undefined,
tlsFingerprint: "abc123",
preauthHandshakeTimeoutMs: undefined,
method: "status",
timeoutMs: 5_000,
sharedStateMode: "read-only",
configPath: "/tmp/openclaw-daemon/openclaw.json",
onHelloOk: expect.any(Function),
});
});
});
expect(probeGatewayMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledOnce();
expect(callGatewayMock).toHaveBeenCalledWith({
url: "ws://127.0.0.1:19191",
localPortOverride,
token: "temp-token",
password: undefined,
tlsFingerprint: "abc123",
preauthHandshakeTimeoutMs: undefined,
method: "status",
timeoutMs: 5_000,
sharedStateMode: "read-only",
configPath: "/tmp/openclaw-daemon/openclaw.json",
onHelloOk: expect.any(Function),
});
},
);
it("keeps required status to one timeout-bound RPC", async () => {
callGatewayMock.mockReset();
@@ -351,6 +356,7 @@ describe("probeGatewayStatus", () => {
tlsFingerprint: undefined,
preauthHandshakeTimeoutMs: 30_000,
config,
localPortOverride: undefined,
method: "status",
timeoutMs: 30_000,
sharedStateMode: "read-only",
@@ -403,6 +409,7 @@ describe("probeGatewayStatus", () => {
timeoutMs: 5_000,
sharedStateMode: "read-only",
onHelloOk: expect.any(Function),
localPortOverride: undefined,
});
});
+2
View File
@@ -52,6 +52,7 @@ function projectGatewayConnectFailure(params: {
/** Probe Gateway connectivity or read-capability status with optional RPC verification. */
export async function probeGatewayStatus(opts: {
url: string;
localPortOverride?: number;
token?: string;
password?: string;
config?: OpenClawConfig;
@@ -85,6 +86,7 @@ export async function probeGatewayStatus(opts: {
let server: GatewayProbeServerSummary | undefined;
await callGateway({
url: opts.url,
localPortOverride: opts.localPortOverride,
token: opts.token,
password: opts.password,
tlsFingerprint: opts.tlsFingerprint,
@@ -144,4 +144,30 @@ describe("addGatewayServiceCommands", () => {
expect(expectSingleDaemonCall(runner).json).toBe(true);
});
it("inherits an explicit parent port instead of a status leaf default", async () => {
const gateway = createGatewayParentLikeCommand().enablePositionalOptions();
const status = gateway.commands.find((command) => command.name() === "status")!;
status.setOptionValueWithSource("port", "19003", "default");
await gateway.parseAsync(["--port", "19002", "status"], { from: "user" });
expect(expectSingleDaemonCall(runDaemonStatus).rpc).toMatchObject({
port: "19002",
localPortOverride: 19002,
});
});
it.each([
{ argv: ["status", "--port", "0"], error: "--port must be an integer between 1 and 65535." },
{
argv: ["--port", "19002", "status", "--url", "ws://localhost:19002"],
error: "Use either --url or --port, not both.",
},
])("rejects invalid status options $argv", async ({ argv, error }) => {
const gateway = createGatewayParentLikeCommand().enablePositionalOptions().exitOverride();
await expect(gateway.parseAsync(argv, { from: "user" })).rejects.toThrow(error);
expect(runDaemonStatus).not.toHaveBeenCalled();
});
});
+10 -30
View File
@@ -2,24 +2,13 @@
import type { Command } from "commander";
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
import { inheritOptionFromParent } from "../command-options.js";
import type { DaemonInstallOptions, DaemonLifecycleOptions, GatewayRpcOpts } from "./types.js";
import { resolveGatewayRpcOptionsWithLocalPort } from "../gateway-rpc.js";
import type { DaemonInstallOptions, DaemonLifecycleOptions } from "./types.js";
const daemonInstallModuleLoader = createLazyImportLoader(() => import("./install.runtime.js"));
const daemonLifecycleModuleLoader = createLazyImportLoader(() => import("./lifecycle.runtime.js"));
const daemonStatusModuleLoader = createLazyImportLoader(() => import("./status.runtime.js"));
function loadDaemonInstallModule() {
return daemonInstallModuleLoader.load();
}
function loadDaemonLifecycleModule() {
return daemonLifecycleModuleLoader.load();
}
function loadDaemonStatusModule() {
return daemonStatusModuleLoader.load();
}
function resolveJsonOption(cmdOpts: { json?: boolean }, command?: Command): boolean {
const parentJson = inheritOptionFromParent<boolean>(command, "json", "cli");
return Boolean(cmdOpts.json || parentJson);
@@ -41,16 +30,6 @@ function resolveInstallOptions(
};
}
function resolveRpcOptions(cmdOpts: GatewayRpcOpts, command?: Command): GatewayRpcOpts {
const parentToken = inheritOptionFromParent<string>(command, "token");
const parentPassword = inheritOptionFromParent<string>(command, "password");
return {
...cmdOpts,
token: cmdOpts.token ?? parentToken,
password: cmdOpts.password ?? parentPassword,
};
}
function resolveRestartOptions(cmdOpts: DaemonLifecycleOptions, command?: Command) {
const parentForce = inheritOptionFromParent<boolean>(command, "force");
return {
@@ -78,6 +57,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
opts?.statusDescription ?? "Show gateway service status + probe connectivity/capability",
)
.option("--url <url>", "Gateway WebSocket URL (defaults to config/remote/local)")
.option("--port <port>", "Local Gateway port")
.option("--token <token>", "Gateway token (if required)")
.option("--password <password>", "Gateway password (password auth)")
.option("--timeout <ms>", "Timeout in ms", "10000")
@@ -86,9 +66,9 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.option("--deep", "Scan system-level services", false)
.option("--json", "Output JSON", false)
.action(async (cmdOpts, command) => {
const { runDaemonStatus } = await loadDaemonStatusModule();
const { runDaemonStatus } = await daemonStatusModuleLoader.load();
await runDaemonStatus({
rpc: resolveRpcOptions(cmdOpts, command),
rpc: resolveGatewayRpcOptionsWithLocalPort(cmdOpts, command),
probe: Boolean(cmdOpts.probe),
requireRpc: Boolean(cmdOpts.requireRpc),
deep: Boolean(cmdOpts.deep),
@@ -106,7 +86,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.option("--force", "Reinstall/overwrite if already installed", false)
.option("--json", "Output JSON", false)
.action(async (cmdOpts, command) => {
const { runDaemonInstall } = await loadDaemonInstallModule();
const { runDaemonInstall } = await daemonInstallModuleLoader.load();
await runDaemonInstall(resolveInstallOptions(cmdOpts, command));
});
@@ -115,7 +95,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.description("Uninstall the Gateway service (launchd/systemd/schtasks)")
.option("--json", "Output JSON", false)
.action(async (cmdOpts, command) => {
const { runDaemonUninstall } = await loadDaemonLifecycleModule();
const { runDaemonUninstall } = await daemonLifecycleModuleLoader.load();
await runDaemonUninstall({ ...cmdOpts, json: resolveJsonOption(cmdOpts, command) });
});
@@ -124,7 +104,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.description("Start the Gateway service (launchd/systemd/schtasks)")
.option("--json", "Output JSON", false)
.action(async (cmdOpts, command) => {
const { runDaemonStart } = await loadDaemonLifecycleModule();
const { runDaemonStart } = await daemonLifecycleModuleLoader.load();
await runDaemonStart({ ...cmdOpts, json: resolveJsonOption(cmdOpts, command) });
});
@@ -139,7 +119,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
false,
)
.action(async (cmdOpts, command) => {
const { runDaemonStop } = await loadDaemonLifecycleModule();
const { runDaemonStop } = await daemonLifecycleModuleLoader.load();
await runDaemonStop(resolveStopOptions(cmdOpts, command));
});
@@ -165,7 +145,7 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
)
.option("--json", "Output JSON", false)
.action(async (cmdOpts, command) => {
const { runDaemonRestart } = await loadDaemonLifecycleModule();
const { runDaemonRestart } = await daemonLifecycleModuleLoader.load();
await runDaemonRestart(resolveRestartOptions(cmdOpts, command));
});
}
+92
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Command } from "commander";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { StaleOpenClawUpdateLaunchdJob } from "../../daemon/launchd.js";
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
@@ -10,6 +11,8 @@ import type { GatewayRestartHandoff } from "../../infra/restart-handoff.js";
import { defaultRuntime } from "../../runtime.js";
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../test-utils/env.js";
import { VERSION } from "../../version.js";
import { registerGatewayCli } from "../gateway-cli/register.js";
import { registerDaemonCli } from "./register.js";
import type { GatewayRestartSnapshot } from "./restart-health.js";
import { gatherDaemonStatus, renderPortDiagnosticsForCli } from "./status.gather.js";
import { printDaemonStatus } from "./status.print.js";
@@ -493,6 +496,95 @@ describe("gatherDaemonStatus", () => {
expect(inspectWindowsGatewayFirewall).not.toHaveBeenCalled();
});
it.each(
[
["gateway", "status", "--port", "19002"],
["gateway", "--port", "19002", "status"],
["gateway", "--port", "19003", "status", "--port", "19002"],
["daemon", "status", "--port", "19002"],
].map((argv) => ({ name: argv.join(" "), argv })),
)("targets the selected local port for $name", async ({ argv }) => {
const program = new Command().enablePositionalOptions().exitOverride();
program.configureOutput({ writeErr: () => {} });
registerGatewayCli(program);
registerDaemonCli(program);
const writeJson = vi.spyOn(defaultRuntime, "writeJson").mockImplementation(() => {});
try {
await program.parseAsync([...argv, "--json"], { from: "user" });
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
expect.objectContaining({
url: "ws://127.0.0.1:19002",
localPortOverride: 19002,
config: cliLoadedConfig,
configPath: "/tmp/openclaw-cli/openclaw.json",
}),
);
expect(writeJson).toHaveBeenCalledWith(
expect.objectContaining({
gateway: expect.objectContaining({
port: 19002,
portSource: "cli",
probeUrl: "ws://127.0.0.1:19002",
}),
rpc: expect.objectContaining({ url: "ws://127.0.0.1:19002" }),
service: expect.objectContaining({ targetRole: "diagnostic-only" }),
}),
);
} finally {
writeJson.mockRestore();
}
});
it.each([true, false])(
"keeps an explicit local port in remote config with probe=%s",
async (probe) => {
cliLoadedConfig = {
gateway: {
mode: "remote",
bind: "tailnet",
tls: { enabled: true },
auth: { token: "local-token" },
remote: { url: "wss://gateway.example", token: "remote-token" },
},
};
const status = await gatherStatus({ rpc: { localPortOverride: 19002 }, probe, deep: true });
expect(status.gateway).toMatchObject({
port: 19002,
portSource: "cli",
probeUrl: "wss://127.0.0.1:19002",
});
expect(inspectPortConnections).toHaveBeenCalledWith(19002);
expect(status.service.targetRole).toBe("diagnostic-only");
expect(inspectGatewayRestart).not.toHaveBeenCalled();
if (probe) {
expect(callGatewayStatusProbe).toHaveBeenCalledWith(
expect.objectContaining({
url: "wss://127.0.0.1:19002",
token: "local-token",
tlsFingerprint: "sha256:11:22:33:44",
}),
);
} else {
expect(callGatewayStatusProbe).not.toHaveBeenCalled();
expect(status.rpc).toBeUndefined();
}
},
);
it.each([
{ rpc: { port: "65536" }, message: "--port must be an integer between 1 and 65535." },
{
rpc: { port: "19002", url: "ws://localhost:19002" },
message: "Use either --url or --port, not both.",
},
])("rejects invalid status target $rpc before service reads", async ({ rpc, message }) => {
await expect(gatherStatus({ rpc })).rejects.toThrow(message);
expect(serviceReadCommand).not.toHaveBeenCalled();
expect(callGatewayStatusProbe).not.toHaveBeenCalled();
});
it("batches daemon and CLI port status inspection when ports differ", async () => {
await gatherStatus();
+25 -12
View File
@@ -68,6 +68,7 @@ import {
} from "../../plugins/plugin-version-drift.js";
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
import { VERSION } from "../../version.js";
import { resolveGatewayLocalPortOverride } from "../gateway-port-option.js";
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
import type { GatewayRpcOpts } from "./types.js";
@@ -87,7 +88,7 @@ type GatewayStatusSummary = {
customBindHost?: string;
tlsEnabled?: boolean;
port: number;
portSource: "service args" | "env/config";
portSource: "cli" | "service args" | "env/config";
probeUrl: string;
controlUiLinks?: { httpUrl: string; wsUrl: string };
probeNote?: string;
@@ -433,12 +434,15 @@ async function resolveGatewayStatusSummary(params: {
mergedDaemonEnv: Record<string, string | undefined>;
commandProgramArguments?: string[];
rpcUrlOverride?: string;
localPortOverride?: number;
}): Promise<ResolvedGatewayStatus> {
const portFromArgs = parsePortFromArgs(params.commandProgramArguments);
const daemonPort = portFromArgs ?? resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv);
const portSource: GatewayStatusSummary["portSource"] = portFromArgs
? "service args"
: "env/config";
const daemonPort =
params.localPortOverride ??
portFromArgs ??
resolveGatewayPort(params.daemonCfg, params.mergedDaemonEnv);
const portSource: GatewayStatusSummary["portSource"] =
params.localPortOverride !== undefined ? "cli" : portFromArgs ? "service args" : "env/config";
const bindMode: GatewayBindMode = params.daemonCfg.gateway?.bind ?? "loopback";
const customBindHost = params.daemonCfg.gateway?.customBindHost;
const { bindHost, warning: bindHostWarning } = await resolveBestEffortGatewayBindHostForDisplay({
@@ -449,7 +453,10 @@ async function resolveGatewayStatusSummary(params: {
const { tailnetIPv4, warning: tailnetWarning } = inspectBestEffortPrimaryTailnetIPv4({
warningPrefix: "Status could not inspect tailnet addresses",
});
const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
const probeHost =
params.localPortOverride !== undefined
? "127.0.0.1"
: pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
const probeUrlOverride = trimToUndefined(params.rpcUrlOverride) ?? null;
const tlsEnabled = params.daemonCfg.gateway?.tls?.enabled === true;
const scheme = tlsEnabled ? "wss" : "ws";
@@ -595,6 +602,7 @@ export async function gatherDaemonStatus(
allowExecSecretRefs?: boolean;
} & FindExtraGatewayServicesOptions,
): Promise<DaemonStatus> {
const localPortOverride = resolveGatewayLocalPortOverride(opts.rpc);
const timeoutMs = parseTimeoutMsWithFallback(opts.rpc.timeout, 10_000, {
invalidType: "error",
});
@@ -605,10 +613,12 @@ export async function gatherDaemonStatus(
});
const { command, env: serviceEnv, loadState, runtime } = serviceState;
const loaded = loadState.status === "loaded";
// A non-default or externally supervised process does not own the host's
// native service. Keep that service visible, but do not let it retarget probes.
// An explicit local port or separate process context does not select the
// native service. Keep that service visible without borrowing its target or auth.
const useNativeServiceTargetContext =
isDefaultInstallIdentity(process.env) && !isGatewayExternallySupervised(process.env);
localPortOverride === undefined &&
isDefaultInstallIdentity(process.env) &&
!isGatewayExternallySupervised(process.env);
const targetServiceCommand = useNativeServiceTargetContext ? command : null;
const restartHandoff = opts.deep ? readGatewayRestartHandoffSync(serviceEnv) : null;
const configAudit: ServiceConfigAudit = command
@@ -635,9 +645,12 @@ export async function gatherDaemonStatus(
mergedDaemonEnv,
commandProgramArguments: targetServiceCommand?.programArguments,
rpcUrlOverride: opts.rpc.url,
localPortOverride,
});
const probeMode =
localPortOverride === undefined && daemonCfg.gateway?.mode === "remote" ? "remote" : "local";
const serviceTargetsProbe = useNativeServiceTargetContext && !probeUrlOverride;
const shouldInspectLocalGateway = daemonCfg.gateway?.mode !== "remote" && !probeUrlOverride;
const shouldInspectLocalGateway = probeMode === "local" && !probeUrlOverride;
const windowsFirewall =
opts.deep === true && shouldInspectLocalGateway
? await inspectWindowsGatewayFirewall({
@@ -655,7 +668,7 @@ export async function gatherDaemonStatus(
const establishedClients = await inspectEstablishedGatewayClients({
daemonPort,
deep: opts.deep,
gatewayMode: daemonCfg.gateway?.mode,
gatewayMode: probeMode,
});
const extraServices = opts.deep
@@ -688,7 +701,6 @@ export async function gatherDaemonStatus(
let allowRpcConfigCredentials = true;
let skippedProbeAuthForDisabledExecSecretRef = false;
if (opts.probe) {
const probeMode = daemonCfg.gateway?.mode === "remote" ? "remote" : "local";
const explicitAuth = {
token: opts.rpc.token,
password: opts.rpc.password,
@@ -725,6 +737,7 @@ export async function gatherDaemonStatus(
? await loadDaemonProbeModule().then(({ probeGatewayStatus }) =>
probeGatewayStatus({
url: probeUrl,
localPortOverride,
token: daemonProbeAuth?.token,
password: daemonProbeAuth?.password,
config: daemonCfg,
+3 -6
View File
@@ -1,13 +1,10 @@
// Shared option types for Gateway service CLI commands.
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
import type { GatewayRpcOpts as SharedGatewayRpcOpts } from "../gateway-rpc.types.js";
/** RPC probe options accepted by Gateway service status commands. */
export type GatewayRpcOpts = {
url?: string;
token?: string;
password?: string;
timeout?: string;
json?: boolean;
export type GatewayRpcOpts = Omit<SharedGatewayRpcOpts, "expectFinal"> & {
localPortOverride?: number;
};
/** Full option bag for Gateway service status. */
+6 -33
View File
@@ -21,8 +21,12 @@ import { createLazyImportLoader } from "../../shared/lazy-promise.js";
import { inheritOptionFromParent } from "../command-options.js";
import { addGatewayServiceCommands } from "../daemon-cli/register-service-commands.js";
import { formatCliJsonFailure, rethrowExpectedCliError } from "../failure-output.js";
import { parseGatewayPortOption } from "../gateway-port-option.js";
import { addGatewayClientOptions, callGatewayFromCliWithTransport } from "../gateway-rpc.js";
import {
addGatewayClientOptions,
callGatewayFromCliWithTransport,
resolveGatewayRpcOptions,
resolveGatewayRpcOptionsWithLocalPort,
} from "../gateway-rpc.js";
import { formatHelpExamples } from "../help-format.js";
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
import { setCommandJsonMode } from "../program/json-mode.js";
@@ -179,37 +183,6 @@ function parseDaysOption(raw: unknown, fallback = 30): number {
return fallback;
}
function resolveGatewayRpcOptions<T extends { token?: string; password?: string }>(
opts: T,
command?: Command,
): T {
const parentToken = inheritOptionFromParent<string>(command, "token");
const parentPassword = inheritOptionFromParent<string>(command, "password");
return {
...opts,
token: opts.token ?? parentToken,
password: opts.password ?? parentPassword,
};
}
function resolveGatewayRpcOptionsWithLocalPort(
opts: GatewayRpcOpts & { port?: unknown },
command?: Command,
): GatewayRpcOpts {
const rpcOpts = resolveGatewayRpcOptions(opts, command);
const port = parseGatewayPortOption(opts.port ?? inheritOptionFromParent(command, "port"));
if (port === undefined) {
return rpcOpts;
}
if (typeof opts.url === "string" && opts.url.trim()) {
throw new Error("Use either --url or --port, not both.");
}
return {
...rpcOpts,
localPortOverride: port,
};
}
async function renderCostUsageSummaryAsync(
summary: CostUsageSummary,
days: number,
+30
View File
@@ -8,6 +8,8 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { OperatorScope } from "../gateway/operator-scopes.js";
import type { DeviceIdentity } from "../infra/device-identity.js";
import { createLazyImportLoader } from "../shared/lazy-promise.js";
import { inheritOptionFromParent } from "./command-options.js";
import { resolveGatewayLocalPortOverride } from "./gateway-port-option.js";
import type { GatewayRpcOpts } from "./gateway-rpc.types.js";
export type { GatewayRpcOpts } from "./gateway-rpc.types.js";
@@ -32,6 +34,34 @@ export function addGatewayClientOptions(cmd: Command, defaults?: { timeoutMs?: n
.option("--expect-final", "Wait for final response (agent)", false);
}
export function resolveGatewayRpcOptions<T extends { token?: string; password?: string }>(
opts: T,
command?: Command,
): T {
return {
...opts,
token: opts.token ?? inheritOptionFromParent<string>(command, "token"),
password: opts.password ?? inheritOptionFromParent<string>(command, "password"),
};
}
export function resolveGatewayRpcOptionsWithLocalPort<
T extends Pick<GatewayRpcOpts, "url" | "port" | "token" | "password"> & {
localPortOverride?: number;
},
>(opts: T, command?: Command) {
// Leaf defaults must not hide an explicit port supplied before the subcommand.
const port = command?.getOptionValueSource("port") === "default" ? undefined : opts.port;
const rpcOpts = {
...resolveGatewayRpcOptions(opts, command),
port: port ?? inheritOptionFromParent<string>(command, "port"),
};
return {
...rpcOpts,
localPortOverride: resolveGatewayLocalPortOverride(rpcOpts),
};
}
export async function callGatewayFromCli(
method: string,
opts: GatewayRpcOpts,