fix(cli): make group help side-effect free

This commit is contained in:
Codex
2026-06-01 22:34:16 +08:00
parent d4c05d7963
commit 80cf8c7f19
2 changed files with 140 additions and 0 deletions
+21
View File
@@ -111,6 +111,27 @@ test("hwlab-cli client auth status exposes local session state and next commands
assert.equal(JSON.stringify(mismatch.payload).includes("session-old"), false);
});
test("hwlab-cli client group help is visible and does not issue HTTP requests", async () => {
let fetchCount = 0;
const fetchImpl = async () => {
fetchCount += 1;
return new Response(JSON.stringify({ ok: false }), { status: 500 });
};
const auth = await runHwlabCli(["client", "auth", "--help", "--base-url", "http://web.test"], { fetchImpl });
assert.equal(auth.exitCode, 0);
assert.equal(auth.payload.action, "client.auth.help");
assert.equal(auth.payload.serviceRuntime, false);
assert.ok(auth.payload.commands.some((command: string) => command.startsWith("login ")));
const agent = await runHwlabCli(["client", "agent", "--help", "--base-url", "http://web.test"], { fetchImpl });
assert.equal(agent.exitCode, 0);
assert.equal(agent.payload.action, "client.agent.help");
assert.equal(agent.payload.serviceRuntime, false);
assert.ok(agent.payload.commands.some((command: string) => command.startsWith("send ")));
assert.equal(fetchCount, 0);
});
test("hwlab-cli client device-pods events compacts event stream by default", async () => {
const result = await runHwlabCli(["client", "device-pods", "events", "pod-1", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a"], {
fetchImpl: async () => new Response(JSON.stringify({
+119
View File
@@ -62,6 +62,10 @@ async function clientCommand(context: any) {
const group = context.rest[0] || "help";
const next = { ...context, rest: context.rest.slice(1) };
if (["help", "--help", "-h"].includes(group)) return help();
if (context.parsed.help === true) {
const groupHelp = clientSubcommandHelp(group);
if (groupHelp) return groupHelp;
}
if (group === "auth") return authCommand(next);
if (group === "device-pods" || group === "device-pod") return devicePodsCommand(next);
if (group === "runtime") return runtimeCommand(next);
@@ -74,6 +78,19 @@ async function clientCommand(context: any) {
throw cliError("unsupported_client_command", `unsupported client command: ${group}`, { group });
}
function clientSubcommandHelp(group: string) {
if (group === "auth") return authHelp();
if (group === "device-pods" || group === "device-pod") return devicePodsHelp();
if (group === "runtime") return runtimeHelp();
if (group === "gateway") return gatewayHelp();
if (group === "agent") return agentHelp();
if (["harness", "harness-ops", "harness-opt"].includes(group)) return harnessHelp();
if (group === "workbench") return workbenchHelp();
if (group === "rpc") return rpcHelp();
if (group === "request") return requestHelp();
return null;
}
function help() {
return ok("help", {
version: VERSION,
@@ -116,6 +133,7 @@ function help() {
async function authCommand(context: any) {
const subcommand = context.rest[0] || "session";
if (wantsHelp(context)) return authHelp();
if (subcommand === "login") return authLogin(context);
if (subcommand === "status") return authStatus(context);
if (subcommand === "profiles") return authProfiles(context);
@@ -132,6 +150,20 @@ async function authCommand(context: any) {
throw cliError("unsupported_auth_command", `unsupported auth command: ${subcommand}`, { subcommand });
}
function authHelp() {
return ok("client.auth.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"login --base-url URL --username USER --password-env HWLAB_PASSWORD [--profile NAME]",
"status [--base-url URL] [--profile NAME]",
"session [--base-url URL] [--profile NAME]",
"profiles [--base-url URL]",
"logout [--base-url URL] [--profile NAME]"
]
});
}
async function authStatus(context: any) {
const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }));
return ok("client.auth.status", {
@@ -190,6 +222,7 @@ async function authLogin(context: any) {
async function devicePodsCommand(context: any) {
const subcommand = context.rest[0] || "list";
if (wantsHelp(context)) return devicePodsHelp();
if (subcommand === "list") {
const response = await requestJson({ ...context, method: "GET", path: "/v1/device-pods" });
return responsePayload("client.device-pods.list", response, context, { route: route("GET", "/v1/device-pods"), body: responseBodyForCli(response.body, context.parsed) });
@@ -210,8 +243,22 @@ async function devicePodsCommand(context: any) {
throw cliError("unsupported_device_pods_command", `unsupported device-pods command: ${subcommand}`, { subcommand });
}
function devicePodsHelp() {
return ok("client.device-pods.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"list [--base-url URL]",
"status POD_ID [--base-url URL]",
"events POD_ID [--limit N]",
"probe POD_ID [--max-bytes N]"
]
});
}
async function runtimeCommand(context: any) {
const subcommand = context.rest[0] || "routes";
if (wantsHelp(context)) return runtimeHelp();
if (subcommand !== "routes" && subcommand !== "pods") {
throw cliError("unsupported_runtime_command", `unsupported runtime command: ${subcommand}`, { subcommand });
}
@@ -237,6 +284,17 @@ async function runtimeCommand(context: any) {
});
}
function runtimeHelp() {
return ok("client.runtime.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"routes [--service-id SERVICE]",
"pods [--service-id SERVICE]"
]
});
}
function liveBuildServices(body: any) {
const builds = Array.isArray(body?.builds) ? body.builds : [];
if (builds.length > 0) return builds;
@@ -523,6 +581,7 @@ function gatewayScenarioPass({ scenario, response, dispatch, stdout, stderr, gat
async function agentCommand(context: any) {
const subcommand = context.rest[0] || "send";
if (wantsHelp(context)) return agentHelp();
if (subcommand === "send") return agentSend(context);
if (subcommand === "result") {
const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId);
@@ -550,6 +609,20 @@ async function agentCommand(context: any) {
throw cliError("unsupported_agent_command", `unsupported agent command: ${subcommand}`, { subcommand });
}
function agentHelp() {
return ok("client.agent.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] [--no-wait]",
"result TRACE_ID [--full]",
"trace TRACE_ID [--render web] [--limit N] [--full]",
"inspect --trace-id TRACE_ID|--conversation-id ID|--session-id ID|--thread-id ID",
"cancel TRACE_ID"
]
});
}
function agentInspectPath(context: any) {
const params = new URLSearchParams();
const traceId = text(context.rest[1] ?? context.parsed.traceId);
@@ -892,6 +965,7 @@ async function pollAgentResult(context: any, traceId: string, acceptedBody: any)
async function workbenchCommand(context: any) {
const subcommand = context.rest[0] || "summary";
if (wantsHelp(context)) return workbenchHelp();
if (subcommand === "restore" || subcommand === "status") return workbenchRestoreCommand(context, subcommand);
if (subcommand === "watch") return workbenchWatchCommand(context);
if (subcommand === "reset") return workbenchResetCommand(context);
@@ -921,6 +995,20 @@ async function workbenchCommand(context: any) {
return ok("client.workbench.summary", { status: failed ? "degraded" : "succeeded", baseUrl: baseUrl(context.parsed, context.env), devicePodId: podId, probes }, failed ? "degraded" : "succeeded");
}
function workbenchHelp() {
return ok("client.workbench.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"summary [--pod-id POD_ID]",
"restore [--profile NAME]",
"status [--profile NAME]",
"watch [--after-revision REVISION]",
"reset --confirm"
]
});
}
async function workbenchRestoreCommand(context: any, subcommand: string) {
const workspace = await restoreWorkbenchWorkspace(context, { quiet: false });
return ok(`client.workbench.${subcommand}`, {
@@ -954,6 +1042,7 @@ async function workbenchResetCommand(context: any) {
}
async function requestCommand(context: any) {
if (wantsHelp(context)) return requestHelp();
const method = requiredText(context.rest[0], "method").toUpperCase();
const pathName = normalizeRequestPath(requiredText(context.rest[1], "path"));
const body = await requestBodyValue(context);
@@ -976,7 +1065,20 @@ async function requestCommand(context: any) {
});
}
function requestHelp() {
return ok("client.request.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"GET /path [--full]",
"POST /path --body-json '{...}' [--full]",
"METHOD /path --body-file FILE"
]
});
}
async function rpcCommand(context: any) {
if (wantsHelp(context)) return rpcHelp();
const method = requiredText(context.rest[0] ?? context.parsed.method, "method");
const params = await rpcParamsValue(context);
const traceId = text(context.parsed.traceId) || makeId("trc");
@@ -1006,6 +1108,23 @@ async function rpcCommand(context: any) {
});
}
function rpcHelp() {
return ok("client.rpc.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"METHOD [--params-json '{...}']",
"METHOD --params-file FILE",
"METHOD --full"
]
});
}
function wantsHelp(context: any) {
const token = text(context.rest?.[0]);
return context.parsed.help === true || token === "help" || token === "--help" || token === "-h";
}
async function rpcParamsValue(context: any) {
const { parsed } = context;
const raw = typeof parsed.paramsJson === "string"