168 lines
5.9 KiB
JavaScript
168 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import http from "node:http";
|
|
import https from "node:https";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
const DEFAULT_API_BASE_URL = "http://127.0.0.1:6667";
|
|
const DEFAULT_PROJECT_ID = "prj_mvp_topology";
|
|
const DEFAULT_GATEWAY_SESSION_ID = "gws_DESKTOP-1MHOD9I";
|
|
const DEFAULT_RESOURCE_ID = "res_windows_host";
|
|
const DEFAULT_CAPABILITY_ID = "cap_windows_cmd_exec";
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help || !args.command) {
|
|
printHelp();
|
|
process.exit(args.help ? 0 : 2);
|
|
}
|
|
|
|
const apiBaseUrl = stripTrailingSlash(args.apiBaseUrl || process.env.HWLAB_GATEWAY_SHELL_API_BASE_URL || DEFAULT_API_BASE_URL);
|
|
const traceId = args.traceId || `trc_gateway_shell_cli_${Date.now()}`;
|
|
const requestId = args.requestId || `req_gateway_shell_cli_${randomUUID()}`;
|
|
const timeoutMs = positiveInteger(args.timeoutMs, 30000);
|
|
const response = await requestJson(`${apiBaseUrl}/v1/rpc/hardware.invoke.shell`, {
|
|
method: "POST",
|
|
timeoutMs,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-trace-id": traceId,
|
|
"x-request-id": requestId,
|
|
"x-actor-id": args.actorId || "usr_code_agent",
|
|
"x-source-service-id": "hwlab-cloud-api"
|
|
},
|
|
body: {
|
|
projectId: args.projectId || DEFAULT_PROJECT_ID,
|
|
gatewaySessionId: args.gatewaySessionId || DEFAULT_GATEWAY_SESSION_ID,
|
|
resourceId: args.resourceId || DEFAULT_RESOURCE_ID,
|
|
capabilityId: args.capabilityId || DEFAULT_CAPABILITY_ID,
|
|
input: {
|
|
command: args.command,
|
|
cwd: args.cwd || undefined,
|
|
timeoutMs
|
|
}
|
|
}
|
|
});
|
|
|
|
if (args.json) {
|
|
process.stdout.write(`${JSON.stringify(response.body, null, 2)}\n`);
|
|
} else {
|
|
process.stdout.write(formatResponse(response.body, { httpStatus: response.statusCode, traceId, requestId }));
|
|
}
|
|
|
|
const result = response.body?.result ?? {};
|
|
const dispatch = result.dispatch ?? {};
|
|
const exitCode = Number.isInteger(dispatch.exitCode) ? dispatch.exitCode : Number.isInteger(result.exitCode) ? result.exitCode : null;
|
|
const failed = response.statusCode < 200 || response.statusCode >= 300 || response.body?.error || result.status === "failed" || result.status === "rejected" || (exitCode !== null && exitCode !== 0);
|
|
process.exit(failed ? 1 : 0);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const item = argv[index];
|
|
if (item === "--help" || item === "-h") {
|
|
args.help = true;
|
|
} else if (item === "--json") {
|
|
args.json = true;
|
|
} else if (item.startsWith("--") && item.includes("=")) {
|
|
const [key, ...rest] = item.slice(2).split("=");
|
|
args[toCamel(key)] = rest.join("=");
|
|
} else if (item.startsWith("--")) {
|
|
const key = toCamel(item.slice(2));
|
|
args[key] = argv[index + 1] ?? "";
|
|
index += 1;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function toCamel(value) {
|
|
return String(value).replace(/-([a-z])/gu, (_, char) => char.toUpperCase());
|
|
}
|
|
|
|
function printHelp() {
|
|
process.stdout.write([
|
|
"Usage: node tools/hwlab-gateway-shell.mjs --command \"cmd /c ...\" [--json]",
|
|
"",
|
|
"Options:",
|
|
" --api-base-url URL Cloud API/edge base URL, default http://127.0.0.1:6667",
|
|
" --command CMD Windows cmd command to execute through the registered PC gateway",
|
|
" --timeout-ms N Short dispatch timeout in milliseconds",
|
|
" --project-id ID Default prj_mvp_topology",
|
|
" --gateway-session-id ID Default gws_DESKTOP-1MHOD9I",
|
|
" --resource-id ID Default res_windows_host",
|
|
" --capability-id ID Default cap_windows_cmd_exec",
|
|
" --json Print full JSON-RPC response"
|
|
].join("\n") + "\n");
|
|
}
|
|
|
|
function requestJson(url, { method, headers, body, timeoutMs }) {
|
|
return new Promise((resolve, reject) => {
|
|
const target = new URL(url);
|
|
const client = target.protocol === "https:" ? https : http;
|
|
const payload = JSON.stringify(body ?? {});
|
|
const request = client.request(target, {
|
|
method,
|
|
headers: {
|
|
...headers,
|
|
"content-length": Buffer.byteLength(payload)
|
|
},
|
|
timeout: timeoutMs
|
|
}, (response) => {
|
|
let text = "";
|
|
response.setEncoding("utf8");
|
|
response.on("data", (chunk) => {
|
|
text += chunk;
|
|
});
|
|
response.on("end", () => {
|
|
try {
|
|
resolve({
|
|
statusCode: response.statusCode ?? 0,
|
|
body: text ? JSON.parse(text) : null
|
|
});
|
|
} catch (error) {
|
|
reject(new Error(`Cloud API returned non-JSON response: ${error.message}`));
|
|
}
|
|
});
|
|
});
|
|
request.on("timeout", () => {
|
|
request.destroy(new Error(`Gateway shell request timed out after ${timeoutMs}ms`));
|
|
});
|
|
request.on("error", reject);
|
|
request.end(payload);
|
|
});
|
|
}
|
|
|
|
function formatResponse(body, { httpStatus, traceId, requestId }) {
|
|
if (body?.error) {
|
|
return [
|
|
`http=${httpStatus} trace=${traceId} request=${requestId}`,
|
|
`error=${body.error.message ?? body.error.code}`,
|
|
body.error.data?.reason ? `reason=${body.error.data.reason}` : null
|
|
].filter(Boolean).join("\n") + "\n";
|
|
}
|
|
const result = body?.result ?? {};
|
|
const dispatch = result.dispatch ?? {};
|
|
const lines = [
|
|
`http=${httpStatus} trace=${traceId} request=${requestId}`,
|
|
`status=${result.status ?? "unknown"} operation=${result.operationId ?? "null"} dispatch=${dispatch.dispatchStatus ?? "unknown"} exit=${dispatch.exitCode ?? "null"}`
|
|
];
|
|
if (dispatch.stdout) lines.push(String(dispatch.stdout).trimEnd());
|
|
if (dispatch.stderr) lines.push(String(dispatch.stderr).trimEnd());
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
function positiveInteger(value, fallback) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function stripTrailingSlash(value) {
|
|
return String(value).replace(/\/+$/u, "");
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error.message}\n`);
|
|
process.exit(1);
|
|
});
|