import * as plugins from './plugins.js'; import type { ICommandResult } from './types.js'; export interface ISshRunOptions { extraArgs?: string[]; localForwards?: string[]; reverseForwards?: string[]; remoteCommand?: string; exitOnForwardFailure?: boolean; } export class SshClient { public buildSshArgs(host: string, options: ISshRunOptions = {}): string[] { const args: string[] = []; if (options.exitOnForwardFailure || options.localForwards?.length || options.reverseForwards?.length) { args.push('-o', 'ExitOnForwardFailure=yes'); } for (const localForward of options.localForwards ?? []) { args.push('-L', localForward); } for (const reverseForward of options.reverseForwards ?? []) { args.push('-R', reverseForward); } args.push(...(options.extraArgs ?? [])); args.push(host); if (options.remoteCommand) { args.push(options.remoteCommand); } return args; } public async ssh(host: string, options: ISshRunOptions = {}): Promise { return this.spawnInteractive('ssh', this.buildSshArgs(host, options)); } public async spawnInteractive(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise { return new Promise((resolve, reject) => { const child = plugins.childProcess.spawn(command, args, { stdio: 'inherit', shell: false, ...options, }); child.once('error', reject); child.once('exit', (code, signal) => { if (signal) { resolve(128); return; } resolve(code ?? 0); }); }); } public async spawnDetached(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise { const child = plugins.childProcess.spawn(command, args, { stdio: 'ignore', detached: true, shell: false, ...options, }); child.unref(); return child.pid ?? 0; } public async runCapture(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise { return new Promise((resolve, reject) => { const child = plugins.childProcess.spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: false, ...options, }); const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; child.stdout?.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk))); child.stderr?.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk))); child.once('error', reject); child.once('exit', (code) => { resolve({ exitCode: code ?? 0, stdout: Buffer.concat(stdoutChunks).toString('utf8'), stderr: Buffer.concat(stderrChunks).toString('utf8'), }); }); }); } public async commandExists(command: string): Promise { const result = await this.runCapture('/bin/sh', ['-lc', `command -v ${SshClient.quoteForSh(command)}`]); return result.exitCode === 0; } public static quoteForSh(value: string): string { return `'${value.replace(/'/g, `'"'"'`)}'`; } }