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