@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
260 lines • 9.66 kB
JavaScript
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