mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
The SSH modules predate the stricter lint config that landed on main (curly, no-empty, perfectionist sorting, prettier). Mechanical lint:fix + fmt pass, empty catch blocks filled with the codebase's void-0 convention, and inline no-control-regex disables on the three deliberate control-char patterns (same pattern as lib/ansi.ts).
175 lines
3.7 KiB
TypeScript
175 lines
3.7 KiB
TypeScript
/**
|
|
* ssh-config.ts
|
|
*
|
|
* Pure, electron-free helpers for reading the user's OpenSSH client config:
|
|
* `Host` aliases for the settings UI's suggestions, `Include` traversal
|
|
* (read-only), and `ssh -G` output parsing. No `import 'electron'` so it's
|
|
* unit-testable without Electron; main.ts wires the fs + `ssh -G` exec in.
|
|
*/
|
|
|
|
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
function parseSshConfigHosts(text) {
|
|
const hosts: string[] = []
|
|
const seen = new Set()
|
|
|
|
for (const rawLine of String(text || '').split('\n')) {
|
|
const line = rawLine.trim()
|
|
|
|
if (!line || line.startsWith('#')) {
|
|
continue
|
|
}
|
|
|
|
const m = /^host\s+(.+)$/i.exec(line)
|
|
|
|
if (!m) {
|
|
continue
|
|
}
|
|
|
|
for (const pattern of m[1].split(/\s+/)) {
|
|
if (!pattern || pattern.includes('*') || pattern.includes('?') || pattern.startsWith('!')) {
|
|
continue
|
|
}
|
|
|
|
if (!seen.has(pattern)) {
|
|
seen.add(pattern)
|
|
hosts.push(pattern)
|
|
}
|
|
}
|
|
}
|
|
|
|
return hosts
|
|
}
|
|
|
|
function parseSshConfigIncludes(text) {
|
|
const includes: string[] = []
|
|
|
|
for (const rawLine of String(text || '').split('\n')) {
|
|
const line = rawLine.trim()
|
|
|
|
if (!line || line.startsWith('#')) {
|
|
continue
|
|
}
|
|
|
|
const m = /^include\s+(.+)$/i.exec(line)
|
|
|
|
if (!m) {
|
|
continue
|
|
}
|
|
|
|
for (const token of m[1].split(/\s+/)) {
|
|
if (token) {
|
|
includes.push(token)
|
|
}
|
|
}
|
|
}
|
|
|
|
return includes
|
|
}
|
|
|
|
function collectSshConfigHosts(rootPath = '', deps: any = {}) {
|
|
const readFile =
|
|
deps.readFile ||
|
|
(p => {
|
|
try {
|
|
return fs.readFileSync(p, 'utf8')
|
|
} catch {
|
|
return null
|
|
}
|
|
})
|
|
|
|
const homeDir = deps.homeDir || os.homedir()
|
|
const root = rootPath || path.join(homeDir, '.ssh', 'config')
|
|
const sshDir = path.join(homeDir, '.ssh')
|
|
|
|
const out: string[] = []
|
|
const seen = new Set()
|
|
const visited = new Set()
|
|
|
|
const resolveIncludePath = token => {
|
|
if (token.startsWith('~/')) {
|
|
return path.join(homeDir, token.slice(2))
|
|
}
|
|
|
|
if (path.isAbsolute(token)) {
|
|
return token
|
|
}
|
|
|
|
return path.join(sshDir, token)
|
|
}
|
|
|
|
const walk = (filePath, depth) => {
|
|
if (depth > 8 || visited.has(filePath)) {
|
|
return
|
|
}
|
|
|
|
visited.add(filePath)
|
|
const text = readFile(filePath)
|
|
|
|
if (text == null) {
|
|
return
|
|
}
|
|
|
|
for (const host of parseSshConfigHosts(text)) {
|
|
if (!seen.has(host)) {
|
|
seen.add(host)
|
|
out.push(host)
|
|
}
|
|
}
|
|
|
|
for (const token of parseSshConfigIncludes(text)) {
|
|
const target = resolveIncludePath(token)
|
|
const expanded = deps.globSync ? deps.globSync(target) : [target]
|
|
|
|
for (const p of expanded) {
|
|
walk(p, depth + 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(root, 0)
|
|
|
|
return out
|
|
}
|
|
|
|
function parseSshGOutput(text) {
|
|
const out: { hostname: string | null; user: string | null; port: number | null; identityFile: string | null } = {
|
|
hostname: null,
|
|
user: null,
|
|
port: null,
|
|
identityFile: null
|
|
}
|
|
|
|
for (const rawLine of String(text || '').split('\n')) {
|
|
const line = rawLine.trim()
|
|
|
|
if (!line) {
|
|
continue
|
|
}
|
|
|
|
const sp = line.indexOf(' ')
|
|
|
|
if (sp === -1) {
|
|
continue
|
|
}
|
|
|
|
const key = line.slice(0, sp).toLowerCase()
|
|
const value = line.slice(sp + 1).trim()
|
|
|
|
if (key === 'hostname' && !out.hostname) {
|
|
out.hostname = value
|
|
} else if (key === 'user' && !out.user) {
|
|
out.user = value
|
|
} else if (key === 'port' && !out.port) {
|
|
out.port = Number.parseInt(value, 10) || null
|
|
} else if (key === 'identityfile' && !out.identityFile) {
|
|
out.identityFile = value
|
|
}
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
export { collectSshConfigHosts, parseSshConfigHosts, parseSshConfigIncludes, parseSshGOutput }
|