CLI: resolve parent/subcommand option collisions (#18725)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b7e51cf90950cdd3049ac3c7a3a949717b8ba261
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-02-17 20:57:09 -05:00
committed by GitHub
parent fa4f66255c
commit 985ec71c55
17 changed files with 856 additions and 38 deletions

View File

@@ -0,0 +1,72 @@
import { Command } from "commander";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { addGatewayServiceCommands } from "./register-service-commands.js";
const runDaemonInstall = vi.fn(async () => {});
const runDaemonRestart = vi.fn(async () => {});
const runDaemonStart = vi.fn(async () => {});
const runDaemonStatus = vi.fn(async () => {});
const runDaemonStop = vi.fn(async () => {});
const runDaemonUninstall = vi.fn(async () => {});
vi.mock("./runners.js", () => ({
runDaemonInstall: (opts: unknown) => runDaemonInstall(opts),
runDaemonRestart: (opts: unknown) => runDaemonRestart(opts),
runDaemonStart: (opts: unknown) => runDaemonStart(opts),
runDaemonStatus: (opts: unknown) => runDaemonStatus(opts),
runDaemonStop: (opts: unknown) => runDaemonStop(opts),
runDaemonUninstall: (opts: unknown) => runDaemonUninstall(opts),
}));
function createGatewayParentLikeCommand() {
const gateway = new Command().name("gateway");
// Mirror overlapping root gateway options that conflict with service subcommand options.
gateway.option("--port <port>", "Port for the gateway WebSocket");
gateway.option("--token <token>", "Gateway token");
gateway.option("--password <password>", "Gateway password");
gateway.option("--force", "Gateway run --force", false);
addGatewayServiceCommands(gateway);
return gateway;
}
describe("addGatewayServiceCommands", () => {
beforeEach(() => {
runDaemonInstall.mockClear();
runDaemonRestart.mockClear();
runDaemonStart.mockClear();
runDaemonStatus.mockClear();
runDaemonStop.mockClear();
runDaemonUninstall.mockClear();
});
it("forwards install option collisions from parent gateway command", async () => {
const gateway = createGatewayParentLikeCommand();
await gateway.parseAsync(["install", "--force", "--port", "19000", "--token", "tok_test"], {
from: "user",
});
expect(runDaemonInstall).toHaveBeenCalledWith(
expect.objectContaining({
force: true,
port: "19000",
token: "tok_test",
}),
);
});
it("forwards status auth collisions from parent gateway command", async () => {
const gateway = createGatewayParentLikeCommand();
await gateway.parseAsync(["status", "--token", "tok_status", "--password", "pw_status"], {
from: "user",
});
expect(runDaemonStatus).toHaveBeenCalledWith(
expect.objectContaining({
rpc: expect.objectContaining({
token: "tok_status",
password: "pw_status",
}),
}),
);
});
});

View File

@@ -1,4 +1,6 @@
import type { Command } from "commander";
import type { DaemonInstallOptions, GatewayRpcOpts } from "./types.js";
import { inheritOptionFromParent } from "../command-options.js";
import {
runDaemonInstall,
runDaemonRestart,
@@ -8,6 +10,31 @@ import {
runDaemonUninstall,
} from "./runners.js";
function resolveInstallOptions(
cmdOpts: DaemonInstallOptions,
command?: Command,
): DaemonInstallOptions {
const parentForce = inheritOptionFromParent<boolean>(command, "force");
const parentPort = inheritOptionFromParent<string>(command, "port");
const parentToken = inheritOptionFromParent<string>(command, "token");
return {
...cmdOpts,
force: Boolean(cmdOpts.force || parentForce),
port: cmdOpts.port ?? parentPort,
token: cmdOpts.token ?? parentToken,
};
}
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,
};
}
export function addGatewayServiceCommands(parent: Command, opts?: { statusDescription?: string }) {
parent
.command("status")
@@ -19,9 +46,9 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.option("--no-probe", "Skip RPC probe")
.option("--deep", "Scan system-level services", false)
.option("--json", "Output JSON", false)
.action(async (cmdOpts) => {
.action(async (cmdOpts, command) => {
await runDaemonStatus({
rpc: cmdOpts,
rpc: resolveRpcOptions(cmdOpts, command),
probe: Boolean(cmdOpts.probe),
deep: Boolean(cmdOpts.deep),
json: Boolean(cmdOpts.json),
@@ -36,8 +63,8 @@ export function addGatewayServiceCommands(parent: Command, opts?: { statusDescri
.option("--token <token>", "Gateway token (token auth)")
.option("--force", "Reinstall/overwrite if already installed", false)
.option("--json", "Output JSON", false)
.action(async (cmdOpts) => {
await runDaemonInstall(cmdOpts);
.action(async (cmdOpts, command) => {
await runDaemonInstall(resolveInstallOptions(cmdOpts, command));
});
parent