UNPKG

@xec-sh/core

Version:

Universal shell execution engine

334 lines 12.9 kB
import { unifiedConfig } from '../config/unified-config.js'; export class ConvenienceAPI { constructor(engine) { this.engine = engine; } async onHost(hosts, command, ...values) { const hostArray = Array.isArray(hosts) ? hosts : [hosts]; if (hostArray.length === 1) { const hostConfig = await this.resolveHost(hostArray[0]); const $ssh = this.engine.ssh(hostConfig); if (typeof command === 'string') { const cmd = command; return $ssh `${cmd}`; } else { return $ssh(command, ...values); } } const tasks = hostArray.map(async (host) => { const hostConfig = await this.resolveHost(host); const $ssh = this.engine.ssh(hostConfig); if (typeof command === 'string') { const cmd = command; return $ssh `${cmd}`; } else { return $ssh(command, ...values); } }); return Promise.all(tasks); } async in(target, command, ...values) { if (target.startsWith('pod:')) { const podName = target.substring(4); const podConfig = await this.resolvePod(podName); const $k8s = this.engine.k8s(podConfig); if (!command) { return $k8s `/bin/sh`; } if (typeof command === 'string') { const cmd = command; return $k8s `${cmd}`; } else { return $k8s(command, ...values); } } else { const containerName = target.startsWith('container:') ? target.substring(10) : target; const containerConfig = await this.resolveContainer(containerName); const $docker = this.engine.docker(containerConfig); if (!command) { return $docker `/bin/sh`; } if (typeof command === 'string') { const cmd = command; return $docker `${cmd}`; } else { return $docker(command, ...values); } } } async copy(source, destination) { const sourceInfo = this.parseLocation(source); const destInfo = this.parseLocation(destination); if (sourceInfo.type === 'local' && destInfo.type === 'local') { await this.engine.run `cp -r ${source} ${destination}`; return; } if (sourceInfo.type === 'ssh' || destInfo.type === 'ssh') { if (sourceInfo.type === 'ssh' && destInfo.type === 'local') { const hostConfig = await this.resolveHost(sourceInfo.host); const $ssh = this.engine.ssh(hostConfig); await $ssh.downloadFile(sourceInfo.path, destInfo.path); } else if (sourceInfo.type === 'local' && destInfo.type === 'ssh') { const hostConfig = await this.resolveHost(destInfo.host); const $ssh = this.engine.ssh(hostConfig); await $ssh.uploadFile(sourceInfo.path, destInfo.path); } else { throw new Error('Direct SSH to SSH copy not supported yet'); } return; } if (sourceInfo.type === 'docker' || destInfo.type === 'docker') { const containerName = sourceInfo.type === 'docker' ? sourceInfo.container : destInfo.container; const containerConfig = await this.resolveContainer(containerName); const container = await this.engine.docker(containerConfig).start(); if (sourceInfo.type === 'docker' && destInfo.type === 'local') { await container.copyFrom(sourceInfo.path, destInfo.path); } else if (sourceInfo.type === 'local' && destInfo.type === 'docker') { await container.copyTo(sourceInfo.path, destInfo.path); } else { throw new Error('Direct container to container copy not supported yet'); } return; } if (sourceInfo.type === 'k8s' || destInfo.type === 'k8s') { const podName = sourceInfo.type === 'k8s' ? sourceInfo.pod : destInfo.pod; const podConfig = await this.resolvePod(podName); const k8sContext = this.engine.k8s(podConfig); const pod = k8sContext.pod(podConfig.pod); if (sourceInfo.type === 'k8s' && destInfo.type === 'local') { await pod.copyFrom(sourceInfo.path, destInfo.path); } else if (sourceInfo.type === 'local' && destInfo.type === 'k8s') { await pod.copyTo(sourceInfo.path, destInfo.path); } else { throw new Error('Direct pod to pod copy not supported yet'); } } } async forward(source, localPort) { const parts = source.split(':'); if (parts[0] === 'pod') { const [, podName, remotePort] = parts; if (!podName || !remotePort) { throw new Error('Invalid pod forward format. Use: pod:name:port'); } const podConfig = await this.resolvePod(podName); const k8sContext = this.engine.k8s(podConfig); const pod = k8sContext.pod(podConfig.pod); if (localPort) { return pod.portForward(localPort, parseInt(remotePort, 10)); } else { return pod.portForwardDynamic(parseInt(remotePort, 10)); } } else if (parts[0] === 'container') { const [, containerName, remotePort] = parts; throw new Error('Docker port forwarding not implemented yet'); } else { const [hostName, remotePort] = parts; if (!hostName || !remotePort) { throw new Error('Invalid SSH tunnel format. Use: host:port'); } const hostConfig = await this.resolveHost(hostName); const $ssh = this.engine.ssh(hostConfig); const tunnel = await $ssh.tunnel({ localPort: localPort || 0, remoteHost: 'localhost', remotePort: parseInt(remotePort, 10) }); return tunnel; } } async logs(source, options = {}) { if (source.startsWith('container:')) { const containerName = source.substring(10); const containerConfig = await this.resolveContainer(containerName); const container = await this.engine.docker(containerConfig).start(); if (options.follow && options.onData) { await container.streamLogs(options.onData, { follow: true, tail: options.tail, timestamps: options.timestamps }); } else { return container.logs({ tail: options.tail, timestamps: options.timestamps }); } } else if (source.startsWith('pod:')) { const podName = source.substring(4); const podConfig = await this.resolvePod(podName); const k8sContext = this.engine.k8s(podConfig); const pod = k8sContext.pod(podConfig.pod); if (options.follow && options.onData) { return pod.streamLogs(options.onData, { follow: true, tail: options.tail, timestamps: options.timestamps }); } else { return pod.logs({ tail: options.tail, timestamps: options.timestamps }); } } else if (source.includes(':')) { const [hostName, filePath] = source.split(':', 2); if (!hostName || !filePath) { throw new Error('Invalid SSH log source format. Use: host:path'); } const hostConfig = await this.resolveHost(hostName); const $ssh = this.engine.ssh(hostConfig); if (options.follow) { const tailCmd = `tail -f ${options.tail ? `-n ${options.tail}` : ''} ${filePath}`; const proc = $ssh `${tailCmd}`; if (options.onData) { const stream = await proc; options.onData(stream.stdout); } return proc; } else { const tailCmd = `tail ${options.tail ? `-n ${options.tail}` : '-n 50'} ${filePath}`; const result = await $ssh `${tailCmd}`; return result.stdout; } } else { if (options.follow) { const tailCmd = `tail -f ${options.tail ? `-n ${options.tail}` : ''} ${source}`; const proc = this.engine.run `${tailCmd}`; if (options.onData) { const stream = await proc; options.onData(stream.stdout); } return proc; } else { const tailCmd = `tail ${options.tail ? `-n ${options.tail}` : '-n 50'} ${source}`; const result = await this.engine.run `${tailCmd}`; return result.stdout; } } } async smart(command) { const parts = command.split(' '); const firstPart = parts[0]; const restCommand = parts.slice(1).join(' '); try { const config = await unifiedConfig.load(); if (firstPart && config.hosts?.[firstPart]) { const result = await this.onHost(firstPart, restCommand); return Array.isArray(result) ? result[0] : result; } } catch { } try { const config = await unifiedConfig.load(); if (firstPart && config.containers?.[firstPart]) { return await this.in(firstPart, restCommand); } } catch { } try { const config = await unifiedConfig.load(); if (firstPart && config.pods?.[firstPart]) { return await this.in(`pod:${firstPart}`, restCommand); } } catch { } return await this.engine.run `${command}`; } async resolveHost(name) { const config = await unifiedConfig.load(); const hostConfig = config.hosts?.[name]; if (!hostConfig) { return { host: name }; } return unifiedConfig.hostToSSHOptions(name); } async resolveContainer(name) { const config = await unifiedConfig.load(); const containerConfig = config.containers?.[name]; if (!containerConfig) { return { container: name }; } return { ...containerConfig, container: containerConfig.container || containerConfig.name || name }; } async resolvePod(name) { const config = await unifiedConfig.load(); const podConfig = config.pods?.[name]; if (!podConfig) { return { pod: name }; } return unifiedConfig.podToK8sOptions(name); } parseLocation(location) { if (location.startsWith('container:')) { const [containerPart, ...pathParts] = location.substring(10).split(':'); return { type: 'docker', container: containerPart, path: pathParts.join(':') || '/' }; } if (location.startsWith('pod:')) { const [podPart, ...pathParts] = location.substring(4).split(':'); return { type: 'k8s', pod: podPart, path: pathParts.join(':') || '/' }; } if (location.includes(':') && !location.match(/^[a-zA-Z]:\\/)) { const [host, ...pathParts] = location.split(':'); return { type: 'ssh', host, path: pathParts.join(':') }; } return { type: 'local', path: location }; } } export function attachConvenienceMethods(engine) { const api = new ConvenienceAPI(engine); return Object.assign(engine, { onHost: api.onHost.bind(api), in: api.in.bind(api), copy: api.copy.bind(api), forward: api.forward.bind(api), logs: api.logs.bind(api), smart: api.smart.bind(api) }); } //# sourceMappingURL=convenience.js.map