ssh-service.ts•4.92 kB
import { NodeSSH } from 'node-ssh';
import { readFileSync } from 'fs';
import { SSHConfig, CommandResult } from '../types/index.js';
import { logger } from '../utils/logger.js';
export class SSHService {
private ssh: NodeSSH;
private isConnected = false;
private config: SSHConfig;
constructor(config: SSHConfig) {
this.ssh = new NodeSSH();
this.config = config;
}
async connect(): Promise<boolean> {
try {
const connectionConfig: Parameters<NodeSSH['connect']>[0] = {
host: this.config.host,
port: this.config.port || 22,
username: this.config.username,
};
// Handle authentication methods
if (this.config.privateKeyPath) {
logger.info('Authenticating with private key', {
keyPath: this.config.privateKeyPath,
});
connectionConfig.privateKey = readFileSync(this.config.privateKeyPath, 'utf8');
if (this.config.passphrase) {
connectionConfig.passphrase = this.config.passphrase;
}
} else if (this.config.password) {
logger.info('Authenticating with password');
connectionConfig.password = this.config.password;
} else {
throw new Error('Either password or privateKeyPath must be provided');
}
await this.ssh.connect(connectionConfig);
this.isConnected = true;
logger.info('Successfully connected to SSH server', {
host: this.config.host,
username: this.config.username,
});
return true;
} catch (error) {
logger.error('Failed to connect to SSH server', {
error: error instanceof Error ? error.message : 'Unknown error',
host: this.config.host,
});
this.isConnected = false;
return false;
}
}
async executeCommand(command: string): Promise<CommandResult> {
if (!this.isConnected) {
throw new Error('SSH connection not established');
}
try {
logger.debug('Executing command', { command });
const result = await this.ssh.execCommand(command);
const commandResult: CommandResult = {
success: result.code === 0,
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.code || 0,
};
if (commandResult.success) {
logger.debug('Command executed successfully', {
command,
exitCode: commandResult.exitCode,
});
} else {
logger.warn('Command execution failed', {
command,
exitCode: commandResult.exitCode,
stderr: commandResult.stderr,
});
}
return commandResult;
} catch (error) {
logger.error('Error executing command', {
command,
error: error instanceof Error ? error.message : 'Unknown error',
});
return {
success: false,
stdout: '',
stderr: error instanceof Error ? error.message : 'Unknown error',
exitCode: -1,
};
}
}
async uploadFile(localPath: string, remotePath: string): Promise<boolean> {
if (!this.isConnected) {
throw new Error('SSH connection not established');
}
try {
logger.info('Uploading file', { localPath, remotePath });
await this.ssh.putFile(localPath, remotePath);
logger.info('File uploaded successfully', { localPath, remotePath });
return true;
} catch (error) {
logger.error('Failed to upload file', {
localPath,
remotePath,
error: error instanceof Error ? error.message : 'Unknown error',
});
return false;
}
}
async downloadFile(remotePath: string, localPath: string): Promise<boolean> {
if (!this.isConnected) {
throw new Error('SSH connection not established');
}
try {
logger.info('Downloading file', { remotePath, localPath });
await this.ssh.getFile(localPath, remotePath);
logger.info('File downloaded successfully', { remotePath, localPath });
return true;
} catch (error) {
logger.error('Failed to download file', {
remotePath,
localPath,
error: error instanceof Error ? error.message : 'Unknown error',
});
return false;
}
}
async ensureDirectory(path: string): Promise<boolean> {
const result = await this.executeCommand(`mkdir -p "${path}"`);
return result.success;
}
async fileExists(path: string): Promise<boolean> {
const result = await this.executeCommand(`test -f "${path}"`);
return result.success;
}
async directoryExists(path: string): Promise<boolean> {
const result = await this.executeCommand(`test -d "${path}"`);
return result.success;
}
async disconnect(): Promise<void> {
if (this.isConnected) {
this.ssh.dispose();
this.isConnected = false;
logger.info('SSH connection closed');
}
}
isConnectionActive(): boolean {
return this.isConnected;
}
}