feat(core): add SSH data access proxy CLI and core managers

This commit is contained in:
2026-05-30 10:02:08 +00:00
commit 47d9846c93
23 changed files with 10399 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
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<number> {
return this.spawnInteractive('ssh', this.buildSshArgs(host, options));
}
public async spawnInteractive(command: string, args: string[], options: plugins.SpawnOptions = {}): Promise<number> {
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<number> {
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<ICommandResult> {
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<boolean> {
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, `'"'"'`)}'`;
}
}