487 lines
20 KiB
JavaScript
487 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
import http from "node:http";
|
|
import https from "node:https";
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
statSync,
|
|
writeFileSync
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const LOCAL_API_BASE_URL = "http://127.0.0.1:6667";
|
|
const DEFAULT_API_PORT = "6667";
|
|
const DEFAULT_PROJECT_ID = "prj_mvp_topology";
|
|
const DEFAULT_RESOURCE_ID = "res_windows_host";
|
|
const DEFAULT_CAPABILITY_ID = "cap_windows_cmd_exec";
|
|
const DEFAULT_TIMEOUT_MS = 120000;
|
|
const MAX_TIMEOUT_MS = 600000;
|
|
const DEFAULT_CHUNK_SIZE = 2000;
|
|
const MAX_CHUNK_SIZE = 2000;
|
|
const REQUEST_TIMEOUT_GRACE_MS = 10000;
|
|
const POWERSHELL_PROLOGUE = [
|
|
"$ErrorActionPreference = 'Stop';",
|
|
"$utf8NoBom = [System.Text.UTF8Encoding]::new($false);",
|
|
"$ProgressPreference = 'SilentlyContinue';",
|
|
"[Console]::InputEncoding = $utf8NoBom;",
|
|
"[Console]::OutputEncoding = $utf8NoBom;",
|
|
"$OutputEncoding = $utf8NoBom;",
|
|
"$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8';",
|
|
"$PSDefaultParameterValues['Set-Content:Encoding'] = 'utf8';"
|
|
].join("\n");
|
|
|
|
async function main() {
|
|
const cli = parseCli(process.argv.slice(2));
|
|
if (cli.help) {
|
|
printHelp(cli.exitCode ?? 0);
|
|
return;
|
|
}
|
|
|
|
const apiBaseUrl = stripTrailingSlash(cli.options.apiBaseUrl || process.env.HWLAB_GATEWAY_TRAN_API_BASE_URL || process.env.HWLAB_GATEWAY_SHELL_API_BASE_URL || defaultApiBaseUrl());
|
|
const timeoutMs = boundedPositiveInteger(cli.options.timeoutMs ?? process.env.HWLAB_GATEWAY_TRAN_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, { min: 1000, max: MAX_TIMEOUT_MS });
|
|
const requestTimeoutMs = boundedPositiveInteger(cli.options.requestTimeoutMs, timeoutMs + REQUEST_TIMEOUT_GRACE_MS, {
|
|
min: timeoutMs,
|
|
max: timeoutMs + Math.max(REQUEST_TIMEOUT_GRACE_MS, Math.floor(timeoutMs / 2))
|
|
});
|
|
const chunkSize = boundedPositiveInteger(cli.options.chunkSize ?? process.env.HWLAB_GATEWAY_TRAN_CHUNK_SIZE, DEFAULT_CHUNK_SIZE, { min: 128, max: MAX_CHUNK_SIZE });
|
|
const target = parseLocator(cli.locator);
|
|
const context = { apiBaseUrl, timeoutMs, requestTimeoutMs, chunkSize, json: cli.options.json, target };
|
|
|
|
if (cli.command === "cmd") {
|
|
await runCmd(context, cli.args);
|
|
} else if (cli.command === "ps") {
|
|
await runPowerShell(context, cli.args);
|
|
} else if (cli.command === "upload") {
|
|
await uploadFile(context, cli.args);
|
|
} else if (cli.command === "download") {
|
|
await downloadFile(context, cli.args);
|
|
} else {
|
|
fail(`unknown command: ${cli.command}. Use cmd, ps, upload, or download.`);
|
|
}
|
|
}
|
|
|
|
function defaultApiBaseUrl() {
|
|
const namespace = readKubernetesNamespace();
|
|
if (process.env.KUBERNETES_SERVICE_HOST && namespace) {
|
|
return `http://hwlab-cloud-api.${namespace}.svc.cluster.local:${DEFAULT_API_PORT}`;
|
|
}
|
|
return LOCAL_API_BASE_URL;
|
|
}
|
|
|
|
function readKubernetesNamespace() {
|
|
try {
|
|
const value = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "utf8").trim();
|
|
if (/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(value)) return value;
|
|
} catch {
|
|
// Non-Kubernetes hosts keep the local cloud-api default.
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function parseCli(argv) {
|
|
if (argv.length === 0) return { help: true, exitCode: 2 };
|
|
if (argv[0] === "--help" || argv[0] === "-h") return { help: true, exitCode: 0 };
|
|
const [locator, command, ...rest] = argv;
|
|
if (!locator || !command) return { help: true, exitCode: 2 };
|
|
const options = {};
|
|
const args = [];
|
|
for (let index = 0; index < rest.length; index += 1) {
|
|
const item = rest[index];
|
|
if (item === "--") {
|
|
args.push(...rest.slice(index + 1));
|
|
break;
|
|
}
|
|
if (item === "--json") {
|
|
options.json = true;
|
|
} else if (item === "--api-base-url") {
|
|
options.apiBaseUrl = requireOptionValue(rest, index, item);
|
|
index += 1;
|
|
} else if (item.startsWith("--api-base-url=")) {
|
|
options.apiBaseUrl = item.slice("--api-base-url=".length);
|
|
} else if (item === "--timeout-ms") {
|
|
options.timeoutMs = requireOptionValue(rest, index, item);
|
|
index += 1;
|
|
} else if (item.startsWith("--timeout-ms=")) {
|
|
options.timeoutMs = item.slice("--timeout-ms=".length);
|
|
} else if (item === "--request-timeout-ms") {
|
|
options.requestTimeoutMs = requireOptionValue(rest, index, item);
|
|
index += 1;
|
|
} else if (item.startsWith("--request-timeout-ms=")) {
|
|
options.requestTimeoutMs = item.slice("--request-timeout-ms=".length);
|
|
} else if (item === "--chunk-size") {
|
|
options.chunkSize = requireOptionValue(rest, index, item);
|
|
index += 1;
|
|
} else if (item.startsWith("--chunk-size=")) {
|
|
options.chunkSize = item.slice("--chunk-size=".length);
|
|
} else {
|
|
args.push(item);
|
|
}
|
|
}
|
|
return { locator, command, options, args };
|
|
}
|
|
|
|
function requireOptionValue(argv, index, flag) {
|
|
const value = argv[index + 1];
|
|
if (!value) fail(`${flag} requires a value`);
|
|
return value;
|
|
}
|
|
|
|
function parseLocator(locator) {
|
|
const colonIndex = String(locator).indexOf(":");
|
|
if (colonIndex < 1) {
|
|
fail(`invalid locator: ${locator}. Expected gatewaySessionId:/f/work or gatewaySessionId:f:/work.`);
|
|
}
|
|
const gatewaySessionId = locator.slice(0, colonIndex);
|
|
const rawWorkspace = locator.slice(colonIndex + 1);
|
|
if (!gatewaySessionId || !rawWorkspace) {
|
|
fail(`invalid locator: ${locator}. Gateway id and workspace path are required.`);
|
|
}
|
|
return {
|
|
gatewaySessionId,
|
|
rawWorkspace,
|
|
workspace: normalizeGatewayPath(rawWorkspace)
|
|
};
|
|
}
|
|
|
|
function normalizeGatewayPath(value) {
|
|
const input = String(value ?? "").trim();
|
|
if (!input) fail("empty gateway workspace path");
|
|
if (/^[a-zA-Z]:[\\/]/u.test(input)) {
|
|
return `${input[0].toUpperCase()}:${input.slice(2).replace(/[\\/]+/gu, "\\")}`;
|
|
}
|
|
const msysMatch = input.match(/^\/([a-zA-Z])(?:\/|$)(.*)$/u);
|
|
if (msysMatch) {
|
|
const drive = msysMatch[1].toUpperCase();
|
|
const rest = msysMatch[2] ? `\\${msysMatch[2].replace(/[\\/]+/gu, "\\")}` : "\\";
|
|
return `${drive}:${rest}`;
|
|
}
|
|
if (/^\\\\/u.test(input)) return input.replace(/\//gu, "\\");
|
|
return input.replace(/[\\/]+/gu, "\\");
|
|
}
|
|
|
|
async function runCmd(context, args) {
|
|
const command = shellText(args, "cmd requires a command after --");
|
|
const response = await invokeShell(context, {
|
|
command: buildPowerShellCommand(buildCmdPassthroughScript(command), context.target.workspace),
|
|
cwd: context.target.workspace
|
|
});
|
|
if (context.json) printJson(response);
|
|
else printDispatch(response);
|
|
process.exit(isFailed(response) ? 1 : 0);
|
|
}
|
|
|
|
function buildCmdPassthroughScript(command) {
|
|
return [
|
|
`$cmd = '${psQuote(command)}'`,
|
|
"$env:PYTHONUTF8 = '1'",
|
|
"$env:PYTHONIOENCODING = 'utf-8'",
|
|
"$env:PYTHONLEGACYWINDOWSSTDIO = '0'",
|
|
"& cmd.exe /d /s /c \"chcp 65001 >NUL & $cmd\"",
|
|
"exit $LASTEXITCODE"
|
|
].join("\n");
|
|
}
|
|
|
|
async function runPowerShell(context, args) {
|
|
const script = shellText(args, "ps requires a PowerShell script after --");
|
|
const command = buildPowerShellCommand(script, context.target.workspace);
|
|
const response = await invokeShell(context, { command, cwd: context.target.workspace });
|
|
if (context.json) printJson(response);
|
|
else printDispatch(response);
|
|
process.exit(isFailed(response) ? 1 : 0);
|
|
}
|
|
|
|
async function uploadFile(context, args) {
|
|
if (args.length < 1) fail("upload requires <local_source_path> [remote_target_path]");
|
|
const localPath = args[0];
|
|
if (!existsSync(localPath)) fail(`local file not found: ${localPath}`);
|
|
const data = readFileSync(localPath);
|
|
const stat = statSync(localPath);
|
|
const localHash = sha256(data);
|
|
const remotePath = resolveRemotePath(context.target.workspace, args[1] || path.basename(localPath));
|
|
const tempPath = `${remotePath}.hwlab-upload-${randomUUID()}.b64`;
|
|
const dataTempPath = `${remotePath}.hwlab-upload-${randomUUID()}.tmp`;
|
|
const chunks = splitText(data.toString("base64"), context.chunkSize);
|
|
|
|
process.stderr.write(`upload ${localPath} -> ${remotePath} (${stat.size} bytes, ${chunks.length} chunks)\n`);
|
|
await expectOk(await invokePowerShell(context, [
|
|
`$remotePath = '${psQuote(remotePath)}'`,
|
|
`$tempPath = '${psQuote(tempPath)}'`,
|
|
"$dir = [System.IO.Path]::GetDirectoryName($remotePath)",
|
|
"if (-not [string]::IsNullOrWhiteSpace($dir)) { [System.IO.Directory]::CreateDirectory($dir) | Out-Null }",
|
|
"[System.IO.File]::WriteAllText($tempPath, '', [System.Text.Encoding]::ASCII)",
|
|
`Write-Output 'upload-init:${chunks.length}'`
|
|
].join("\n")), "upload init");
|
|
|
|
for (let index = 0; index < chunks.length; index += 1) {
|
|
await expectOk(await invokePowerShell(context, [
|
|
`$tempPath = '${psQuote(tempPath)}'`,
|
|
`[System.IO.File]::AppendAllText($tempPath, '${chunks[index]}', [System.Text.Encoding]::ASCII)`,
|
|
`Write-Output 'upload-chunk:${index + 1}/${chunks.length}'`
|
|
].join("\n")), `upload chunk ${index + 1}/${chunks.length}`);
|
|
}
|
|
|
|
const finalResponse = await invokePowerShell(context, [
|
|
`$remotePath = '${psQuote(remotePath)}'`,
|
|
`$tempPath = '${psQuote(tempPath)}'`,
|
|
`$dataTempPath = '${psQuote(dataTempPath)}'`,
|
|
`$expectedBytes = ${stat.size}`,
|
|
`$expectedHash = '${localHash}'`,
|
|
"$b64 = [System.IO.File]::ReadAllText($tempPath, [System.Text.Encoding]::ASCII)",
|
|
"$bytes = [System.Convert]::FromBase64String($b64)",
|
|
"[System.IO.File]::WriteAllBytes($dataTempPath, $bytes)",
|
|
"$item = Get-Item -LiteralPath $dataTempPath",
|
|
"$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $dataTempPath).Hash.ToLowerInvariant()",
|
|
"if ($item.Length -ne $expectedBytes) { throw \"upload byte mismatch: $($item.Length) != $expectedBytes\" }",
|
|
"if ($hash -ne $expectedHash) { throw \"upload sha256 mismatch: $hash != $expectedHash\" }",
|
|
"Move-Item -LiteralPath $dataTempPath -Destination $remotePath -Force",
|
|
"Remove-Item -LiteralPath $tempPath -Force",
|
|
"$item = Get-Item -LiteralPath $remotePath",
|
|
"$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $remotePath).Hash.ToLowerInvariant()",
|
|
"if ($item.Length -ne $expectedBytes) { throw \"upload final byte mismatch: $($item.Length) != $expectedBytes\" }",
|
|
"if ($hash -ne $expectedHash) { throw \"upload final sha256 mismatch: $hash != $expectedHash\" }",
|
|
"Write-Output (\"upload-ok path={0} bytes={1} sha256={2}\" -f $remotePath, $item.Length, $hash)"
|
|
].join("\n"));
|
|
|
|
if (context.json) printJson(finalResponse);
|
|
else printDispatch(finalResponse);
|
|
process.exit(isFailed(finalResponse) ? 1 : 0);
|
|
}
|
|
|
|
async function downloadFile(context, args) {
|
|
if (args.length < 1) fail("download requires <remote_source_path> [local_target_path]");
|
|
const remotePath = resolveRemotePath(context.target.workspace, args[0]);
|
|
const localPath = args[1] || path.basename(remotePath.replace(/\\/gu, "/"));
|
|
const statResponse = await invokePowerShell(context, [
|
|
`$remotePath = '${psQuote(remotePath)}'`,
|
|
"if (-not (Test-Path -LiteralPath $remotePath -PathType Leaf)) { throw \"remote file not found: $remotePath\" }",
|
|
"$item = Get-Item -LiteralPath $remotePath",
|
|
"$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $remotePath).Hash.ToLowerInvariant()",
|
|
"$b64Length = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($remotePath)).Length",
|
|
"[pscustomobject]@{ path = $remotePath; bytes = $item.Length; sha256 = $hash; base64Length = $b64Length } | ConvertTo-Json -Compress"
|
|
].join("\n"));
|
|
expectOk(statResponse, "download stat");
|
|
const info = parseJsonLine(dispatchOf(statResponse).stdout, "download stat JSON");
|
|
const chunks = [];
|
|
process.stderr.write(`download ${remotePath} -> ${localPath} (${info.bytes} bytes)\n`);
|
|
for (let offset = 0; offset < info.base64Length; offset += context.chunkSize) {
|
|
const length = Math.min(context.chunkSize, info.base64Length - offset);
|
|
const response = await invokePowerShell(context, [
|
|
`$remotePath = '${psQuote(remotePath)}'`,
|
|
`$offset = ${offset}`,
|
|
`$length = ${length}`,
|
|
"$b64 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($remotePath))",
|
|
"Write-Output $b64.Substring($offset, $length)"
|
|
].join("\n"));
|
|
expectOk(response, `download chunk ${Math.floor(offset / context.chunkSize) + 1}`);
|
|
chunks.push(String(dispatchOf(response).stdout ?? "").replace(/\s+/gu, ""));
|
|
}
|
|
const data = Buffer.from(chunks.join(""), "base64");
|
|
const actualHash = sha256(data);
|
|
if (data.length !== info.bytes) fail(`download byte mismatch: ${data.length} != ${info.bytes}`);
|
|
if (actualHash !== String(info.sha256).toLowerCase()) fail(`download sha256 mismatch: ${actualHash} != ${info.sha256}`);
|
|
mkdirSync(path.dirname(localPath), { recursive: true });
|
|
writeFileSync(localPath, data);
|
|
if (context.json) printJson({ status: "downloaded", remotePath, localPath, bytes: data.length, sha256: actualHash });
|
|
else process.stderr.write(`download-ok path=${localPath} bytes=${data.length} sha256=${actualHash}\n`);
|
|
}
|
|
|
|
function resolveRemotePath(workspace, candidate) {
|
|
const text = String(candidate ?? "").trim();
|
|
if (!text) fail("remote path is empty");
|
|
if (/^[a-zA-Z]:[\\/]/u.test(text) || /^\/([a-zA-Z])(?:\/|$)/u.test(text) || /^\\\\/u.test(text)) {
|
|
return normalizeGatewayPath(text);
|
|
}
|
|
return `${workspace.replace(/\\+$/u, "")}\\${text.replace(/[\\/]+/gu, "\\")}`;
|
|
}
|
|
|
|
function shellText(args, message) {
|
|
if (!Array.isArray(args) || args.length === 0) fail(message);
|
|
return args.join(" ");
|
|
}
|
|
|
|
function buildPowerShellCommand(script, cwd) {
|
|
const source = [
|
|
POWERSHELL_PROLOGUE,
|
|
cwd ? `Set-Location -LiteralPath '${psQuote(cwd)}';` : "",
|
|
script
|
|
].filter(Boolean).join("\n");
|
|
const encoded = Buffer.from(source, "utf16le").toString("base64");
|
|
return `powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
}
|
|
|
|
function invokePowerShell(context, script) {
|
|
return invokeShell(context, {
|
|
command: buildPowerShellCommand(script, context.target.workspace),
|
|
cwd: context.target.workspace
|
|
});
|
|
}
|
|
|
|
async function invokeShell(context, { command, cwd }) {
|
|
const requestId = `req_gateway_tran_${randomUUID()}`;
|
|
const operationId = `op_gateway_tran_${randomUUID()}`;
|
|
const response = await requestJson(`${context.apiBaseUrl}/v1/rpc/hardware.invoke.shell`, {
|
|
method: "POST",
|
|
timeoutMs: context.requestTimeoutMs,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-trace-id": `trc_gateway_tran_${Date.now()}`,
|
|
"x-request-id": requestId,
|
|
"x-actor-id": "usr_code_agent",
|
|
"x-source-service-id": "hwlab-cloud-api"
|
|
},
|
|
body: {
|
|
projectId: DEFAULT_PROJECT_ID,
|
|
operationId,
|
|
gatewaySessionId: context.target.gatewaySessionId,
|
|
resourceId: DEFAULT_RESOURCE_ID,
|
|
capabilityId: DEFAULT_CAPABILITY_ID,
|
|
input: {
|
|
command,
|
|
cwd,
|
|
timeoutMs: context.timeoutMs
|
|
}
|
|
}
|
|
});
|
|
return response.body;
|
|
}
|
|
|
|
function dispatchOf(response) {
|
|
const result = response?.result ?? {};
|
|
return result.dispatch ?? result;
|
|
}
|
|
|
|
function printDispatch(response) {
|
|
const dispatch = dispatchOf(response);
|
|
if (dispatch.stdout) process.stdout.write(String(dispatch.stdout));
|
|
if (dispatch.stderr) process.stderr.write(String(dispatch.stderr));
|
|
if (dispatch.stdout && !String(dispatch.stdout).endsWith("\n")) process.stdout.write("\n");
|
|
if (dispatch.stderr && !String(dispatch.stderr).endsWith("\n")) process.stderr.write("\n");
|
|
if (response?.error) process.stderr.write(`error=${response.error.message ?? response.error.code}\n`);
|
|
if (dispatch.dispatchStatus && dispatch.dispatchStatus !== "succeeded") process.stderr.write(`dispatch=${dispatch.dispatchStatus}\n`);
|
|
if (Number.isInteger(dispatch.exitCode) && dispatch.exitCode !== 0) process.stderr.write(`exit=${dispatch.exitCode}\n`);
|
|
}
|
|
|
|
function isFailed(response) {
|
|
if (!response || response.error) return true;
|
|
const result = response.result ?? {};
|
|
const dispatch = dispatchOf(response);
|
|
if (["failed", "rejected", "timed_out"].includes(result.status) || ["failed", "rejected", "timed_out", "not_connected"].includes(dispatch.dispatchStatus)) return true;
|
|
if (Number.isInteger(dispatch.exitCode) && dispatch.exitCode !== 0) return true;
|
|
if (result.accepted === true && dispatch.shellExecuted === false) return true;
|
|
return false;
|
|
}
|
|
|
|
function expectOk(response, label) {
|
|
if (!isFailed(response)) return;
|
|
printDispatch(response);
|
|
fail(`${label} failed`);
|
|
}
|
|
|
|
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 tran request timed out after ${timeoutMs}ms`)));
|
|
request.on("error", reject);
|
|
request.end(payload);
|
|
});
|
|
}
|
|
|
|
function parseJsonLine(text, label) {
|
|
const line = String(text ?? "").split(/\r?\n/u).find((item) => item.trim().startsWith("{") || item.trim().startsWith("["));
|
|
if (!line) fail(`${label} was not present`);
|
|
try {
|
|
return JSON.parse(line);
|
|
} catch (error) {
|
|
fail(`${label} parse failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function splitText(text, size) {
|
|
const chunks = [];
|
|
for (let index = 0; index < text.length; index += size) chunks.push(text.slice(index, index + size));
|
|
return chunks.length ? chunks : [""];
|
|
}
|
|
|
|
function psQuote(value) {
|
|
return String(value).replace(/'/gu, "''");
|
|
}
|
|
|
|
function sha256(buffer) {
|
|
return createHash("sha256").update(buffer).digest("hex");
|
|
}
|
|
|
|
function printJson(value) {
|
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function boundedPositiveInteger(value, fallback, { min, max } = {}) {
|
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
const selected = Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
return Math.min(Math.max(selected, min ?? 1), max ?? selected);
|
|
}
|
|
|
|
function stripTrailingSlash(value) {
|
|
return String(value).replace(/\/+$/u, "");
|
|
}
|
|
|
|
function printHelp(exitCode = 0) {
|
|
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
|
stream.write([
|
|
"Usage: node /app/tools/hwlab-gateway-tran.mjs <gateway:path> <cmd|ps|upload|download> [options] -- <args>",
|
|
"",
|
|
"Locator examples:",
|
|
" gws_DESKTOP-1MHOD9I:/f/work",
|
|
" gws_DESKTOP-1MHOD9I:f:/work/hwlab/.tmp",
|
|
"",
|
|
"Commands:",
|
|
" cmd -- <command> Run a Windows cmd command in the gateway workspace.",
|
|
" ps -- <script> Run a UTF-8 PowerShell script in the gateway workspace.",
|
|
" upload <local> [remote] Upload a local file using verified base64 chunks.",
|
|
" download <remote> [local] Download a remote file using verified base64 chunks.",
|
|
"",
|
|
"Options:",
|
|
" --api-base-url URL Default env override, in-cluster hwlab-cloud-api service, or http://127.0.0.1:6667.",
|
|
" --timeout-ms N Gateway command timeout, default 120000ms.",
|
|
" --request-timeout-ms N HTTP wait timeout; defaults to command timeout plus grace.",
|
|
" --chunk-size N Upload/download base64 chunk size, default 2000, max 2000.",
|
|
" --json Print the full JSON response for cmd/ps or transfer summary.",
|
|
"",
|
|
"Examples:",
|
|
" node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work cmd -- pwd",
|
|
" node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work ps -- 'Get-ChildItem'",
|
|
" node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:f:/work/hwlab/.tmp upload ./hwlab-gateway-tran.mjs",
|
|
" node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work download hwlab/.tmp/hwlab-gateway-tran.mjs ./hwlab-gateway-tran.mjs"
|
|
].join("\n") + "\n");
|
|
process.exit(exitCode);
|
|
}
|
|
|
|
function fail(message) {
|
|
process.stderr.write(`error: ${message}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(`fatal: ${error.message}\n`);
|
|
process.exit(1);
|
|
});
|