UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

260 lines 9.66 kB
import { $ } from '@xec-sh/core'; import * as fs from 'fs/promises'; import { TargetResolver } from '../config/target-resolver.js'; import { createTargetEngine } from '../utils/direct-execution.js'; import { ConfigurationManager } from '../config/configuration-manager.js'; export class TargetAPI { constructor() { this.activeForwards = new Map(); this.configManager = new ConfigurationManager(); } async initialize() { if (!this.resolver) { await this.configManager.load(); this.resolver = new TargetResolver(this.configManager.getConfig()); } } async list(type) { await this.initialize(); const config = this.configManager.getConfig(); const targets = []; if (!type || type === 'ssh') { const hosts = config.targets?.hosts || {}; for (const [name, hostConfig] of Object.entries(hosts)) { targets.push({ id: `hosts.${name}`, type: 'ssh', name, config: { ...hostConfig, type: 'ssh' }, source: 'configured' }); } } if (!type || type === 'docker') { const containers = config.targets?.containers || {}; for (const [name, containerConfig] of Object.entries(containers)) { targets.push({ id: `containers.${name}`, type: 'docker', name, config: { ...containerConfig, type: 'docker' }, source: 'configured' }); } } if (!type || type === 'k8s') { const pods = config.targets?.pods || {}; for (const [name, podConfig] of Object.entries(pods)) { targets.push({ id: `pods.${name}`, type: 'k8s', name, config: { ...podConfig, type: 'k8s' }, source: 'configured' }); } } return targets; } async get(ref) { await this.initialize(); try { return await this.resolver.resolve(ref); } catch { return undefined; } } async find(pattern) { await this.initialize(); if (pattern.includes('*') || pattern.includes(',')) { return this.resolver.find(pattern); } const target = await this.get(pattern); return target ? [target] : []; } async exec(ref, command, options = {}) { await this.initialize(); const target = await this.resolver.resolve(ref); const engine = await createTargetEngine(target, options); const result = await engine `${command}`; return { ...result, target }; } async copy(source, destination, options = {}) { await this.initialize(); const { target: sourceTarget, path: sourcePath } = this.parseTargetPath(source); const { target: destTarget, path: destPath } = this.parseTargetPath(destination); if (sourceTarget && destTarget) { throw new Error('Cannot copy between two remote targets directly'); } const isUpload = !sourceTarget && Boolean(destTarget); const target = sourceTarget || destTarget; if (!target) { if (options.recursive) { await fs.cp(sourcePath, destPath, { recursive: true }); } else { await fs.copyFile(sourcePath, destPath); } return; } const resolvedTarget = await this.resolver.resolve(target); switch (resolvedTarget.type) { case 'ssh': await this.copySSH(resolvedTarget, sourcePath, destPath, isUpload, options); break; case 'docker': await this.copyDocker(resolvedTarget, sourcePath, destPath, isUpload, options); break; case 'k8s': await this.copyKubernetes(resolvedTarget, sourcePath, destPath, isUpload, options); break; default: throw new Error(`Copy not supported for target type: ${resolvedTarget.type}`); } } async forward(target, localPort, options = {}) { await this.initialize(); const match = target.match(/^(.+):(\d+)$/); if (!match) { throw new Error('Target must include port (e.g., hosts.web:8080)'); } const targetRef = match[1]; const remotePortStr = match[2]; if (!targetRef || !remotePortStr) { throw new Error('Invalid target format'); } const remotePort = parseInt(remotePortStr, 10); const resolvedTarget = await this.resolver.resolve(targetRef); if (!localPort && options.dynamic) { localPort = await this.findAvailablePort(); } else if (!localPort) { localPort = remotePort; } let forwardProcess; switch (resolvedTarget.type) { case 'ssh': forwardProcess = await this.forwardSSH(resolvedTarget, localPort, remotePort); break; case 'k8s': forwardProcess = await this.forwardKubernetes(resolvedTarget, localPort, remotePort); break; default: throw new Error(`Port forwarding not supported for target type: ${resolvedTarget.type}`); } const forward = { localPort, remotePort, target: resolvedTarget, close: async () => { if (forwardProcess) { forwardProcess.kill(); } this.activeForwards.delete(`${targetRef}:${remotePort}`); } }; this.activeForwards.set(`${targetRef}:${remotePort}`, forward); return forward; } async create(definition) { await this.initialize(); const config = definition.config || {}; config.type = definition.type; const target = { id: `dynamic.${definition.name}`, type: definition.type, name: definition.name, config, source: 'created' }; const engine = await createTargetEngine(target); return target; } async test(ref) { try { const result = await this.exec(ref, 'echo "test"', { timeout: 5000, throwOnNonZeroExit: false }); return result.ok; } catch { return false; } } getActiveForwards() { return Array.from(this.activeForwards.values()); } async closeAllForwards() { const forwards = Array.from(this.activeForwards.values()); await Promise.all(forwards.map(f => f.close())); } parseTargetPath(path) { const match = path.match(/^([^:]+):(.+)$/); if (match) { return { target: match[1] || undefined, path: match[2] || '' }; } return { path }; } async copySSH(target, source, dest, isUpload, options) { const config = target.config; const { host, user, port = 22, privateKey } = config; const sshDest = `${user}@${host}:`; const scpArgs = [ port !== 22 ? `-P ${port}` : null, privateKey ? `-i ${privateKey}` : null, options.recursive === true ? '-r' : null, options.compress === true ? '-C' : null, isUpload ? source : `${sshDest}${source}`, isUpload ? `${sshDest}${dest}` : dest ].filter((arg) => arg !== null).join(' '); await $ `scp ${scpArgs}`; } async copyDocker(target, source, dest, isUpload, options) { const config = target.config; const container = config.container || target.name; if (isUpload) { await $ `docker cp ${source} ${container}:${dest}`; } else { await $ `docker cp ${container}:${source} ${dest}`; } } async copyKubernetes(target, source, dest, isUpload, options) { const config = target.config; const { namespace = 'default', pod, container } = config; const containerFlag = container ? `-c ${container}` : ''; if (isUpload) { await $ `kubectl cp ${source} ${namespace}/${pod}:${dest} ${containerFlag}`; } else { await $ `kubectl cp ${namespace}/${pod}:${source} ${dest} ${containerFlag}`; } } async forwardSSH(target, localPort, remotePort) { const config = target.config; const { host, user, port = 22, privateKey } = config; const sshArgs = [ '-N', '-L', `${localPort}:localhost:${remotePort}`, port !== 22 ? `-p ${port}` : null, privateKey ? `-i ${privateKey}` : null, `${user}@${host}` ].filter((arg) => arg !== null).join(' '); return $ `ssh ${sshArgs}`.nothrow(); } async forwardKubernetes(target, localPort, remotePort) { const config = target.config; const { namespace = 'default', pod } = config; return $ `kubectl port-forward -n ${namespace} ${pod} ${localPort}:${remotePort}`.nothrow(); } async findAvailablePort() { return Math.floor(Math.random() * (65535 - 30000) + 30000); } } export const targets = new TargetAPI(); //# sourceMappingURL=target-api.js.map