UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

542 lines 19 kB
import * as path from 'path'; import { homedir } from 'os'; import { $ } from '@xec-sh/core'; import * as fs from 'fs/promises'; import { deepMerge, matchPattern, expandBraces, parseTargetReference } from './utils.js'; export class TargetResolver { constructor(config, options = {}) { this.config = config; this.options = options; this.targetsCache = new Map(); this.options.autoDetect = this.options.autoDetect ?? true; this.options.cacheTimeout = this.options.cacheTimeout ?? 60000; } async resolve(reference) { const cached = this.targetsCache.get(reference); if (cached) { return cached; } const parsed = parseTargetReference(reference); let resolved; if (parsed.type !== 'auto') { resolved = await this.resolveConfigured(parsed); if (!resolved) { throw new Error(`Target '${parsed.name}' not found in ${parsed.type}`); } } else { if (this.options.autoDetect) { resolved = await this.autoDetect(reference); } } if (!resolved) { throw new Error(`Target '${reference}' not found`); } this.targetsCache.set(reference, resolved); setTimeout(() => { this.targetsCache.delete(reference); }, this.options.cacheTimeout); return resolved; } async find(pattern) { const parsed = parseTargetReference(pattern); const targets = []; if (parsed.type === 'local') { return [await this.resolveLocal()]; } const patterns = expandBraces(parsed.name || pattern); for (const expandedPattern of patterns) { if (parsed.type === 'hosts' || parsed.type === 'auto') { targets.push(...await this.findHosts(expandedPattern)); } if (parsed.type === 'containers' || parsed.type === 'auto') { targets.push(...await this.findContainers(expandedPattern)); } if (parsed.type === 'pods' || parsed.type === 'auto') { targets.push(...await this.findPods(expandedPattern)); } } const seen = new Set(); return targets.filter(target => { if (seen.has(target.id)) { return false; } seen.add(target.id); return true; }); } async list() { const targets = []; targets.push(await this.resolveLocal()); if (this.config.targets?.hosts) { for (const [name, config] of Object.entries(this.config.targets.hosts)) { targets.push({ id: `hosts.${name}`, type: 'ssh', name, config: this.applyDefaults({ ...config, type: 'ssh' }), source: 'configured' }); } } if (this.config.targets?.containers) { for (const [name, config] of Object.entries(this.config.targets.containers)) { targets.push({ id: `containers.${name}`, type: 'docker', name, config: this.applyDefaults({ ...config, type: 'docker' }), source: 'configured' }); } } if (this.config.targets?.pods) { for (const [name, config] of Object.entries(this.config.targets.pods)) { targets.push({ id: `pods.${name}`, type: 'k8s', name, config: this.applyDefaults({ ...config, type: 'k8s' }), source: 'configured' }); } } return targets; } async create(config) { const id = this.generateTargetId(config); const resolved = { id, type: config.type, name: config.name, config, source: 'created' }; this.targetsCache.set(id, resolved); return resolved; } async resolveConfigured(ref) { if (ref.type === 'local') { return this.resolveLocal(); } const targets = this.config.targets; if (!targets) { return undefined; } let targetConfig; let targetType; switch (ref.type) { case 'hosts': targetConfig = targets.hosts?.[ref.name]; targetType = 'ssh'; break; case 'containers': targetConfig = targets.containers?.[ref.name]; targetType = 'docker'; break; case 'pods': targetConfig = targets.pods?.[ref.name]; targetType = 'k8s'; break; default: return undefined; } if (!targetConfig) { return undefined; } const fullConfig = { type: targetType }; for (const key in targetConfig) { if (Object.prototype.hasOwnProperty.call(targetConfig, key)) { const value = targetConfig[key]; if (value !== undefined) { fullConfig[key] = value; } } } return { id: `${ref.type}.${ref.name}`, type: targetType, name: ref.name, config: this.applyDefaults(fullConfig), source: 'configured' }; } async resolveLocal() { return { id: 'local', type: 'local', config: this.applyDefaults({ type: 'local', ...this.config.targets?.local }), source: 'configured' }; } async findHosts(pattern) { const targets = []; if (this.config.targets?.hosts) { for (const [name, config] of Object.entries(this.config.targets.hosts)) { if (matchPattern(pattern, name)) { targets.push({ id: `hosts.${name}`, type: 'ssh', name, config: this.applyDefaults({ ...config, type: 'ssh' }), source: 'configured' }); } } } return targets; } async findContainers(pattern) { const targets = []; if (this.config.targets?.containers) { for (const [name, config] of Object.entries(this.config.targets.containers)) { if (matchPattern(pattern, name)) { targets.push({ id: `containers.${name}`, type: 'docker', name, config: this.applyDefaults({ ...config, type: 'docker' }), source: 'configured' }); } } } if (this.config.targets?.$compose && this.options.autoDetect) { const composeTargets = await this.findComposeServices(pattern); targets.push(...composeTargets); } return targets; } async findPods(pattern) { const targets = []; if (this.config.targets?.pods) { for (const [name, config] of Object.entries(this.config.targets.pods)) { if (matchPattern(pattern, name)) { targets.push({ id: `pods.${name}`, type: 'k8s', name, config: this.applyDefaults({ ...config, type: 'k8s' }), source: 'configured' }); } } } return targets; } async autoDetect(reference) { if (await this.isDockerContainer(reference)) { return { id: reference, type: 'docker', name: reference, config: this.applyDefaults({ type: 'docker', container: reference }), source: 'detected' }; } if (await this.isKubernetesPod(reference)) { const namespace = this.config.targets?.kubernetes?.$namespace || 'default'; return { id: reference, type: 'k8s', name: reference, config: this.applyDefaults({ type: 'k8s', pod: reference, namespace }), source: 'detected' }; } const sshHost = await this.getSSHHost(reference); if (sshHost) { return { id: reference, type: 'ssh', name: reference, config: this.applyDefaults(sshHost), source: 'detected' }; } if (reference.includes('.') || reference.includes('@')) { let host = reference; let user; if (reference.includes('@')) { const parts = reference.split('@', 2); user = parts[0]; host = parts[1] || host; } return { id: reference, type: 'ssh', name: reference, config: this.applyDefaults({ type: 'ssh', host, user }), source: 'detected' }; } return undefined; } async isDockerContainer(name) { try { const result = await $ `docker ps --format "{{.Names}}"`.nothrow(); if (!result.ok) { return false; } const containers = result.stdout.trim().split('\n').filter(line => line); return containers.includes(name); } catch { return false; } } async isKubernetesPod(name) { try { const namespace = this.config.targets?.kubernetes?.$namespace || 'default'; const context = this.config.targets?.kubernetes?.$context; const args = ['get', 'pod', name, '-n', namespace]; if (context) { args.push('--context', context); } const result = await $ `kubectl ${args.join(' ')}`.quiet().nothrow(); return result.ok; } catch { return false; } } async getSSHHost(name) { try { const sshConfigPath = path.join(homedir(), '.ssh', 'config'); const configContent = await fs.readFile(sshConfigPath, 'utf-8'); const lines = configContent.split('\n'); let currentHost; const hosts = {}; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Host ')) { currentHost = trimmed.substring(5).trim(); hosts[currentHost] = {}; } else if (currentHost && trimmed.includes(' ')) { const [key, ...valueParts] = trimmed.split(/\s+/); const value = valueParts.join(' '); const keyMap = { 'HostName': 'host', 'User': 'user', 'Port': 'port', 'IdentityFile': 'privateKey' }; if (key) { const mappedKey = keyMap[key]; if (mappedKey && currentHost) { hosts[currentHost][mappedKey] = value; } } } } if (hosts[name]) { return { type: 'ssh', host: hosts[name].host || name, ...hosts[name] }; } } catch { } return undefined; } async findComposeServices(pattern) { const targets = []; const compose = this.config.targets?.$compose; if (!compose) { return targets; } try { const args = ['compose']; if (compose.file) { args.push('-f', compose.file); } if (compose.project) { args.push('-p', compose.project); } args.push('ps', '--format', 'json'); const result = await $.shell(false) `docker ${args}`.nothrow(); if (!result.ok) { return targets; } const lines = result.stdout.trim().split('\n').filter(line => line); for (const line of lines) { const service = JSON.parse(line); if (matchPattern(pattern, service.Service)) { targets.push({ id: `containers.${service.Service}`, type: 'docker', name: service.Service, config: this.applyDefaults({ type: 'docker', container: service.Name }), source: 'detected' }); } } } catch { } return targets; } generateTargetId(config) { switch (config.type) { case 'ssh': return `dynamic-ssh-${config.host}`; case 'docker': return `dynamic-docker-${config.container || 'ephemeral'}`; case 'k8s': return `dynamic-k8s-${config.pod || 'unknown'}`; case 'local': return 'local'; } } clearCache() { this.targetsCache.clear(); } applyDefaults(targetConfig) { const defaults = this.config.targets?.defaults; if (!defaults) { return targetConfig; } const commonDefaults = {}; if (defaults.timeout !== undefined) { commonDefaults.timeout = defaults.timeout; } if (defaults.shell !== undefined) { commonDefaults.shell = defaults.shell; } if (defaults.encoding !== undefined) { commonDefaults.encoding = defaults.encoding; } if (defaults.maxBuffer !== undefined) { commonDefaults.maxBuffer = defaults.maxBuffer; } if (defaults.throwOnNonZeroExit !== undefined) { commonDefaults.throwOnNonZeroExit = defaults.throwOnNonZeroExit; } if (defaults.cwd !== undefined) { commonDefaults.cwd = defaults.cwd; } if (defaults.env) { commonDefaults.env = defaults.env; } let typeSpecificDefaults = {}; switch (targetConfig.type) { case 'ssh': if (defaults.ssh) { typeSpecificDefaults = this.applySshDefaults(defaults.ssh, targetConfig); } break; case 'docker': if (defaults.docker) { typeSpecificDefaults = this.applyDockerDefaults(defaults.docker, targetConfig); } break; case 'k8s': if (defaults.kubernetes) { typeSpecificDefaults = this.applyKubernetesDefaults(defaults.kubernetes, targetConfig); } break; case 'local': break; } const withCommonDefaults = deepMerge({}, commonDefaults); const withTypeDefaults = deepMerge(withCommonDefaults, typeSpecificDefaults); let final = deepMerge(withTypeDefaults, targetConfig); if (targetConfig.type === 'k8s') { const k8sTarget = targetConfig; if (defaults.kubernetes?.execFlags && k8sTarget.execFlags) { final = { ...final, execFlags: [...(defaults.kubernetes.execFlags || []), ...(k8sTarget.execFlags || [])] }; } } return final; } applySshDefaults(sshDefaults, hostConfig) { const defaults = {}; if (sshDefaults.port !== undefined) { defaults.port = sshDefaults.port; } if (sshDefaults.keepAlive !== undefined) { defaults.keepAlive = sshDefaults.keepAlive; } if (sshDefaults.keepAliveInterval !== undefined) { defaults.keepAliveInterval = sshDefaults.keepAliveInterval; } if (sshDefaults.connectionPool !== undefined) { defaults.connectionPool = sshDefaults.connectionPool; } if (sshDefaults.sudo !== undefined) { defaults.sudo = sshDefaults.sudo; } if (sshDefaults.sftp !== undefined) { defaults.sftp = sshDefaults.sftp; } return defaults; } applyDockerDefaults(dockerDefaults, containerConfig) { const defaults = {}; if (dockerDefaults.tty !== undefined) { defaults.tty = dockerDefaults.tty; } if (dockerDefaults.workdir !== undefined) { defaults.workdir = dockerDefaults.workdir; } if (dockerDefaults.autoRemove !== undefined) { defaults.autoRemove = dockerDefaults.autoRemove; } if (dockerDefaults.socketPath !== undefined) { defaults.socketPath = dockerDefaults.socketPath; } if (dockerDefaults.user !== undefined) { defaults.user = dockerDefaults.user; } if (dockerDefaults.runMode !== undefined) { defaults.runMode = dockerDefaults.runMode; } return defaults; } applyKubernetesDefaults(k8sDefaults, podConfig) { const defaults = {}; if (k8sDefaults.namespace !== undefined) { defaults.namespace = k8sDefaults.namespace; } if (k8sDefaults.tty !== undefined) { defaults.tty = k8sDefaults.tty; } if (k8sDefaults.stdin !== undefined) { defaults.stdin = k8sDefaults.stdin; } if (k8sDefaults.kubeconfig !== undefined) { defaults.kubeconfig = k8sDefaults.kubeconfig; } if (k8sDefaults.context !== undefined) { defaults.context = k8sDefaults.context; } if (k8sDefaults.execFlags !== undefined) { defaults.execFlags = k8sDefaults.execFlags; } return defaults; } } //# sourceMappingURL=target-resolver.js.map