import { Client } from 'ssh2';
import { readFileSync } from 'fs';
import type { SSHConnectionConfig } from '../types.js';
export async function createSSHConnection(config: SSHConnectionConfig): Promise<Client> {
return new Promise((resolve, reject) => {
const client = new Client();
const connectionConfig: Record<string, unknown> = {
host: config.host,
port: config.port || 22,
username: config.username,
readyTimeout: 30000,
keepaliveInterval: 10000,
keepaliveCountMax: 3,
};
// Handle authentication
if (config.privateKey) {
connectionConfig.privateKey = config.privateKey;
if (config.passphrase) {
connectionConfig.passphrase = config.passphrase;
}
} else if (config.privateKeyPath) {
try {
connectionConfig.privateKey = readFileSync(config.privateKeyPath, 'utf8');
if (config.passphrase) {
connectionConfig.passphrase = config.passphrase;
}
} catch (err) {
reject(new Error(`Failed to read private key from ${config.privateKeyPath}: ${err}`));
return;
}
} else if (config.password) {
connectionConfig.password = config.password;
} else {
reject(new Error('No authentication method provided. Supply password, privateKey, or privateKeyPath.'));
return;
}
client.on('ready', () => {
resolve(client);
});
client.on('error', (err) => {
reject(new Error(`SSH connection error: ${err.message}`));
});
client.on('timeout', () => {
reject(new Error('SSH connection timeout'));
});
client.connect(connectionConfig);
});
}
export function isClientConnected(client: Client): boolean {
// Check if the client is still connected by checking the internal state
// ssh2 doesn't expose a direct "connected" property, so we rely on the socket
try {
// @ts-expect-error - accessing internal property
return client._sock && !client._sock.destroyed;
} catch {
return false;
}
}
export function closeConnection(client: Client): void {
try {
client.end();
} catch {
// Ignore errors during close
}
}