feat: add remote main server cli passthrough

This commit is contained in:
Codex
2026-05-05 01:56:42 +00:00
parent eb6df3ba92
commit cc1533c02f
7 changed files with 150 additions and 2 deletions
+120
View File
@@ -0,0 +1,120 @@
import { spawn } from "node:child_process";
export interface RemoteCliOptions {
host: string | null;
user: string;
port: number;
projectRoot: string;
identityFile: string | null;
args: string[];
}
const hostOptions = new Set(["--main-server-ip", "--main-server", "--server"]);
const userOptions = new Set(["--main-server-user", "--server-user"]);
const portOptions = new Set(["--main-server-port", "--server-port"]);
const rootOptions = new Set(["--main-server-root", "--server-root"]);
const keyOptions = new Set(["--main-server-key", "--server-key"]);
function positivePort(raw: string, option: string): number {
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0 || value > 65535) throw new Error(`${option} must be a TCP port from 1 to 65535`);
return value;
}
function requiredValue(argv: string[], index: number, option: string): string {
const value = argv[index + 1];
if (value === undefined || value.length === 0) throw new Error(`${option} requires a non-empty value`);
return value;
}
export function extractRemoteCliOptions(argv: string[]): RemoteCliOptions {
const rest: string[] = [];
const options: RemoteCliOptions = {
host: null,
user: "root",
port: 22,
projectRoot: "/root/unidesk",
identityFile: null,
args: rest,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index] ?? "";
if (arg === "--") {
rest.push(...argv.slice(index + 1));
break;
}
if (hostOptions.has(arg)) {
options.host = requiredValue(argv, index, arg);
index += 1;
continue;
}
if (userOptions.has(arg)) {
options.user = requiredValue(argv, index, arg);
index += 1;
continue;
}
if (portOptions.has(arg)) {
options.port = positivePort(requiredValue(argv, index, arg), arg);
index += 1;
continue;
}
if (rootOptions.has(arg)) {
options.projectRoot = requiredValue(argv, index, arg);
index += 1;
continue;
}
if (keyOptions.has(arg)) {
options.identityFile = requiredValue(argv, index, arg);
index += 1;
continue;
}
rest.push(arg);
}
return options;
}
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
export async function runRemoteCli(options: RemoteCliOptions): Promise<number> {
if (options.host === null) throw new Error("runRemoteCli requires --main-server-ip or --server");
const remoteArgs = options.args.length === 0 ? ["help"] : options.args;
const remoteCommand = [
"cd",
shellQuote(options.projectRoot),
"&&",
"exec",
"bun",
"scripts/cli.ts",
...remoteArgs.map(shellQuote),
].join(" ");
const sshArgs = [
"-p",
String(options.port),
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
"ServerAliveInterval=20",
"-o",
"ServerAliveCountMax=3",
];
if (options.identityFile !== null) sshArgs.push("-i", options.identityFile);
sshArgs.push(`${options.user}@${options.host}`, remoteCommand);
const child = spawn("ssh", sshArgs, {
stdio: ["inherit", "inherit", "inherit"],
});
return await new Promise<number>((resolve) => {
child.on("error", (error) => {
process.stderr.write(`unidesk remote cli failed to start ssh: ${error.message}\n`);
resolve(127);
});
child.on("close", (code) => resolve(code ?? 255));
});
}