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
+9 -1
View File
@@ -6,8 +6,10 @@ import { emitError, emitJson } from "./src/output";
import { jobWithTail, listJobs, readJob, runJob } from "./src/jobs";
import { runChecks } from "./src/check";
import { runSsh } from "./src/ssh";
import { extractRemoteCliOptions, runRemoteCli } from "./src/remote";
const args = process.argv.slice(2);
const remoteOptions = extractRemoteCliOptions(process.argv.slice(2));
const args = remoteOptions.args;
const commandName = args.join(" ") || "help";
function help(): unknown {
@@ -16,6 +18,7 @@ function help(): unknown {
output: "json",
commands: [
{ command: "help", description: "List supported commands." },
{ command: "--main-server-ip <ip> [--main-server-user root] [--main-server-key path] <command>", description: "Pass the command through SSH and execute this CLI on the main server." },
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
{ command: "check", description: "Run config, TypeScript, file presence, and docker-compose config checks." },
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, and provider gateway." },
@@ -84,6 +87,11 @@ function latestJobId(): string {
}
async function main(): Promise<void> {
if (remoteOptions.host !== null) {
process.exitCode = await runRemoteCli(remoteOptions);
return;
}
const [top, sub, third, fourth] = args;
if (top === undefined || top === "help" || top === "--help" || top === "-h") {
emitJson(commandName, help());
+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));
});
}