import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as fs from 'node:fs/promises'; import * as os from 'node:os'; import * as path from 'node:path'; import { buildSshArgs, listConnectableHosts, parseSshConfig, readSshConfig, saveSshHostConfig, } from '../packages/ssh/ts/index.js'; tap.test('should parse ssh config hosts', async () => { const hosts = parseSshConfig(` Host * ServerAliveInterval 30 Host dev-box staging-box HostName dev.example.com User deploy Port 2222 IdentityFile ~/.ssh/id_ed25519 ProxyJump bastion Host *.internal User ignored `); const connectableHosts = listConnectableHosts(hosts); expect(connectableHosts).toHaveLength(2); expect(connectableHosts[0]!.alias).toEqual('dev-box'); expect(connectableHosts[0]!.hostName).toEqual('dev.example.com'); expect(connectableHosts[0]!.user).toEqual('deploy'); expect(connectableHosts[0]!.port).toEqual(2222); expect(connectableHosts[0]!.identityFiles[0]!).toEndWith('/.ssh/id_ed25519'); expect(connectableHosts[0]!.proxyJump).toEqual('bastion'); }); tap.test('should build ssh args with destination and command', async () => { const args = buildSshArgs( { id: 'dev-box', hostAlias: 'dev-box', user: 'deploy', port: 2222, }, 'uname -a', ); expect(args).toContain('-p'); expect(args).toContain('2222'); expect(args).toContain('deploy@dev-box'); expect(args[args.length - 1]).toEqual('uname -a'); }); tap.test('should build ssh args for one-time hostname overrides', async () => { const args = buildSshArgs( { id: 'manual-box', hostAlias: 'manual-box', hostName: '192.168.1.20', user: 'root', }, 'uname -a', ); expect(args).toContain('-o'); expect(args).toContain('HostName=192.168.1.20'); expect(args).toContain('root@manual-box'); }); tap.test('should save and update ssh host config', async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-ssh-')); const configPath = path.join(tempDir, '.ssh', 'config'); await saveSshHostConfig({ alias: 'dev-box', hostName: 'dev.example.com', user: 'deploy', port: 22, identityFile: '~/.ssh/id_ed25519', }, configPath); await saveSshHostConfig({ alias: 'dev-box', hostName: 'dev2.example.com', user: 'deploy', port: 2200, }, configPath); const configText = await fs.readFile(configPath, 'utf8'); const hosts = parseSshConfig(configText); expect(hosts).toHaveLength(1); expect(hosts[0]!.hostName).toEqual('dev2.example.com'); expect(hosts[0]!.port).toEqual(2200); expect(configText.includes('dev.example.com')).toEqual(false); }); tap.test('should read included ssh config files', async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitzone-ssh-')); const sshDir = path.join(tempDir, '.ssh'); const includeDir = path.join(sshDir, 'config.d'); await fs.mkdir(includeDir, { recursive: true }); await fs.writeFile(path.join(sshDir, 'config'), 'Include config.d/*\n'); await fs.writeFile(path.join(includeDir, 'dev.conf'), 'Host included-box\n HostName included.example.com\n'); const hosts = await readSshConfig(path.join(sshDir, 'config')); const connectableHosts = listConnectableHosts(hosts); expect(connectableHosts).toHaveLength(1); expect(connectableHosts[0]!.alias).toEqual('included-box'); expect(connectableHosts[0]!.hostName).toEqual('included.example.com'); }); export default tap.start();