Files
pikasTech-HWLAB/tools/src/workbench-cli.ts
T

83 lines
5.8 KiB
TypeScript

import type { WorkbenchCommand } from "../../internal/workbench/contracts.ts";
import { workbenchRuntime } from "../../internal/workbench/runtime.ts";
import { workbenchNativeServiceCommand } from "./workbench-native-service.ts";
export async function runWorkbenchCli(argv: string[], env: Record<string, string | undefined> = process.env) {
const parsed = parse(argv);
if (parsed.help || parsed.positionals.length === 0) return help();
if (parsed.positionals[0] === "service") return workbenchNativeServiceCommand({ service: parsed.positionals[1], action: parsed.positionals[2] ?? "status", cwd: process.cwd(), env });
const command = commandFrom(parsed, env);
if (parsed.overApi) return overApi(command, parsed, env);
const runtime = workbenchRuntime(env);
try {
const result = await runtime.dispatch(command);
return { ...result, transport: "local", route: "application-dispatcher", identity: identity(result.data) };
} finally {
await runtime.close();
}
}
function help() {
return {
ok: true,
operation: "help",
mode: "local-dispatcher-default",
usage: [
"hwlab-cli workbench health [--over-api]",
"hwlab-cli workbench session create --actor-id ID [--provider-profile PROFILE] [--over-api]",
"hwlab-cli workbench turn submit --actor-id ID --session-id ID --message TEXT [--trace-id ID] [--over-api]",
"hwlab-cli workbench turn cancel --actor-id ID --trace-id ID [--over-api]",
"hwlab-cli workbench service api|worker|web start|stop|restart|status|logs"
],
transportContract: "--over-api only changes transport; --overapi is unsupported",
localConfig: ["WORKBENCH_MODE", "WORKBENCH_NATIVE_STATE_FILE"],
nativeServiceConfig: ["WORKBENCH_API_BIND_HOST", "WORKBENCH_API_PROBE_HOST", "WORKBENCH_API_PUBLIC_HOST", "WORKBENCH_API_PORT", "WORKBENCH_WORKER_BIND_HOST", "WORKBENCH_WORKER_PROBE_HOST", "WORKBENCH_WORKER_HEALTH_PORT", "WORKBENCH_WEB_BIND_HOST", "WORKBENCH_WEB_PROBE_HOST", "WORKBENCH_WEB_PUBLIC_HOST", "WORKBENCH_WEB_PORT", "WORKBENCH_NATIVE_API_URL"],
apiConfig: ["WORKBENCH_API_URL", "HWLAB_API_KEY"]
};
}
function commandFrom(parsed: Parsed, env: Record<string, string | undefined>): WorkbenchCommand {
const [group, action] = parsed.positionals;
if (group === "health") return { operation: "health" };
const actor = { id: required(parsed, "actor-id"), role: parsed.values["actor-role"] ?? "user" };
if (group === "session" && action === "create") return { operation: "session.create", actor, params: compact({ sessionId: parsed.values["session-id"], conversationId: parsed.values["conversation-id"], projectId: parsed.values["project-id"], providerProfile: parsed.values["provider-profile"] }) };
if (group === "turn" && action === "submit") return { operation: "turn.submit", actor, traceId: parsed.values["trace-id"], params: compact({ sessionId: required(parsed, "session-id"), message: required(parsed, "message"), projectId: parsed.values["project-id"], providerProfile: parsed.values["provider-profile"], shortConnection: true }) };
if (group === "turn" && action === "cancel") return { operation: "turn.cancel", actor, traceId: required(parsed, "trace-id"), params: compact({ sessionId: parsed.values["session-id"] }) };
throw codedError("unsupported_workbench_command", `unsupported Workbench command: ${group} ${action ?? ""}`.trim());
}
async function overApi(command: WorkbenchCommand, parsed: Parsed, env: Record<string, string | undefined>) {
const baseUrl = parsed.values["api-url"] || env.WORKBENCH_API_URL;
if (!baseUrl) throw codedError("workbench_api_url_required", "WORKBENCH_API_URL or --api-url is required with --over-api");
const headers: Record<string, string> = { "content-type": "application/json" };
const apiKey = env.HWLAB_API_KEY;
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
const route = "/v1/workbench/commands";
const response = await fetch(`${baseUrl.replace(/\/$/u, "")}${route}`, { method: "POST", headers, body: JSON.stringify(command) });
const body = await response.json().catch(() => null) as Record<string, any> | null;
if (!body) throw codedError("workbench_api_invalid_json", `Workbench API returned invalid JSON (${response.status})`);
return { ...body, transport: "api", baseUrl, route: `POST ${route}`, httpStatus: response.status, identity: identity(body.data) };
}
type Parsed = { positionals: string[]; values: Record<string, string>; overApi: boolean; help: boolean };
function parse(argv: string[]): Parsed {
const parsed: Parsed = { positionals: [], values: {}, overApi: false, help: false };
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (token === "--overapi") throw codedError("unsupported_option", "--overapi was retired; use --over-api");
if (token === "--over-api") parsed.overApi = true;
else if (token === "--help" || token === "-h") parsed.help = true;
else if (token.startsWith("--")) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) throw codedError("option_value_required", `${token} requires a value`);
parsed.values[token.slice(2)] = value;
index += 1;
} else parsed.positionals.push(token);
}
return parsed;
}
function identity(data: any) { return { sessionId: data?.sessionId ?? data?.session?.sessionId ?? null, traceId: data?.traceId ?? null, workflowId: data?.workflowId ?? null, workflowRunId: data?.workflowRunId ?? null }; }
function compact(value: Record<string, unknown>) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)); }
function required(parsed: Parsed, key: string) { const value = parsed.values[key]; if (!value) throw codedError("option_required", `--${key} is required`); return value; }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }