Files
pikasTech-HWLAB/tools/src/hwlab-cli-lib.ts
T
2026-05-31 22:54:50 +08:00

1554 lines
68 KiB
TypeScript

import { createHash } from "node:crypto";
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
const VERSION = "0.2.0-client";
const CLI_NAME = "hwlab-cli";
const DEFAULT_BASE_URL = "http://74.48.78.17:19666";
const DEFAULT_TIMEOUT_MS = 30000;
const DEFAULT_AGENT_TIMEOUT_MS = 120000;
const DEFAULT_POLL_INTERVAL_MS = 1000;
const HARNESS_WAIT_DEFAULT_TIMEOUT_MS = 50000;
const HARNESS_WAIT_MAX_TIMEOUT_MS = 50000;
const DEFAULT_GATEWAY_SESSION_ID = "gws_D601_F103";
const DEFAULT_GATEWAY_RESOURCE_ID = "res_windows_host";
const DEFAULT_GATEWAY_CAPABILITY_ID = "cap_windows_cmd_exec";
const DEFAULT_GATEWAY_PROJECT_ID = "prj_mvp_topology";
type EnvLike = Record<string, string | undefined>;
type ParsedArgs = Record<string, unknown> & { _: string[] };
type FetchLike = typeof fetch;
type CliOptions = {
env?: EnvLike;
fetchImpl?: FetchLike;
stdinText?: string;
cwd?: string;
now?: () => string;
sleep?: (ms: number) => Promise<void>;
};
export async function main(argv = process.argv.slice(2), options: CliOptions = {}) {
const result = await runHwlabCli(argv, options);
console.log(JSON.stringify(result.payload, null, 2));
process.exitCode = result.exitCode;
}
export async function runHwlabCli(argv: string[], options: CliOptions = {}) {
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
try {
const parsed = parseOptions(argv);
const target = parsed._[0] || "help";
const context = {
parsed,
env,
fetchImpl: options.fetchImpl ?? fetch,
stdinText: options.stdinText,
cwd: options.cwd ?? process.cwd(),
now,
sleep: options.sleep ?? wait
};
const payload = target === "client" ? await clientCommand({ ...context, rest: parsed._.slice(1) }) : help();
return { exitCode: payload.ok === false ? 1 : 0, payload: withMeta(payload, now) };
} catch (error) {
return { exitCode: 1, payload: withMeta(failure("hwlab-cli", error), now) };
}
}
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 (group === "auth") return authCommand(next);
if (group === "device-pods" || group === "device-pod") return devicePodsCommand(next);
if (group === "runtime") return runtimeCommand(next);
if (group === "gateway") return gatewayCommand(next);
if (group === "agent") return agentCommand(next);
if (["harness", "harness-ops", "harness-opt"].includes(group)) return harnessCommand(next);
if (group === "workbench") return workbenchCommand(next);
if (group === "rpc") return rpcCommand(next);
if (group === "request") return requestCommand(next);
throw cliError("unsupported_client_command", `unsupported client command: ${group}`, { group });
}
function help() {
return ok("help", {
version: VERSION,
contractVersion: "hwlab-web-equivalent-client-v1",
mode: "short-connection-client",
defaultBaseUrl: DEFAULT_BASE_URL,
stateFile: ".state/hwlab-cli/session.json",
serviceRuntime: false,
imagePublished: false,
jobTemplate: false,
usage: [
"hwlab-cli client auth login --base-url URL --username USER --password-env HWLAB_PASSWORD",
"hwlab-cli client auth status",
"hwlab-cli client auth session",
"hwlab-cli client device-pods list",
"hwlab-cli client device-pods status device-pod-71-freq",
"hwlab-cli client runtime routes",
"hwlab-cli client gateway sessions",
"hwlab-cli client gateway pressure --gateway-session-id gws_D601_F103 --api-base-url http://API:19667",
"hwlab-cli client workbench summary --pod-id device-pod-71-freq",
"hwlab-cli client request GET /v1/access/status [--full]",
"hwlab-cli client rpc system.health [--full]",
"hwlab-cli client agent send --message TEXT --provider-profile deepseek --timeout-ms 120000",
"hwlab-cli client agent result TRACE_ID",
"hwlab-cli client agent trace TRACE_ID",
"hwlab-cli client agent inspect --trace-id TRACE_ID",
"hwlab-cli client agent cancel TRACE_ID",
"hwlab-cli client harness submit --message TEXT --provider-profile deepseek",
"hwlab-cli client harness wait TRACE_ID --timeout-ms 50000",
"hwlab-cli client harness audit TRACE_ID --require-bootsharp"
]
});
}
async function authCommand(context: any) {
const subcommand = context.rest[0] || "session";
if (subcommand === "login") return authLogin(context);
if (subcommand === "status") return authStatus(context);
if (subcommand === "session") {
const localSession = sessionStateSummary(await readSessionState({ parsed: context.parsed, env: context.env, cwd: context.cwd ?? process.cwd() }));
const response = await requestJson({ ...context, method: "GET", path: "/auth/session" });
return responsePayload("client.auth.session", response, context, { route: route("GET", "/auth/session"), localSession });
}
if (subcommand === "logout") {
const response = await requestJson({ ...context, method: "POST", path: "/auth/logout" });
await clearSession(context);
return responsePayload("client.auth.logout", response, context, { route: route("POST", "/auth/logout"), localSessionCleared: true });
}
throw cliError("unsupported_auth_command", `unsupported auth command: ${subcommand}`, { subcommand });
}
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", {
baseUrl: baseUrl(context.parsed, context.env),
stateFile: localSession.stateFile,
localSession,
nextCommands: authNextCommands({ baseUrl: baseUrl(context.parsed, context.env), username: text(context.parsed.username ?? context.env.HWLAB_USERNAME) || "admin", stateFile: localSession.stateFile })
});
}
async function authLogin(context: any) {
const { parsed, env } = context;
const username = text(parsed.username ?? env.HWLAB_USERNAME) || "admin";
const password = await passwordValue(context);
const response = await requestJson({ ...context, method: "POST", path: "/auth/login", body: { username, password }, auth: false });
const cookie = cookieFromResponse(response);
const success = isHttpSuccess(response) && response.body?.authenticated === true;
if (success && cookie) {
await saveSession(context, {
baseUrl: baseUrl(parsed, env),
cookie,
user: safeUser(response.body?.user ?? response.body?.actor),
expiresAt: textOrNull(response.body?.expiresAt),
updatedAt: context.now()
});
}
return responsePayload("client.auth.login", response, context, {
route: route("POST", "/auth/login"),
username,
cookieStored: Boolean(success && cookie),
body: authBodySummary(response.body)
});
}
async function devicePodsCommand(context: any) {
const subcommand = context.rest[0] || "list";
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) });
}
const podId = requiredText(context.parsed.podId ?? context.rest[1], "podId");
if (subcommand === "status" || subcommand === "show") {
const pathName = `/v1/device-pods/${encodeURIComponent(podId)}/status`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.device-pods.status", response, context, { route: route("GET", pathName), devicePodId: podId, body: responseBodyForCli(response.body, context.parsed) });
}
if (subcommand === "events") {
const limit = numberOption(context.parsed.limit) ?? 120;
const pathName = `/v1/device-pods/${encodeURIComponent(podId)}/events?limit=${encodeURIComponent(String(limit))}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.device-pods.events", response, context, { route: route("GET", pathName), devicePodId: podId, body: responseBodyForCli(response.body, context.parsed) });
}
if (subcommand === "probe") return devicePodProbe(context, podId);
throw cliError("unsupported_device_pods_command", `unsupported device-pods command: ${subcommand}`, { subcommand });
}
async function runtimeCommand(context: any) {
const subcommand = context.rest[0] || "routes";
if (subcommand !== "routes" && subcommand !== "pods") {
throw cliError("unsupported_runtime_command", `unsupported runtime command: ${subcommand}`, { subcommand });
}
const response = await requestJson({ ...context, method: "GET", path: "/v1/live-builds", auth: false });
const services = liveBuildServices(response.body);
const requested = text(context.parsed.serviceId);
const selected = services.filter((item: any) => {
const serviceId = text(item?.serviceId ?? item?.id ?? item?.name);
return !requested || serviceId === requested;
}).map(runtimeRouteSummary).filter(Boolean);
return responsePayload("client.runtime.routes", response, context, {
route: route("GET", "/v1/live-builds"),
discovery: {
ok: true,
source: "cloud-web:/v1/live-builds",
serviceCount: services.length,
routeCount: selected.length,
readyRouteCount: selected.filter((item: any) => Boolean(item.unideskRoute)).length
},
serviceId: requested || null,
items: selected,
body: responseBodyForCli(response.body, context.parsed)
});
}
function liveBuildServices(body: any) {
const builds = Array.isArray(body?.builds) ? body.builds : [];
if (builds.length > 0) return builds;
return Array.isArray(body?.services) ? body.services : [];
}
function runtimeRouteSummary(value: any) {
const serviceId = text(value?.serviceId ?? value?.id ?? value?.name);
const runtime = value?.runtime && typeof value.runtime === "object" ? value.runtime : {};
const pod = runtime?.pod && typeof runtime.pod === "object" ? runtime.pod : {};
const podText = typeof value?.pod === "string" ? value.pod : typeof runtime?.pod === "string" ? runtime.pod : "";
const podName = text(
value?.podName ??
runtime?.podName ??
pod?.name ??
podText ??
value?.live?.podName ??
value?.health?.podName
);
const namespace = text(
value?.namespace ??
runtime?.namespace ??
pod?.namespace ??
value?.live?.namespace ??
value?.desiredState?.namespace
) || "hwlab-v02";
const container = text(
value?.container ??
value?.containerName ??
runtime?.container ??
runtime?.containerName ??
pod?.container ??
pod?.containerName ??
value?.live?.containerName
) || defaultRuntimeContainer(serviceId);
if (!serviceId && !podName) return null;
const unideskRoute = normalizeUnideskPodRoute(
text(value?.unideskRoute ?? runtime?.unideskRoute ?? pod?.unideskRoute),
namespace,
podName,
container
);
return pruneUndefined({
serviceId,
podName,
namespace,
container,
unideskRoute,
source: "cloud-web:/v1/live-builds",
useWith: unideskRoute ? `bun scripts/cli.ts ssh '${unideskRoute}' script -- 'pwd'` : undefined
});
}
function defaultRuntimeContainer(serviceId: string) {
if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-mgr", "hwlab-agent-skills"].includes(serviceId)) return serviceId;
return "";
}
function normalizeUnideskPodRoute(routeValue: string, namespace: string, podName: string, container: string) {
if (podName) return `G14:k3s:${namespace}:pod:${podName}${container ? `:${container}` : ""}`;
if (!routeValue) return null;
return routeValue.replace(/:pod\//gu, ":pod:");
}
async function devicePodProbe(context: any, podId: string) {
const encoded = encodeURIComponent(podId);
const paths = [
`/v1/device-pods/${encoded}/debug-probe/chip-id`,
`/v1/device-pods/${encoded}/io-probe/uart/1`,
`/v1/device-pods/${encoded}/io-probe/uart/1/tail?maxBytes=${encodeURIComponent(String(numberOption(context.parsed.maxBytes) ?? 12000))}`
];
const probes = await Promise.all(paths.map(async (pathName) => {
try {
const response = await requestJson({ ...context, method: "GET", path: pathName });
return compactProbe("GET", pathName, response);
} catch (error) {
return { route: route("GET", pathName), ok: false, error: errorSummary(error) };
}
}));
const failed = probes.some((probe) => probe.ok === false);
return ok("client.device-pods.probe", { status: failed ? "degraded" : "succeeded", devicePodId: podId, probes }, failed ? "degraded" : "succeeded");
}
async function gatewayCommand(context: any) {
const subcommand = context.rest[0] || "sessions";
if (["help", "--help", "-h"].includes(subcommand)) return gatewayHelp();
const apiContext = gatewayApiContext(context);
if (subcommand === "sessions") {
const response = await requestJson({
...apiContext,
method: "GET",
path: "/v1/gateway/sessions",
auth: context.parsed.noAuth === true ? false : context.parsed.auth === true
});
return responsePayload("client.gateway.sessions", response, apiContext, {
route: route("GET", "/v1/gateway/sessions"),
body: responseBodyForCli(response.body, context.parsed)
});
}
if (subcommand === "invoke") {
const command = requiredText(context.parsed.command ?? context.rest.slice(1).join(" "), "command");
const result = await invokeGatewayShell(apiContext, {
name: "invoke",
command,
timeoutMs: numberOption(context.parsed.commandTimeoutMs) ?? numberOption(context.parsed.timeoutMs) ?? 30000
});
return ok("client.gateway.invoke", { status: result.ok ? "succeeded" : "failed", result }, result.ok ? "succeeded" : "failed");
}
if (subcommand === "pressure") return gatewayPressure(apiContext);
throw cliError("unsupported_gateway_command", `unsupported gateway command: ${subcommand}`, { subcommand });
}
function gatewayHelp() {
return ok("client.gateway.help", {
serviceRuntime: false,
imagePublished: false,
commands: [
"sessions [--api-base-url URL|--base-url URL]",
"invoke --command CMD [--gateway-session-id ID] [--api-base-url URL]",
"pressure [--gateway-session-id ID] [--api-base-url URL] [--large-bytes N] [--parallel N]"
]
});
}
function gatewayApiContext(context: any) {
const apiBaseUrl = text(
context.parsed.apiBaseUrl ??
context.parsed.cloudApiUrl ??
context.env.HWLAB_CLOUD_API_URL ??
context.env.HWLAB_CLOUD_API_BASE_URL
);
if (!apiBaseUrl) return context;
return { ...context, parsed: { ...context.parsed, baseUrl: apiBaseUrl } };
}
async function gatewayPressure(context: any) {
const largeBytes = boundedInteger(context.parsed.largeBytes, 131072, 4096, 1048576);
const parallel = boundedInteger(context.parsed.parallel, 8, 0, 32);
const requestTimeoutMs = boundedInteger(context.parsed.requestTimeoutMs, 45000, 1000, 180000);
const timeoutScenarioMs = boundedInteger(context.parsed.timeoutScenarioMs, 1000, 250, 30000);
const startedAt = Date.now();
const scenarios = gatewayPressureScenarios({ largeBytes, timeoutScenarioMs });
const results = [];
for (const scenario of scenarios) {
results.push(await invokeGatewayShell(context, { ...scenario, requestTimeoutMs }));
}
if (parallel > 0) {
const parallelResults = await Promise.all(Array.from({ length: parallel }, (_, index) => invokeGatewayShell(context, {
name: `parallel-${index + 1}`,
kind: "parallel",
command: powershellEncodedCommand(`Start-Sleep -Milliseconds 3500; Write-Output 'parallel-${index + 1}-ok'`),
timeoutMs: 12000,
requestTimeoutMs,
allowGatewayBusy: true,
marker: `parallel-${index + 1}-ok`
})));
results.push(...parallelResults);
}
const failures = results.filter((item) => item.ok !== true);
return ok("client.gateway.pressure", {
status: failures.length > 0 ? "failed" : "succeeded",
baseUrl: baseUrl(context.parsed, context.env),
gatewaySessionId: gatewaySelector(context).gatewaySessionId,
scenarioCount: results.length,
failedCount: failures.length,
elapsedMs: Date.now() - startedAt,
config: { largeBytes, parallel, requestTimeoutMs, timeoutScenarioMs },
results,
recommendation: failures.length > 0
? "Gateway transport pressure found failures; inspect failed result.request/result.rpcSummary before continuing device-pod work."
: "Gateway transport pressure passed: large stdout/stderr and over-capacity parallel requests returned bounded structured results without request blackholes."
}, failures.length > 0 ? "failed" : "succeeded");
}
function gatewayPressureScenarios({ largeBytes, timeoutScenarioMs }: { largeBytes: number; timeoutScenarioMs: number }) {
return [
{ name: "small-stdout", kind: "small", command: "echo hwlab-gateway-pressure-small-ok", timeoutMs: 10000, marker: "hwlab-gateway-pressure-small-ok" },
{ name: "large-stdout", kind: "large-stdout", command: powershellEncodedCommand(`[Console]::Out.Write(('O' * ${largeBytes}))`), timeoutMs: 20000, expectStdoutTruncated: true },
{ name: "long-single-line", kind: "long-line", command: powershellEncodedCommand(`[Console]::Out.Write(('L' * ${largeBytes}))`), timeoutMs: 20000, expectStdoutTruncated: true },
{ name: "stderr-flood", kind: "stderr", command: powershellEncodedCommand(`[Console]::Error.Write(('E' * ${largeBytes}))`), timeoutMs: 20000, expectStderrTruncated: true },
{ name: "timeout-kill", kind: "timeout", command: powershellEncodedCommand("Start-Sleep -Seconds 5; Write-Output 'timeout-missed'"), timeoutMs: timeoutScenarioMs, expectTimedOut: true }
];
}
function powershellEncodedCommand(script: string) {
const encoded = Buffer.from(script, "utf16le").toString("base64");
return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
}
async function invokeGatewayShell(context: any, scenario: any) {
const selector = gatewaySelector(context);
const traceId = text(context.parsed.traceId) || makeId(`trc_gateway_${scenario.name.replace(/[^a-z0-9]+/giu, "_")}`);
const requestId = text(context.parsed.id) || makeId(`req_gateway_${scenario.name.replace(/[^a-z0-9]+/giu, "_")}`);
const body = {
gatewaySessionId: selector.gatewaySessionId,
resourceId: selector.resourceId,
capabilityId: selector.capabilityId,
projectId: selector.projectId,
input: {
command: scenario.command,
timeoutMs: scenario.timeoutMs
}
};
const startedAt = Date.now();
const response = await requestJson({
...context,
method: "POST",
path: "/v1/rpc/hardware.invoke.shell",
body,
auth: context.parsed.auth === true,
timeoutMs: scenario.requestTimeoutMs ?? numberOption(context.parsed.requestTimeoutMs) ?? DEFAULT_TIMEOUT_MS,
extraHeaders: {
"x-trace-id": traceId,
"x-request-id": requestId
}
});
return summarizeGatewayScenario({ scenario, response, traceId, requestId, startedAt });
}
function gatewaySelector(context: any) {
return {
gatewaySessionId: text(context.parsed.gatewaySessionId) || DEFAULT_GATEWAY_SESSION_ID,
resourceId: text(context.parsed.resourceId) || DEFAULT_GATEWAY_RESOURCE_ID,
capabilityId: text(context.parsed.capabilityId) || DEFAULT_GATEWAY_CAPABILITY_ID,
projectId: text(context.parsed.projectId) || DEFAULT_GATEWAY_PROJECT_ID
};
}
function summarizeGatewayScenario({ scenario, response, traceId, requestId, startedAt }: any) {
const rpc = response.body && typeof response.body === "object" ? response.body : {};
const result = rpc.result && typeof rpc.result === "object" ? rpc.result : {};
const dispatch = result.dispatch && typeof result.dispatch === "object" ? result.dispatch : {};
const stdout = String(dispatch.stdout ?? "");
const stderr = String(dispatch.stderr ?? "");
const gatewayBusy = dispatch.error?.data?.reason === "gateway_busy" || /gateway is busy/iu.test(String(dispatch.error?.message ?? dispatch.message ?? ""));
const okByKind = gatewayScenarioPass({ scenario, response, dispatch, stdout, stderr, gatewayBusy });
return pruneUndefined({
name: scenario.name,
kind: scenario.kind,
ok: okByKind.ok,
reason: okByKind.reason,
httpStatus: response.status,
elapsedMs: Date.now() - startedAt,
requestElapsedMs: response.elapsedMs,
traceId,
requestId,
operationId: result.operationId ?? dispatch.operationId,
rpcStatus: result.status,
dispatchStatus: dispatch.dispatchStatus,
shellExecuted: dispatch.shellExecuted,
exitCode: dispatch.exitCode,
timedOut: dispatch.timedOut,
gatewayBusy,
stdoutBytes: Buffer.byteLength(stdout, "utf8"),
stderrBytes: Buffer.byteLength(stderr, "utf8"),
stdoutTruncated: dispatch.stdoutTruncated,
stderrTruncated: dispatch.stderrTruncated,
stdoutSha256: stdout ? sha256(stdout) : undefined,
stderrSha256: stderr ? sha256(stderr) : undefined,
stdoutPreview: stdout ? preview(stdout, 160) : undefined,
stderrPreview: stderr ? preview(stderr, 160) : undefined,
error: rpc.error ?? dispatch.error,
message: dispatch.message,
commandPreview: preview(scenario.command, 220)
});
}
function gatewayScenarioPass({ scenario, response, dispatch, stdout, stderr, gatewayBusy }: any) {
if (!isHttpSuccess(response)) return { ok: false, reason: `http_${response.status}` };
if (scenario.allowGatewayBusy && gatewayBusy) return { ok: true, reason: "structured_gateway_busy" };
if (scenario.expectTimedOut) {
return dispatch.timedOut === true || dispatch.dispatchStatus === "timed_out"
? { ok: true, reason: "timeout_structured" }
: { ok: false, reason: "timeout_not_observed" };
}
if (scenario.expectStdoutTruncated && dispatch.stdoutTruncated !== true) return { ok: false, reason: "stdout_not_truncated" };
if (scenario.expectStderrTruncated && dispatch.stderrTruncated !== true) return { ok: false, reason: "stderr_not_truncated" };
if (scenario.marker && !stdout.includes(scenario.marker)) return { ok: false, reason: "marker_missing" };
if (dispatch.exitCode !== 0) return { ok: false, reason: `exit_${dispatch.exitCode}` };
if (scenario.expectStdoutTruncated && Buffer.byteLength(stdout, "utf8") === 0) return { ok: false, reason: "stdout_empty" };
if (scenario.expectStderrTruncated && Buffer.byteLength(stderr, "utf8") === 0) return { ok: false, reason: "stderr_empty" };
return { ok: true, reason: "completed" };
}
async function agentCommand(context: any) {
const subcommand = context.rest[0] || "send";
if (subcommand === "send") return agentSend(context);
if (subcommand === "result") {
const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.agent.result", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed) });
}
if (subcommand === "trace") {
const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.agent.trace", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed) });
}
if (subcommand === "inspect") {
const pathName = agentInspectPath(context);
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.agent.inspect", response, context, { route: route("GET", pathName), body: responseBodyForCli(response.body, context.parsed) });
}
if (subcommand === "cancel") {
const traceId = requiredText(context.rest[1] ?? context.parsed.traceId, "traceId");
const response = await requestJson({ ...context, method: "POST", path: "/v1/agent/chat/cancel", body: clean({ traceId, conversationId: text(context.parsed.conversationId), sessionId: text(context.parsed.sessionId) }), extraHeaders: { "x-trace-id": traceId } });
return responsePayload("client.agent.cancel", response, context, { route: route("POST", "/v1/agent/chat/cancel"), traceId });
}
throw cliError("unsupported_agent_command", `unsupported agent command: ${subcommand}`, { subcommand });
}
function agentInspectPath(context: any) {
const params = new URLSearchParams();
const traceId = text(context.rest[1] ?? context.parsed.traceId);
const conversationId = text(context.parsed.conversationId);
const sessionId = text(context.parsed.sessionId);
const threadId = text(context.parsed.threadId);
if (traceId) params.set("traceId", requiredTraceId(traceId));
if (conversationId) params.set("conversationId", conversationId);
if (sessionId) params.set("sessionId", sessionId);
if (threadId) params.set("threadId", threadId);
if ([...params.keys()].length === 0) {
throw cliError("missing_inspect_query", "client agent inspect requires --trace-id, --conversation-id, --session-id, or --thread-id");
}
return `/v1/agent/chat/inspect?${params.toString()}`;
}
async function harnessCommand(context: any) {
const subcommand = context.rest[0] || "help";
if (["help", "--help", "-h"].includes(subcommand)) return harnessHelp();
if (subcommand === "health") {
const response = await requestJson({ ...context, method: "GET", path: "/health/live", auth: false });
return responsePayload("client.harness.health", response, context, {
route: route("GET", "/health/live"),
body: responseBodyForCli(response.body, context.parsed)
});
}
if (subcommand === "submit") return harnessSubmit({ ...context, rest: context.rest.slice(1) });
if (subcommand === "result") return harnessResult({ ...context, rest: context.rest.slice(1) });
if (subcommand === "trace") return harnessTrace({ ...context, rest: context.rest.slice(1) });
if (subcommand === "wait") return harnessWait({ ...context, rest: context.rest.slice(1) });
if (subcommand === "audit") return harnessAudit({ ...context, rest: context.rest.slice(1) });
throw cliError("unsupported_harness_command", `unsupported harness command: ${subcommand}`, { subcommand });
}
function harnessHelp() {
return ok("client.harness.help", {
serviceRuntime: false,
imagePublished: false,
aliases: ["harness", "harness-ops", "harness-opt"],
commands: [
"health [--base-url URL]",
"submit --message TEXT|--message-file PATH [--conversation-id ID] [--trace-id ID] [--provider-profile deepseek|codex-api]",
"result TRACE_ID [--full]",
"trace TRACE_ID [--limit N] [--full]",
"wait TRACE_ID [--timeout-ms N] [--poll-interval-ms N]",
"audit TRACE_ID|--trace-file PATH [--strict] [--require-bootsharp]"
]
});
}
async function harnessSubmit(context: any) {
const message = await messageValue(context);
if (!message.trim()) {
throw cliError(
"message_required",
"client harness submit requires --message, --message-file, or stdin text"
);
}
const traceId = text(context.parsed.traceId) || makeId("trc");
const conversationId = text(context.parsed.conversationId) || makeId("cnv");
const body = clean({
message,
conversationId,
sessionId: text(context.parsed.sessionId),
threadId: text(context.parsed.threadId),
traceId,
providerProfile: text(context.parsed.providerProfile) || "deepseek",
timeoutMs: numberOption(context.parsed.agentTimeoutMs ?? context.parsed.timeoutMs),
hardTimeoutMs: numberOption(context.parsed.hardTimeoutMs),
gatewayShellTimeoutMs: numberOption(context.parsed.gatewayShellTimeoutMs),
shortConnection: true,
projectId: text(context.parsed.projectId) || "prj_harness_ops"
});
const response = await requestJson({
...context,
method: "POST",
path: "/v1/agent/chat",
body,
timeoutMs: numberOption(context.parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS,
extraHeaders: {
"x-trace-id": traceId,
prefer: "respond-async",
"x-hwlab-short-connection": "1"
}
});
return responsePayload("client.harness.submit", response, context, {
route: route("POST", "/v1/agent/chat"),
traceId,
conversationId,
accepted: response.body?.accepted ?? false,
resultUrl: response.body?.resultUrl ?? `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
traceUrl: response.body?.traceUrl ?? `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function harnessResult(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
return responsePayload("client.harness.result", response, context, {
route: route("GET", pathName),
traceId,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function harnessTrace(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const pathName = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`;
const response = await requestJson({ ...context, method: "GET", path: pathName });
const body = context.parsed.full === true
? response.body
: compactTraceBody(response.body, numberOption(context.parsed.limit) ?? 24);
return responsePayload("client.harness.trace", response, context, {
route: route("GET", pathName),
traceId,
body
});
}
async function harnessWait(context: any) {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const requestedTimeoutMs = numberOption(context.parsed.timeoutMs) ?? HARNESS_WAIT_DEFAULT_TIMEOUT_MS;
const timeoutMs = Math.min(requestedTimeoutMs, HARNESS_WAIT_MAX_TIMEOUT_MS);
const pollIntervalMs = Math.max(numberOption(context.parsed.pollIntervalMs) ?? 1000, 250);
const waitPolicy = harnessWaitPolicy(traceId, requestedTimeoutMs, timeoutMs);
const deadline = Date.now() + timeoutMs;
const pathName = `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
let polls = 0;
let lastResponse: any = null;
while (Date.now() < deadline) {
polls += 1;
lastResponse = await requestJson({
...context,
method: "GET",
path: pathName,
timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000)
});
if (lastResponse.status === 200 && lastResponse.body?.status && lastResponse.body.status !== "running") {
return responsePayload("client.harness.wait", lastResponse, context, {
route: route("GET", pathName),
traceId,
polls,
timedOut: false,
timeoutMsRequested: requestedTimeoutMs,
timeoutMsEffective: timeoutMs,
waitPolicy,
body: responseBodyForCli(lastResponse.body, context.parsed)
});
}
await context.sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
}
return ok("client.harness.wait", {
status: "timeout",
traceId,
route: route("GET", pathName),
polls,
timedOut: true,
timeoutMsRequested: requestedTimeoutMs,
timeoutMsEffective: timeoutMs,
waitPolicy,
body: lastResponse ? responseBodyForCli(lastResponse.body, context.parsed) : null
}, "timeout");
}
function harnessWaitPolicy(traceId: string, requestedTimeoutMs: number, timeoutMs: number) {
return {
remotePassthroughSafe: true,
capped: requestedTimeoutMs > timeoutMs,
reason: "UniDesk ssh/tran has a 60s top-level runtime timeout; keep harness wait below that and continue with short polling.",
nextCommands: [
`hwlab-cli client harness result ${traceId}`,
`hwlab-cli client harness trace ${traceId} --limit 24`,
`hwlab-cli client harness audit ${traceId} --require-bootsharp`
]
};
}
async function harnessAudit(context: any) {
let traceObject: any;
if (typeof context.parsed.traceFile === "string") {
traceObject = JSON.parse(await readFile(path.resolve(context.cwd, context.parsed.traceFile), "utf8"));
} else {
const traceId = requiredTraceId(context.rest[0] ?? context.parsed.traceId);
const response = await requestJson({
...context,
method: "GET",
path: `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`
});
traceObject = response.body;
}
const report = auditTrace(traceObject, { requireBootsharp: context.parsed.requireBootsharp === true });
const status = context.parsed.strict === true && report.frictionSignals.length > 0
? "failed"
: "succeeded";
return ok("client.harness.audit", {
traceId: traceObject?.traceId ?? context.rest[0] ?? null,
...report
}, status);
}
async function agentSend(context: any) {
const { parsed } = context;
const message = text(parsed.message ?? parsed.text) || text(context.stdinText);
if (!message) throw cliError("message_required", "client agent send requires --message or stdin text");
const traceId = text(parsed.traceId) || makeId("trc");
const conversationId = text(parsed.conversationId) || makeId("cnv");
const requestBody = clean({
message,
conversationId,
sessionId: text(parsed.sessionId),
threadId: text(parsed.threadId),
traceId,
providerProfile: text(parsed.providerProfile) || "deepseek",
gatewayShellTimeoutMs: numberOption(parsed.gatewayShellTimeoutMs),
shortConnection: true,
projectId: text(parsed.projectId) || "prj_device_pod_workbench"
});
const accepted = await requestJson({
...context,
method: "POST",
path: "/v1/agent/chat",
body: requestBody,
timeoutMs: numberOption(parsed.submitTimeoutMs) ?? DEFAULT_TIMEOUT_MS,
extraHeaders: {
"x-trace-id": traceId,
"prefer": "respond-async",
"x-hwlab-short-connection": "1"
}
});
if (!isHttpSuccess(accepted)) {
return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId });
}
if (parsed.noWait === true) {
return responsePayload("client.agent.send", accepted, context, { route: route("POST", "/v1/agent/chat"), traceId, conversationId, waited: false });
}
const result = await pollAgentResult(context, traceId, accepted.body);
return ok("client.agent.send", {
status: result.final ? "succeeded" : "timeout",
route: route("POST", "/v1/agent/chat"),
traceId,
conversationId,
accepted: compactBody(accepted.body),
result: result.response ? compactResponse(result.response) : null,
polls: result.polls,
timeoutMs: result.timeoutMs,
resultUrl: result.resultPath
}, result.final ? "succeeded" : "timeout");
}
async function pollAgentResult(context: any, traceId: string, acceptedBody: any) {
const timeoutMs = numberOption(context.parsed.timeoutMs) ?? DEFAULT_AGENT_TIMEOUT_MS;
const pollIntervalMs = numberOption(context.parsed.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS;
const startedAt = Date.now();
const resultPath = text(acceptedBody?.resultUrl) || `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
let polls = 0;
let lastResponse = null;
while (Date.now() - startedAt < timeoutMs) {
polls += 1;
const response = await requestJson({ ...context, method: "GET", path: resultPath, timeoutMs: Math.min(DEFAULT_TIMEOUT_MS, pollIntervalMs + 2000) });
lastResponse = response;
if (response.status === 200 && response.body?.status && response.body.status !== "running") {
return { final: true, response, polls, timeoutMs, resultPath };
}
await context.sleep(pollIntervalMs);
}
return { final: false, response: lastResponse, polls, timeoutMs, resultPath };
}
async function workbenchCommand(context: any) {
const subcommand = context.rest[0] || "summary";
if (subcommand !== "summary") throw cliError("unsupported_workbench_command", `unsupported workbench command: ${subcommand}`, { subcommand });
const podId = text(context.parsed.podId) || "device-pod-71-freq";
const encodedPodId = encodeURIComponent(podId);
const paths = [
"/health/live",
"/v1",
"/v1/live-builds",
"/v1/device-pods",
`/v1/device-pods/${encodedPodId}/status`,
`/v1/device-pods/${encodedPodId}/events?limit=${encodeURIComponent(String(numberOption(context.parsed.limit) ?? 120))}`,
`/v1/device-pods/${encodedPodId}/debug-probe/chip-id`,
`/v1/device-pods/${encodedPodId}/io-probe/uart/1`,
`/v1/device-pods/${encodedPodId}/io-probe/uart/1/tail?maxBytes=${encodeURIComponent(String(numberOption(context.parsed.maxBytes) ?? 12000))}`
];
const probes = await Promise.all(paths.map(async (pathName) => {
try {
const response = await requestJson({ ...context, method: "GET", path: pathName, auth: !["/health/live", "/v1", "/v1/live-builds"].includes(pathName) });
return compactProbe("GET", pathName, response);
} catch (error) {
return { route: route("GET", pathName), ok: false, error: errorSummary(error) };
}
}));
const failed = probes.some((probe) => probe.ok === false);
return ok("client.workbench.summary", { status: failed ? "degraded" : "succeeded", baseUrl: baseUrl(context.parsed, context.env), devicePodId: podId, probes }, failed ? "degraded" : "succeeded");
}
async function requestCommand(context: any) {
const method = requiredText(context.rest[0], "method").toUpperCase();
const pathName = normalizeRequestPath(requiredText(context.rest[1], "path"));
const body = await requestBodyValue(context);
const traceId = text(context.parsed.traceId);
const response = await requestJson({
...context,
method,
path: pathName,
body,
auth: context.parsed.public === true ? false : true,
extraHeaders: clean({
...(traceId ? { "x-trace-id": traceId } : {}),
...(context.parsed.preferAsync === true ? { prefer: "respond-async" } : {})
})
});
return responsePayload("client.request", response, context, {
route: route(method, pathName),
requestedPath: pathName,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function rpcCommand(context: any) {
const method = requiredText(context.rest[0] ?? context.parsed.method, "method");
const params = await rpcParamsValue(context);
const traceId = text(context.parsed.traceId) || makeId("trc");
const id = text(context.parsed.id) || makeId("req");
const environment = text(context.parsed.environment) || "dev";
const serviceId = text(context.parsed.serviceId) || "hwlab-cloud-web";
const body = {
jsonrpc: "2.0",
id,
method,
params,
meta: { traceId, serviceId, environment }
};
const response = await requestJson({
...context,
method: "POST",
path: "/json-rpc",
body,
extraHeaders: { "x-trace-id": traceId }
});
return responsePayload("client.rpc", response, context, {
route: route("POST", "/json-rpc"),
rpcMethod: method,
traceId,
requestId: id,
body: responseBodyForCli(response.body, context.parsed)
});
}
async function rpcParamsValue(context: any) {
const { parsed } = context;
const raw = typeof parsed.paramsJson === "string"
? parsed.paramsJson
: typeof parsed.params === "string"
? parsed.params
: typeof parsed.paramsFile === "string"
? await readFile(path.resolve(context.cwd, parsed.paramsFile), "utf8")
: "{}";
try {
const parsedParams = JSON.parse(raw);
if (!parsedParams || typeof parsedParams !== "object" || Array.isArray(parsedParams)) throw new Error("params must be a JSON object");
return parsedParams;
} catch (error) {
throw cliError("invalid_params_json", "rpc params must be a JSON object", { message: error instanceof Error ? error.message : String(error) });
}
}
async function messageValue(context: any) {
if (typeof context.parsed.message === "string") return context.parsed.message;
if (typeof context.parsed.text === "string") return context.parsed.text;
if (typeof context.parsed.messageFile === "string") return await readFile(path.resolve(context.cwd, context.parsed.messageFile), "utf8");
return text(context.stdinText);
}
async function requestBodyValue(context: any) {
const { parsed } = context;
const raw = typeof parsed.bodyJson === "string"
? parsed.bodyJson
: typeof parsed.json === "string"
? parsed.json
: typeof parsed.bodyFile === "string"
? await readFile(path.resolve(context.cwd, parsed.bodyFile), "utf8")
: parsed.bodyStdin === true
? (context.stdinText !== undefined ? context.stdinText : await readStdin())
: null;
if (raw === null) return undefined;
try {
return JSON.parse(raw);
} catch (error) {
throw cliError("invalid_body_json", "request body must be valid JSON", { message: error instanceof Error ? error.message : String(error) });
}
}
function normalizeRequestPath(value: string) {
const pathName = value.trim();
if (!pathName.startsWith("/")) throw cliError("invalid_request_path", "request path must be a Cloud Web relative path starting with /", { path: value });
if (pathName.startsWith("//") || /^[a-z][a-z0-9+.-]*:\/\//iu.test(pathName)) throw cliError("invalid_request_path", "request path must not be an absolute URL", { path: value });
return pathName;
}
async function requestJson(context: any) {
const { parsed, env, cwd, method, path: pathName, body, auth = true, extraHeaders = {}, timeoutMs } = context;
const resolvedBaseUrl = baseUrl(parsed, env);
const url = `${resolvedBaseUrl}${pathName}`;
const requestTimeoutMs = timeoutMs ?? numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), requestTimeoutMs);
try {
const effectiveAuth = auth && parsed.noAuth !== true;
const explicitCookie = explicitAuthCookie(parsed, env);
const sessionState = effectiveAuth && !explicitCookie && parsed.noSession !== true
? await readSessionState({ parsed, env, cwd: cwd ?? process.cwd() })
: null;
const session = sessionState?.session ?? null;
let cookie = effectiveAuth ? explicitCookie || session?.cookie || null : null;
const authState: any = {
required: effectiveAuth,
baseUrl: resolvedBaseUrl,
username: text(parsed.username ?? env.HWLAB_USERNAME) || "admin",
stateFile: stateFile(parsed, cwd ?? process.cwd()),
localSession: sessionState ? sessionStateSummary(sessionState) : null,
cookieSource: cookie ? explicitCookie ? "explicit" : "state" : null,
autoLoginAttempted: false,
autoLoginStatus: null,
autoLoginHttpStatus: null,
retryAfterLogin: false
};
if (effectiveAuth && !cookie && autoAuthAllowed(parsed)) {
const login = await autoLoginSession(context, controller.signal);
authState.autoLoginAttempted = true;
authState.autoLoginStatus = login.status;
authState.autoLoginHttpStatus = login.httpStatus ?? null;
authState.cookieSource = login.cookie ? "auto-login" : null;
cookie = login.cookie ?? null;
}
let response = await sendJsonRequest({ context, url, method, body, cookie, extraHeaders, signal: controller.signal, pathName, timeoutMs: requestTimeoutMs });
if (effectiveAuth && response.status === 401 && !explicitCookie && autoAuthAllowed(parsed)) {
const login = await autoLoginSession(context, controller.signal, { force: true });
authState.autoLoginAttempted = true;
authState.autoLoginStatus = login.status;
authState.autoLoginHttpStatus = login.httpStatus ?? null;
if (login.cookie) {
authState.cookieSource = "auto-login";
authState.retryAfterLogin = true;
cookie = login.cookie;
response = await sendJsonRequest({ context, url, method, body, cookie, extraHeaders, signal: controller.signal, pathName, timeoutMs: requestTimeoutMs });
}
}
return { ...response, auth: authVisibility(authState), authDiagnosis: authDiagnosis(response, authState) };
} finally {
clearTimeout(timer);
}
}
async function sendJsonRequest({ context, url, method, body, cookie, extraHeaders, signal, pathName, timeoutMs }: any) {
const headers = clean({
accept: "application/json",
...(body ? { "content-type": "application/json" } : {}),
...(cookie ? { cookie } : {}),
...extraHeaders
});
const startedAt = Date.now();
try {
const response = await context.fetchImpl(url, { method, headers, body: body ? JSON.stringify(body) : undefined, signal });
const textBody = await response.text();
return { status: response.status, headers: response.headers, body: parseJson(textBody), url, method, path: pathName, elapsedMs: Date.now() - startedAt, timeoutMs };
} catch (error) {
const timedOut = signal?.aborted === true || error?.name === "AbortError";
const code = timedOut ? "request_timeout" : "request_failed";
const message = timedOut
? `${method} ${pathName} timed out after ${timeoutMs}ms`
: `${method} ${pathName} failed: ${error?.message ?? String(error)}`;
return {
status: 0,
headers: null,
body: { ok: false, status: "failed", error: { code, message } },
url,
method,
path: pathName,
elapsedMs: Date.now() - startedAt,
timeoutMs,
timedOut,
transportError: { code, name: error?.name ?? null, message: error?.message ?? String(error) }
};
}
}
async function autoLoginSession(context: any, signal: AbortSignal, { force = false } = {}) {
const password = await optionalPasswordValue(context);
if (!password) return { status: "credentials_missing", cookie: null };
const username = text(context.parsed.username ?? context.env.HWLAB_USERNAME) || "admin";
const response = await sendJsonRequest({
context,
url: `${baseUrl(context.parsed, context.env)}/auth/login`,
method: "POST",
pathName: "/auth/login",
body: { username, password },
cookie: null,
extraHeaders: {},
signal
});
const cookie = cookieFromResponse(response);
const success = isHttpSuccess(response) && response.body?.authenticated === true && cookie;
if (success) {
await saveSession(context, {
baseUrl: baseUrl(context.parsed, context.env),
cookie,
user: safeUser(response.body?.user ?? response.body?.actor),
expiresAt: textOrNull(response.body?.expiresAt),
updatedAt: context.now(),
refreshedAfter401: force === true || undefined
});
}
return { status: success ? "succeeded" : `failed_http_${response.status}`, httpStatus: response.status, cookie: success ? cookie : null };
}
function parseOptions(argv: string[]): ParsedArgs {
const out: ParsedArgs = { _: [] };
for (let index = 0; index < argv.length; index += 1) {
const item = argv[index] ?? "";
if (!item.startsWith("--")) { out._.push(item); continue; }
const eq = item.indexOf("=");
const rawKey = eq >= 0 ? item.slice(2, eq) : item.slice(2);
const key = rawKey.replace(/-([a-z])/gu, (_, c) => String(c).toUpperCase());
if (eq >= 0) { out[key] = item.slice(eq + 1); continue; }
const next = argv[index + 1];
if (next && !next.startsWith("--")) { out[key] = next; index += 1; }
else out[key] = true;
}
return out;
}
async function passwordValue({ parsed, env, stdinText }: any) {
const password = await optionalPasswordValue({ parsed, env, stdinText });
return requiredText(password, "password");
}
async function optionalPasswordValue({ parsed, env, stdinText }: any) {
if (typeof parsed.passwordEnv === "string") return text(env[parsed.passwordEnv]);
if (parsed.passwordStdin === true) return stdinText !== undefined ? stdinText.trimEnd() : await readStdin();
return text(parsed.password ?? env.HWLAB_PASSWORD);
}
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks).toString("utf8").trimEnd();
}
function baseUrl(parsed: ParsedArgs, env: EnvLike) {
return text(parsed.baseUrl ?? env.HWLAB_CLIENT_BASE_URL ?? env.HWLAB_CLOUD_WEB_URL ?? env.HWLAB_CLI_BASE_URL) || DEFAULT_BASE_URL;
}
async function authCookie({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) {
const explicit = explicitAuthCookie(parsed, env);
if (explicit) return explicit.includes("=") ? explicit : `hwlab_session=${encodeURIComponent(explicit)}`;
if (parsed.noSession === true) return null;
const session = await loadSession({ parsed, env, cwd });
return session?.cookie ?? null;
}
function explicitAuthCookie(parsed: ParsedArgs, env: EnvLike) {
const explicit = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE);
return explicit ? explicit.includes("=") ? explicit : `hwlab_session=${encodeURIComponent(explicit)}` : null;
}
function autoAuthAllowed(parsed: ParsedArgs) {
return parsed.noAuth !== true && parsed.noAutoAuth !== true;
}
async function loadSession({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) {
return (await readSessionState({ parsed, env, cwd })).session;
}
async function readSessionState({ parsed, env, cwd }: { parsed: ParsedArgs; env: EnvLike; cwd: string }) {
const file = stateFile(parsed, cwd);
const expectedBaseUrl = baseUrl(parsed, env);
let raw = "";
try {
raw = await readFile(file, "utf8");
} catch (error) {
return {
stateFile: file,
expectedBaseUrl,
exists: false,
readable: false,
usable: false,
ignoredReason: error?.code === "ENOENT" ? "state_file_missing" : "state_file_unreadable",
session: null
};
}
let stored: any = null;
try {
stored = JSON.parse(raw);
} catch {
return { stateFile: file, expectedBaseUrl, exists: true, readable: true, parseOk: false, usable: false, ignoredReason: "state_file_invalid_json", session: null };
}
const storedBaseUrl = text(stored?.baseUrl);
const cookieStored = Boolean(text(stored?.cookie));
const baseUrlMatches = storedBaseUrl === expectedBaseUrl;
const usable = Boolean(baseUrlMatches && cookieStored);
return {
stateFile: file,
expectedBaseUrl,
exists: true,
readable: true,
parseOk: true,
usable,
ignoredReason: usable ? null : !baseUrlMatches ? "base_url_mismatch" : "cookie_missing",
baseUrl: storedBaseUrl || null,
cookieStored,
user: safeUser(stored?.user),
expiresAt: textOrNull(stored?.expiresAt),
updatedAt: textOrNull(stored?.updatedAt),
session: usable ? stored : null
};
}
async function saveSession(context: any, session: Record<string, unknown>) {
const file = stateFile(context.parsed, context.cwd);
await mkdir(path.dirname(file), { recursive: true });
await writeFile(file, `${JSON.stringify(session, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
await chmod(file, 0o600).catch(() => {});
}
async function clearSession(context: any) {
await rm(stateFile(context.parsed, context.cwd), { force: true });
}
function stateFile(parsed: ParsedArgs, cwd: string) {
return path.resolve(cwd, text(parsed.stateFile) || ".state/hwlab-cli/session.json");
}
function cookieFromResponse(response: any) {
const values = typeof response.headers?.getSetCookie === "function"
? response.headers.getSetCookie()
: [response.headers?.get?.("set-cookie")].filter(Boolean);
const raw = values.find((value: string) => /(?:^|,)\s*hwlab_session=/u.test(value)) ?? values[0];
if (!raw) return null;
const match = String(raw).match(/hwlab_session=[^;,\s]+/u);
return match?.[0] ?? String(raw).split(";", 1)[0];
}
function responsePayload(action: string, response: any, context: any, extra: Record<string, unknown> = {}) {
const success = isHttpSuccess(response) && response.body?.ok !== false;
return {
ok: success,
action,
status: success ? "succeeded" : "failed",
baseUrl: baseUrl(context.parsed, context.env),
httpStatus: response.status,
route: extra.route ?? route(response.method, response.path),
request: requestVisibility(response),
...(response.auth ? { auth: response.auth } : {}),
...(response.authDiagnosis ? { authDiagnosis: response.authDiagnosis } : {}),
...extra,
body: extra.body ?? response.body
};
}
function requestVisibility(response: any) {
return pruneUndefined({
method: response.method,
path: response.path,
url: response.url,
httpStatus: response.status,
elapsedMs: response.elapsedMs,
timeoutMs: response.timeoutMs,
timedOut: response.timedOut === true ? true : undefined,
transportError: response.transportError ? pruneUndefined({ code: response.transportError.code, name: response.transportError.name, message: response.transportError.message }) : undefined
});
}
function authVisibility(authState: any) {
if (!authState?.required) return { required: false };
return pruneUndefined({
required: true,
baseUrl: authState.baseUrl,
username: authState.username,
cookieSource: authState.cookieSource,
stateFile: authState.stateFile,
localSession: authState.localSession,
autoLoginAttempted: authState.autoLoginAttempted,
autoLoginStatus: authState.autoLoginStatus,
autoLoginHttpStatus: authState.autoLoginHttpStatus,
retryAfterLogin: authState.retryAfterLogin
});
}
function authDiagnosis(response: any, authState: any) {
if (!authState?.required) return null;
if (response.status !== 401 && response.status !== 403) return null;
if (response.status === 403) {
return {
code: "auth_forbidden",
status: "forbidden",
message: "当前账号无权访问该受保护资源,可能是 trace/result 不属于当前 owner,或当前用户不是 admin。",
httpStatus: response.status,
cookieSource: authState.cookieSource,
autoLoginAttempted: authState.autoLoginAttempted,
retryAfterLogin: authState.retryAfterLogin,
stateFile: authState.stateFile,
localSession: authState.localSession,
nextCommands: authNextCommands(authState)
};
}
const missingCredentials = authState.autoLoginAttempted && authState.autoLoginStatus === "credentials_missing";
return {
code: missingCredentials ? "auth_credentials_missing" : "auth_required_or_expired",
status: missingCredentials ? "credentials_missing" : "unauthorized",
message: missingCredentials
? "需要登录后访问该受保护资源;请提供 --password-env、--password、--password-stdin 或 HWLAB_PASSWORD,让 CLI 自动登录。"
: "登录态不存在或已过期;CLI 已按可用凭据尝试自动登录,仍未通过认证。",
httpStatus: response.status,
cookieSource: authState.cookieSource,
autoLoginAttempted: authState.autoLoginAttempted,
autoLoginStatus: authState.autoLoginStatus,
autoLoginHttpStatus: authState.autoLoginHttpStatus,
retryAfterLogin: authState.retryAfterLogin,
stateFile: authState.stateFile,
localSession: authState.localSession,
nextCommands: authNextCommands(authState)
};
}
function sessionStateSummary(state: any) {
return pruneUndefined({
stateFile: state.stateFile,
expectedBaseUrl: state.expectedBaseUrl,
exists: state.exists,
readable: state.readable,
parseOk: state.parseOk,
usable: state.usable,
ignoredReason: state.ignoredReason,
baseUrl: state.baseUrl,
cookieStored: state.cookieStored,
user: state.user,
expiresAt: state.expiresAt,
updatedAt: state.updatedAt
});
}
function authNextCommands(authState: any) {
const resolvedBaseUrl = text(authState?.baseUrl) || DEFAULT_BASE_URL;
const username = text(authState?.username) || "admin";
const stateFileArg = text(authState?.stateFile) ? ` --state-file ${shellArg(authState.stateFile)}` : "";
return [
`export HWLAB_PASSWORD='<password>'`,
`node scripts/run-bun.mjs tools/hwlab-cli/bin/hwlab-cli.ts client auth login --base-url ${shellArg(resolvedBaseUrl)} --username ${shellArg(username)} --password-env HWLAB_PASSWORD${stateFileArg}`,
`node scripts/run-bun.mjs tools/hwlab-cli/bin/hwlab-cli.ts client auth status --base-url ${shellArg(resolvedBaseUrl)}${stateFileArg}`
];
}
function compactProbe(method: string, pathName: string, response: any) {
return { ok: isHttpSuccess(response) && response.body?.ok !== false, httpStatus: response.status, route: route(method, pathName), body: compactApiBody(response.body) };
}
function compactResponse(response: any) {
return { httpStatus: response.status, route: route(response.method, response.path), body: compactApiBody(response.body) };
}
function responseBodyForCli(body: any, parsed: ParsedArgs) {
return parsed.full === true ? body : compactApiBody(body);
}
function compactApiBody(body: any): any {
if (Array.isArray(body)) return { itemCount: body.length, items: body.slice(0, 20).map(compactApiBody), fullBodyAvailable: true };
if (!body || typeof body !== "object") return body;
if (body.jsonrpc === "2.0" || Object.hasOwn(body, "result")) return compactRpcBody(body);
return pruneUndefined({
...compactBody(body),
assistantText: assistantText(body),
reply: compactReply(body.reply ?? body.message),
id: body.id,
eventId: body.eventId,
jobId: body.jobId,
name: body.name,
devicePodId: body.devicePodId,
targetId: body.targetId,
profile: body.profile,
ts: body.ts,
level: body.level,
scope: body.scope,
intent: body.intent,
route: body.route,
environment: body.environment,
adapter: body.adapter,
sourceKind: body.sourceKind,
liveBackend: body.liveBackend,
rowCount: body.rowCount,
columns: Array.isArray(body.columns) ? body.columns : undefined,
roles: Array.isArray(body.roles) ? body.roles : undefined,
routes: body.routes && typeof body.routes === "object" ? Object.keys(body.routes) : undefined,
devicePods: Array.isArray(body.devicePods) ? body.devicePods.slice(0, 20).map(compactApiBody) : undefined,
events: Array.isArray(body.events) ? body.events.slice(0, 20).map(compactApiBody) : undefined,
lines: Array.isArray(body.lines) ? body.lines.slice(0, 20) : undefined,
assistantStreams: Array.isArray(body.assistantStreams) ? body.assistantStreams.slice(-3).map(compactAssistantStream) : undefined,
refs: body.refs,
eventCount: body.eventCount,
elapsedMs: body.elapsedMs,
methodsCount: Array.isArray(body.methods) ? body.methods.length : undefined,
rowsCount: Array.isArray(body.rows) ? body.rows.length : undefined,
devicePodsCount: Array.isArray(body.devicePods) ? body.devicePods.length : undefined,
eventsCount: Array.isArray(body.events) ? body.events.length : undefined,
linesCount: Array.isArray(body.lines) ? body.lines.length : undefined,
messagesCount: Array.isArray(body.messages) ? body.messages.length : undefined,
assistantStreamsCount: Array.isArray(body.assistantStreams) ? body.assistantStreams.length : undefined,
servicesCount: Array.isArray(body.services) ? body.services.length : undefined,
fullBodyAvailable: true
});
}
function assistantText(body: any) {
const direct = text(body?.reply?.content ?? body?.message?.content ?? body?.assistantText);
if (direct) return direct;
if (Array.isArray(body?.assistantStreams)) {
for (const item of [...body.assistantStreams].reverse()) {
const value = text(item?.text ?? item?.lastChunk);
if (value) return value;
}
}
return undefined;
}
function compactReply(value: any) {
if (!value || typeof value !== "object") return undefined;
return pruneUndefined({ messageId: value.messageId, role: value.role, content: value.content, createdAt: value.createdAt });
}
function compactAssistantStream(value: any) {
if (!value || typeof value !== "object") return undefined;
return pruneUndefined({ status: value.status, chunkCount: value.chunkCount, text: value.text, lastChunk: value.lastChunk, updatedAt: value.updatedAt });
}
function compactTraceBody(traceObject: any, limit: number) {
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
const tail = events.slice(-limit).map((event: any) => ({
at: event.createdAt ?? event.timestamp ?? null,
label: event.label ?? null,
status: event.status ?? null,
waitingFor: event.waitingFor ?? null,
message: preview(event.message ?? event.command ?? event.error ?? null, 240)
}));
return pruneUndefined({
traceId: traceObject?.traceId ?? null,
status: traceObject?.status ?? null,
waitingFor: traceObject?.waitingFor ?? traceObject?.lastEvent?.waitingFor ?? null,
eventCount: traceObject?.eventCount ?? events.length,
lastEvent: traceObject?.lastEvent ?? events.at(-1) ?? null,
eventTail: tail,
fullBodyAvailable: true
});
}
function auditTrace(traceObject: any, { requireBootsharp = false } = {}) {
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
if (traceObject?.status === "missing" && events.length === 0) {
return {
status: "trace_missing",
summary: { hasHwpod: false, hasBootsharp: false, hasApplyPatch: false, frictionCount: 0 },
frictionSignals: [],
recommendation: "Trace is missing. Check the traceId and poll result/trace after submit."
};
}
const textValue = commandTextFromEvents(events);
const signals = [
signal(
"low_level_tran",
/\/app\/tools\/(?:hwlab-gateway-)?tran\.mjs/u,
textValue,
"Use named device-pod operations through the CLI instead of low-level transport."
),
signal(
"long_device_pod_cli_path",
/node\s+\/app\/(?:tools\/device-pod-cli\.mjs|skills\/device-pod-cli\/scripts\/device-pod-cli\.mjs)/u,
textValue,
"Prefer hwpod in HWLAB code-agent runners; the long wrapper path is only a compatibility fallback when hwpod is absent."
),
signal(
"temporary_script_workaround",
temporaryScriptPattern,
textValue,
"Add or use a named device-pod operation instead of staging temporary scripts."
),
signal(
"workspace_put_text_edit",
/(?:\S*workspace:\/\S*\s+put\s+(?!status\b)\S+|\bworkspace\s+put\s+(?!status\b)\S+)/u,
textValue,
"Workspace put is available, but source text edits should prefer apply-patch unless whole-file write is intentional."
),
signal(
"status_shell_parsing",
statusShellParsingPattern,
textValue,
"Use compact JSON fields returned by harness result/trace instead of shell parsing."
),
requireBootsharp && !/\bbootsharp\b/u.test(textValue)
? {
kind: "missing_bootsharp",
count: 1,
examples: [],
hint: "No bootsharp/bootstrap device-pod context call was found in retained trace events."
}
: null
].filter(Boolean);
return {
status: signals.length > 0 ? "friction_detected" : "clean",
summary: {
hasHwpod: /\bhwpod\b/u.test(textValue),
hasBootsharp: /\bbootsharp\b/u.test(textValue),
hasApplyPatch: /\bapply-patch\b/u.test(textValue),
frictionCount: signals.length
},
frictionSignals: signals,
recommendation: signals.length > 0
? "Treat these as tool-friction signals and improve named CLI/device-pod operations; this is not a runtime gate. Prefer hwpod for device-pod commands."
: "No obvious device-pod transport friction in retained trace events."
};
}
const temporaryScriptPattern = /(?:cat\s*>\s*\S+\.(?:py|ps1|js|mjs|bat)\b|upload\b[^\n]*\.(?:py|ps1|js|mjs|bat)\b|(?:python|powershell|pwsh)\s+\S+\.(?:py|ps1)\b)/iu;
const statusShellParsingPattern = /(?:device-pod-cli|hwpod|hwlab-cli)\b[^\n]*(?:\bstatus\b|\btrace\b)[^\n]*(?:\|\s*(?:grep|head|python3?\s+-c|jq)\b|;\s*done)/iu;
function commandTextFromEvents(events: any[]) {
return events.map((event: any) => {
if (typeof event?.command === "string") return event.command;
const label = String(event?.label ?? "");
const isToolEvent = event?.type === "tool_call" || event?.stage === "tool_call" || label.includes("commandExecution");
return isToolEvent && typeof event?.message === "string" ? event.message : null;
}).filter(Boolean).join("\n");
}
function signal(kind: string, pattern: RegExp, textValue: string, hint: string) {
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
const matches = [...textValue.matchAll(new RegExp(pattern.source, flags))];
return matches.length > 0 ? { kind, count: matches.length, examples: matches.slice(0, 3).map((match) => preview(match[0], 220)), hint } : null;
}
function compactRpcBody(body: any) {
return pruneUndefined({
jsonrpc: body.jsonrpc,
id: body.id,
error: body.error,
meta: body.meta,
result: body.result && typeof body.result === "object" ? compactApiBody(body.result) : body.result,
fullBodyAvailable: true
});
}
function compactBody(body: any) {
if (!body || typeof body !== "object") return body;
return {
ok: body.ok,
status: body.status,
authenticated: body.authenticated,
actor: body.actor,
user: body.user,
contractVersion: body.contractVersion,
serviceId: body.serviceId,
ready: body.ready,
selectedDevicePodId: body.selectedDevicePodId,
devicePodCount: Array.isArray(body.devicePods) ? body.devicePods.length : undefined,
traceId: body.traceId,
conversationId: body.conversationId,
sessionId: body.sessionId,
threadId: body.threadId,
accepted: body.accepted,
shortConnection: body.shortConnection,
resultUrl: body.resultUrl,
traceUrl: body.traceUrl,
error: body.error,
blocker: body.blocker,
summary: body.summary,
freshness: body.freshness,
profileHash: body.profileHash,
targetId: body.targetId,
source: body.source
};
}
function authBodySummary(body: any) {
if (!body || typeof body !== "object") return body;
return { authenticated: body.authenticated, user: safeUser(body.user ?? body.actor), expiresAt: textOrNull(body.expiresAt), error: body.error };
}
function safeUser(value: any) {
if (!value || typeof value !== "object") return null;
return clean({ id: text(value.id), username: text(value.username), role: text(value.role), displayName: text(value.displayName) });
}
function withMeta(payload: any, now: () => string) { return { generatedAt: now(), cli: CLI_NAME, version: VERSION, ...payload }; }
function ok(action: string, data: Record<string, unknown> = {}, status = "succeeded") {
return { ok: !["failed", "timeout"].includes(status), action, status, ...data };
}
function failure(action: string, error: any) { return { ok: false, action, status: "failed", error: errorSummary(error), ...(error?.details ? { details: error.details } : {}) }; }
function errorSummary(error: any) { return { code: error?.code ?? "hwlab_cli_error", message: error?.message ?? String(error), ...(error?.details ? { details: error.details } : {}) }; }
function cliError(code: string, message: string, details: Record<string, unknown> = {}) { return Object.assign(new Error(message), { code, details }); }
function route(method: string, pathName: string) { return { method, path: pathName }; }
function isHttpSuccess(response: any) { return response.status >= 200 && response.status < 300; }
function text(value: unknown) { return String(value ?? "").trim(); }
function textOrNull(value: unknown) { const result = text(value); return result || null; }
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw cliError("missing_required_value", `${field} is required`, { field }); return result; }
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
function boundedInteger(value: unknown, fallback: number, min: number, max: number) {
const parsed = numberOption(value) ?? fallback;
return Math.min(max, Math.max(min, parsed));
}
function clean<T extends Record<string, any>>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== false && item !== null)) as T; }
function pruneUndefined<T extends Record<string, any>>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T; }
function parseJson(value: string) { if (!value) return null; try { return JSON.parse(value); } catch { return { rawText: value.slice(0, 2000), parseError: true }; } }
function sha256(value: string) { return createHash("sha256").update(value).digest("hex"); }
function makeId(prefix: string) { return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; }
function wait(ms: number) { return new Promise<void>((resolve) => setTimeout(resolve, ms)); }
function requiredTraceId(value: unknown) {
const result = requiredText(value, "traceId");
if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(result)) {
throw cliError("invalid_trace_id", "traceId must start with trc_", { traceId: result });
}
return result;
}
function preview(value: unknown, max = 500) {
if (value === undefined || value === null) return null;
const result = String(value).replace(/\s+/gu, " ").trim();
return result.length > max ? `${result.slice(0, max)}...` : result;
}
function shellArg(value: unknown) {
const textValue = String(value ?? "");
return /^[A-Za-z0-9_./:@-]+$/u.test(textValue) ? textValue : `'${textValue.replace(/'/gu, `'"'"'`)}'`;
}