| 1 | import { Client } from 'ssh2'; |
| 2 | import fs from 'fs'; |
| 3 | |
| 4 | const KEY_PATH = process.env.ZPM_SSH_KEY || '/root/.ssh/id_ed25519'; |
| 5 | |
| 6 | export interface SSHResult { code: number; stdout: string; stderr: string; } |
| 7 | |
| 8 | export function runSSH(host: string, user: string, command: string, timeoutMs = 120_000): Promise<SSHResult> { |
| 9 | return new Promise((resolve, reject) => { |
| 10 | const conn = new Client(); |
| 11 | let stdout = ''; |
| 12 | let stderr = ''; |
| 13 | let settled = false; |
| 14 | const done = (fn: () => void) => { if (!settled) { settled = true; clearTimeout(timer); try { conn.end(); } catch {} fn(); } }; |
| 15 | const timer = setTimeout(() => done(() => reject(new Error(`ssh timeout after ${timeoutMs}ms`))), timeoutMs); |
| 16 | conn |
| 17 | .on('ready', () => { |
| 18 | conn.exec(command, (err, stream) => { |
| 19 | if (err) return done(() => reject(err)); |
| 20 | stream |
| 21 | .on('close', (code: number) => done(() => resolve({ code: code ?? 0, stdout, stderr }))) |
| 22 | .on('data', (d: Buffer) => { stdout += d.toString(); }) |
| 23 | .stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); |
| 24 | }); |
| 25 | }) |
| 26 | .on('error', (e) => done(() => reject(e))) |
| 27 | .connect({ host, port: 22, username: user, privateKey: fs.readFileSync(KEY_PATH), readyTimeout: Math.min(timeoutMs, 20_000) }); |
| 28 | }); |
| 29 | } |
| 30 | |
| 31 | export const b64 = (s: string) => Buffer.from(s, 'utf8').toString('base64'); |